1 /*
2  * Copyright 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package androidx.work
17 
18 import android.annotation.SuppressLint
19 import android.os.Build
20 import androidx.annotation.RequiresApi
21 import androidx.annotation.RestrictTo
22 import androidx.annotation.VisibleForTesting
23 import androidx.work.impl.model.WorkSpec
24 import androidx.work.impl.utils.toMillisCompat
25 import java.time.Duration
26 import java.util.UUID
27 import java.util.concurrent.TimeUnit
28 
29 /**
30  * The base class for specifying parameters for work that should be enqueued in [WorkManager]. There
31  * are two concrete implementations of this class: [OneTimeWorkRequest] and [PeriodicWorkRequest].
32  */
33 abstract class WorkRequest
34 internal constructor(
35     /** The unique identifier associated with this unit of work. */
36     open val id: UUID,
37     /** The [WorkSpec] associated with this unit of work. */
38     @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) val workSpec: WorkSpec,
39     /** The tags associated with this unit of work. */
40     @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) val tags: Set<String>
41 ) {
42 
43     /**
44      * Gets the string for the unique identifier associated with this unit of work.
45      *
46      * @return The string identifier for this unit of work
47      */
48     @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
49     val stringId: String
50         get() = id.toString()
51 
52     /**
53      * A builder for [WorkRequest]s. There are two concrete implementations of this class:
54      * [OneTimeWorkRequest.Builder] and [PeriodicWorkRequest.Builder].
55      */
56     abstract class Builder<B : Builder<B, *>, W : WorkRequest>
57     internal constructor(internal val workerClass: Class<out ListenableWorker>) {
58         internal var backoffCriteriaSet = false
59         internal var id: UUID = UUID.randomUUID()
60         internal var workSpec: WorkSpec = WorkSpec(id.toString(), workerClass.name)
61         internal val tags: MutableSet<String> = mutableSetOf(workerClass.name)
62 
63         /**
64          * Sets a unique identifier for this unit of work.
65          *
66          * The id can be useful when retrieving [WorkInfo] by `id` or when trying to update an
67          * existing work. For example, using [WorkManager.updateWork] requires that the work has an
68          * id.
69          *
70          * @param id The unique identifier for this unit of work.
71          * @return The current [Builder]
72          */
73         @SuppressWarnings("SetterReturnsThis")
setIdnull74         fun setId(id: UUID): B {
75             this.id = id
76             workSpec = WorkSpec(id.toString(), workSpec)
77             return thisObject
78         }
79 
80         /**
81          * Sets the backoff policy and backoff delay for the work. The default values are
82          * [BackoffPolicy.EXPONENTIAL] and [WorkRequest#DEFAULT_BACKOFF_DELAY_MILLIS], respectively.
83          * `backoffDelay` will be clamped between [WorkRequest.MIN_BACKOFF_MILLIS] and
84          * [WorkRequest.MAX_BACKOFF_MILLIS].
85          *
86          * @param backoffPolicy The [BackoffPolicy] to use when increasing backoff time
87          * @param backoffDelay Time to wait before retrying the work in `timeUnit` units
88          * @param timeUnit The [TimeUnit] for `backoffDelay`
89          * @return The current [Builder]
90          */
setBackoffCriterianull91         fun setBackoffCriteria(
92             backoffPolicy: BackoffPolicy,
93             backoffDelay: Long,
94             timeUnit: TimeUnit
95         ): B {
96             backoffCriteriaSet = true
97             workSpec.backoffPolicy = backoffPolicy
98             workSpec.setBackoffDelayDuration(timeUnit.toMillis(backoffDelay))
99             return thisObject
100         }
101 
102         /**
103          * Sets the backoff policy and backoff delay for the work. The default values are
104          * [BackoffPolicy.EXPONENTIAL] and [WorkRequest#DEFAULT_BACKOFF_DELAY_MILLIS], respectively.
105          * `duration` will be clamped between [WorkRequest.MIN_BACKOFF_MILLIS] and
106          * [WorkRequest.MAX_BACKOFF_MILLIS].
107          *
108          * @param backoffPolicy The [BackoffPolicy] to use when increasing backoff time
109          * @param duration Time to wait before retrying the work
110          * @return The current [Builder]
111          */
112         @RequiresApi(26)
setBackoffCriterianull113         fun setBackoffCriteria(backoffPolicy: BackoffPolicy, duration: Duration): B {
114             backoffCriteriaSet = true
115             workSpec.backoffPolicy = backoffPolicy
116             workSpec.setBackoffDelayDuration(duration.toMillisCompat())
117             return thisObject
118         }
119 
120         /**
121          * Adds constraints to the [WorkRequest].
122          *
123          * @param constraints The constraints for the work
124          * @return The current [Builder]
125          */
setConstraintsnull126         fun setConstraints(constraints: Constraints): B {
127             workSpec.constraints = constraints
128             return thisObject
129         }
130 
131         /**
132          * Adds input [Data] to the work. If a worker has prerequisites in its chain, this Data will
133          * be merged with the outputs of the prerequisites using an [InputMerger].
134          *
135          * @param inputData key/value pairs that will be provided to the worker
136          * @return The current [Builder]
137          */
setInputDatanull138         fun setInputData(inputData: Data): B {
139             workSpec.input = inputData
140             return thisObject
141         }
142 
143         /**
144          * Adds a tag for the work. You can query and cancel work by tags. Tags are particularly
145          * useful for modules or libraries to find and operate on their own work.
146          *
147          * @param tag A tag for identifying the work in queries.
148          * @return The current [Builder]
149          */
addTagnull150         fun addTag(tag: String): B {
151             tags.add(tag)
152             return thisObject
153         }
154 
155         /**
156          * Specifies the name of the trace span to be used by [WorkManager] when executing the
157          * specified [WorkRequest].
158          *
159          * [WorkManager] uses the simple name of the [ListenableWorker] class truncated to a `127`
160          * character string, as the [traceTag] by default.
161          *
162          * You should override the [traceTag], when you are using [ListenableWorker] delegation via
163          * a [WorkerFactory].
164          *
165          * @param traceTag The name of the trace tag, truncate to a `127` character string if
166          *   necessary.
167          * @return The current [Builder]
168          */
169         @Suppress("MissingGetterMatchingBuilder")
170         @SuppressWarnings("SetterReturnsThis")
setTraceTagnull171         fun setTraceTag(traceTag: String): B {
172             workSpec.traceTag = traceTag
173             return thisObject
174         }
175 
176         /**
177          * Specifies that the results of this work should be kept for at least the specified amount
178          * of time. After this time has elapsed, the results **may** be pruned at the discretion of
179          * WorkManager when there are no pending dependent jobs.
180          *
181          * When the results of a work are pruned, it becomes impossible to query for its [WorkInfo].
182          *
183          * Specifying a long duration here may adversely affect performance in terms of app storage
184          * and database query time.
185          *
186          * @param duration The minimum duration of time (in `timeUnit` units) to keep the results of
187          *   this work
188          * @param timeUnit The unit of time for `duration`
189          * @return The current [Builder]
190          */
keepResultsForAtLeastnull191         fun keepResultsForAtLeast(duration: Long, timeUnit: TimeUnit): B {
192             workSpec.minimumRetentionDuration = timeUnit.toMillis(duration)
193             return thisObject
194         }
195 
196         /**
197          * Specifies that the backoff policy (as specified via [setBackoffCriteria]) will be applied
198          * when work is interrupted by the system without the app requesting it. This might happen
199          * when the [ListenableWorker] runs longer than it should, or when constraints defined for a
200          * given [ListenableWorker] are unmet.
201          *
202          * @return The current [Builder]
203          * @see setBackoffCriteria
204          */
205         @ExperimentalWorkRequestBuilderApi
206         @Suppress("MissingGetterMatchingBuilder")
207         @SuppressWarnings("SetterReturnsThis")
setBackoffForSystemInterruptionsnull208         fun setBackoffForSystemInterruptions(): B {
209             workSpec.backOffOnSystemInterruptions = true
210             return thisObject
211         }
212 
213         /**
214          * Specifies that the results of this work should be kept for at least the specified amount
215          * of time. After this time has elapsed, the results may be pruned at the discretion of
216          * WorkManager when this WorkRequest has reached a finished state (see
217          * [WorkInfo.State.isFinished]) and there are no pending dependent jobs.
218          *
219          * When the results of a work are pruned, it becomes impossible to query for its [WorkInfo].
220          *
221          * Specifying a long duration here may adversely affect performance in terms of app storage
222          * and database query time.
223          *
224          * @param duration The minimum duration of time to keep the results of this work
225          * @return The current [Builder]
226          */
227         @RequiresApi(26)
keepResultsForAtLeastnull228         fun keepResultsForAtLeast(duration: Duration): B {
229             workSpec.minimumRetentionDuration = duration.toMillisCompat()
230             return thisObject
231         }
232 
233         /**
234          * Sets an initial delay for the [WorkRequest].
235          *
236          * @param duration The length of the delay in `timeUnit` units
237          * @param timeUnit The units of time for `duration`
238          * @return The current [Builder]
239          * @throws IllegalArgumentException if the given initial delay will push the execution time
240          *   past `Long.MAX_VALUE` and cause an overflow
241          */
setInitialDelaynull242         open fun setInitialDelay(duration: Long, timeUnit: TimeUnit): B {
243             workSpec.initialDelay = timeUnit.toMillis(duration)
244             require(Long.MAX_VALUE - System.currentTimeMillis() > workSpec.initialDelay) {
245                 ("The given initial delay is too large and will cause an overflow!")
246             }
247             return thisObject
248         }
249 
250         /**
251          * Sets an initial delay for the [WorkRequest].
252          *
253          * @param duration The length of the delay
254          * @return The current [Builder]
255          * @throws IllegalArgumentException if the given initial delay will push the execution time
256          *   past `Long.MAX_VALUE` and cause an overflow
257          */
258         @RequiresApi(26)
setInitialDelaynull259         open fun setInitialDelay(duration: Duration): B {
260             workSpec.initialDelay = duration.toMillisCompat()
261             require(Long.MAX_VALUE - System.currentTimeMillis() > workSpec.initialDelay) {
262                 "The given initial delay is too large and will cause an overflow!"
263             }
264             return thisObject
265         }
266 
267         /**
268          * Marks the [WorkRequest] as important to the user. In this case, WorkManager provides an
269          * additional signal to the OS that this work is important.
270          *
271          * @param policy The [OutOfQuotaPolicy] to be used.
272          */
273         @SuppressLint("MissingGetterMatchingBuilder")
setExpeditednull274         open fun setExpedited(policy: OutOfQuotaPolicy): B {
275             workSpec.expedited = true
276             workSpec.outOfQuotaPolicy = policy
277             return thisObject
278         }
279 
280         /**
281          * Builds a [WorkRequest] based on this [Builder].
282          *
283          * @return A [WorkRequest] based on this [Builder]
284          */
buildnull285         fun build(): W {
286             val returnValue = buildInternal()
287             val constraints = workSpec.constraints
288             // Check for unsupported constraints.
289             val hasUnsupportedConstraints =
290                 (Build.VERSION.SDK_INT >= 24 && constraints.hasContentUriTriggers() ||
291                     constraints.requiresBatteryNotLow() ||
292                     constraints.requiresCharging() ||
293                     Build.VERSION.SDK_INT >= 23 && constraints.requiresDeviceIdle())
294             if (workSpec.expedited) {
295                 require(!hasUnsupportedConstraints) {
296                     "Expedited jobs only support network and storage constraints"
297                 }
298                 require(workSpec.initialDelay <= 0) { "Expedited jobs cannot be delayed" }
299             }
300             val traceTag = workSpec.traceTag
301             if (traceTag == null) {
302                 // Derive a trace tag based on the fully qualified class name if
303                 // one has not already been defined.
304                 workSpec.traceTag = deriveTraceTagFromClassName(workSpec.workerClassName)
305             } else if (traceTag.length > MAX_TRACE_SPAN_LENGTH) {
306                 // If there is a trace tag but it exceeds the limit, then truncate it.
307                 // Since we also pipe this tag to JobInfo we need to not exceed the limit even
308                 // though androidx.tracing.Trace already truncate tags.
309                 workSpec.traceTag = traceTag.take(MAX_TRACE_SPAN_LENGTH)
310             }
311             // Create a new id and WorkSpec so this WorkRequest.Builder can be used multiple times.
312             setId(UUID.randomUUID())
313             return returnValue
314         }
315 
buildInternalnull316         internal abstract fun buildInternal(): W
317 
318         internal abstract val thisObject: B
319 
320         /**
321          * Sets the initial state for this work. Used in testing only.
322          *
323          * @param state The [WorkInfo.State] to set
324          * @return The current [Builder]
325          */
326         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
327         @VisibleForTesting
328         fun setInitialState(state: WorkInfo.State): B {
329             workSpec.state = state
330             return thisObject
331         }
332 
333         /**
334          * Sets the initial run attempt count for this work. Used in testing only.
335          *
336          * @param runAttemptCount The initial run attempt count
337          * @return The current [Builder]
338          */
339         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
340         @VisibleForTesting
setInitialRunAttemptCountnull341         fun setInitialRunAttemptCount(runAttemptCount: Int): B {
342             workSpec.runAttemptCount = runAttemptCount
343             return thisObject
344         }
345 
346         /**
347          * Sets the enqueue time for this work. Used in testing only.
348          *
349          * @param lastEnqueueTime The enqueue time in `timeUnit` units
350          * @param timeUnit The [TimeUnit] for `periodStartTime`
351          * @return The current [Builder]
352          */
353         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
354         @VisibleForTesting
setLastEnqueueTimenull355         fun setLastEnqueueTime(lastEnqueueTime: Long, timeUnit: TimeUnit): B {
356             workSpec.lastEnqueueTime = timeUnit.toMillis(lastEnqueueTime)
357             return thisObject
358         }
359 
360         /**
361          * Sets when the scheduler actually schedules the worker.
362          *
363          * @param scheduleRequestedAt The time at which the scheduler scheduled a worker.
364          * @param timeUnit The [TimeUnit] for `scheduleRequestedAt`
365          * @return The current [Builder]
366          */
367         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
368         @VisibleForTesting
setScheduleRequestedAtnull369         fun setScheduleRequestedAt(scheduleRequestedAt: Long, timeUnit: TimeUnit): B {
370             workSpec.scheduleRequestedAt = timeUnit.toMillis(scheduleRequestedAt)
371             return thisObject
372         }
373     }
374 
375     companion object {
376         /** The default initial backoff time (in milliseconds) for work that has to be retried. */
377         const val DEFAULT_BACKOFF_DELAY_MILLIS = 30000L
378 
379         /** The maximum backoff time (in milliseconds) for work that has to be retried. */
380         @SuppressLint("MinMaxConstant")
381         const val MAX_BACKOFF_MILLIS = 5 * 60 * 60 * 1000L // 5 hours
382 
383         /** The minimum backoff time for work (in milliseconds) that has to be retried. */
384         @SuppressLint("MinMaxConstant") const val MIN_BACKOFF_MILLIS = 10 * 1000L // 10 seconds.
385 
386         /** The maximum length of a trace span. */
387         private const val MAX_TRACE_SPAN_LENGTH = 127
388 
389         /**
390          * The [androidx.tracing.Trace] class already truncates names.
391          *
392          * We try and extract the class name so it does not get truncated, given package name can be
393          * implied from other sources of information.
394          */
deriveTraceTagFromClassNamenull395         private fun deriveTraceTagFromClassName(workerClassName: String): String {
396             val components = workerClassName.split(".")
397             val label =
398                 when (components.size) {
399                     1 -> components[0]
400                     else -> components.last()
401                 }
402             return if (label.length <= MAX_TRACE_SPAN_LENGTH) {
403                 label
404             } else {
405                 label.take(MAX_TRACE_SPAN_LENGTH)
406             }
407         }
408     }
409 }
410