1 /*
<lambda>null2  * Copyright (C) 2017 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.impl.model
17 
18 import androidx.annotation.IntRange
19 import androidx.annotation.RestrictTo
20 import androidx.arch.core.util.Function
21 import androidx.room.ColumnInfo
22 import androidx.room.Embedded
23 import androidx.room.Entity
24 import androidx.room.Index
25 import androidx.room.PrimaryKey
26 import androidx.room.Relation
27 import androidx.work.BackoffPolicy
28 import androidx.work.Constraints
29 import androidx.work.Data
30 import androidx.work.Logger
31 import androidx.work.OutOfQuotaPolicy
32 import androidx.work.OverwritingInputMerger
33 import androidx.work.PeriodicWorkRequest.Companion.MIN_PERIODIC_FLEX_MILLIS
34 import androidx.work.PeriodicWorkRequest.Companion.MIN_PERIODIC_INTERVAL_MILLIS
35 import androidx.work.WorkInfo
36 import androidx.work.WorkRequest
37 import java.util.UUID
38 
39 // TODO: make a immutable
40 /** Stores information about a logical unit of work. */
41 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
42 @Entity(indices = [Index(value = ["schedule_requested_at"]), Index(value = ["last_enqueue_time"])])
43 data class WorkSpec(
44     @JvmField @ColumnInfo(name = "id") @PrimaryKey val id: String,
45     @JvmField @ColumnInfo(name = "state") var state: WorkInfo.State = WorkInfo.State.ENQUEUED,
46     @JvmField @ColumnInfo(name = "worker_class_name") var workerClassName: String,
47     @JvmField
48     @ColumnInfo(name = "input_merger_class_name")
49     var inputMergerClassName: String = OverwritingInputMerger::class.java.name,
50     @JvmField @ColumnInfo(name = "input") var input: Data = Data.EMPTY,
51     @JvmField @ColumnInfo(name = "output") var output: Data = Data.EMPTY,
52     @JvmField @ColumnInfo(name = "initial_delay") var initialDelay: Long = 0,
53     @JvmField @ColumnInfo(name = "interval_duration") var intervalDuration: Long = 0,
54     @JvmField @ColumnInfo(name = "flex_duration") var flexDuration: Long = 0,
55     @JvmField @Embedded var constraints: Constraints = Constraints.NONE,
56     @JvmField
57     @ColumnInfo(name = "run_attempt_count")
58     @IntRange(from = 0)
59     var runAttemptCount: Int = 0,
60     @JvmField
61     @ColumnInfo(name = "backoff_policy")
62     var backoffPolicy: BackoffPolicy = BackoffPolicy.EXPONENTIAL,
63     @JvmField
64     @ColumnInfo(name = "backoff_delay_duration")
65     var backoffDelayDuration: Long = WorkRequest.DEFAULT_BACKOFF_DELAY_MILLIS,
66 
67     /** Time in millis when work was marked as ENQUEUED in database. */
68     @JvmField
69     @ColumnInfo(name = "last_enqueue_time", defaultValue = "$NOT_ENQUEUED")
70     var lastEnqueueTime: Long = NOT_ENQUEUED,
71     @JvmField
72     @ColumnInfo(name = "minimum_retention_duration")
73     var minimumRetentionDuration: Long = 0,
74 
75     /**
76      * This field tells us if this [WorkSpec] instance, is actually currently scheduled and being
77      * counted against the `SCHEDULER_LIMIT`. This bit is reset for PeriodicWorkRequests in API <
78      * 23, because AlarmManager does not know of PeriodicWorkRequests. So for the next request to be
79      * rescheduled this field has to be reset to `SCHEDULE_NOT_REQUESTED_AT`. For the JobScheduler
80      * implementation, we don't reset this field because JobScheduler natively supports
81      * PeriodicWorkRequests.
82      */
83     @JvmField
84     @ColumnInfo(name = "schedule_requested_at")
85     var scheduleRequestedAt: Long = SCHEDULE_NOT_REQUESTED_YET,
86 
87     /**
88      * This is `true` when the WorkSpec needs to be hosted by a foreground service or a high
89      * priority job.
90      */
91     @JvmField @ColumnInfo(name = "run_in_foreground") var expedited: Boolean = false,
92 
93     /**
94      * When set to `true` this [WorkSpec] falls back to a regular job when an application runs out
95      * of expedited job quota.
96      */
97     @JvmField
98     @ColumnInfo(name = "out_of_quota_policy")
99     var outOfQuotaPolicy: OutOfQuotaPolicy = OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST,
100 
101     /**
102      * A number of periods that this worker has already run. This has no real implication for
103      * OneTimeWork.
104      */
105     @ColumnInfo(name = "period_count", defaultValue = "0") var periodCount: Int = 0,
106     @ColumnInfo(defaultValue = "0") val generation: Int = 0,
107 
108     /**
109      * If not Long.MAX_VALUE, this will be the next schedule time, regardless of configured delay.
110      * Only valid for periodic workers
111      */
112     @ColumnInfo(name = "next_schedule_time_override", defaultValue = Long.MAX_VALUE.toString())
113     var nextScheduleTimeOverride: Long = Long.MAX_VALUE,
114 
115     /**
116      * Generation counter that tracks only the nextScheduleTimeOverride version, which allows the
117      * overall generation to be incremented without clearing the nextScheduleTimeOverride. Eg. while
118      * an override is set, a WorkSpec's constraints are changed using UPDATE, but the override time
119      * is neither set nor cleared.
120      *
121      * We could implicitly cancel the nextScheduleTimeOverride since it was not specified in the
122      * update. However, this would require every caller to know that there is an override, and what
123      * the value of that time was, in order to make unrelated changes.
124      *
125      * Instead, we keep track of a separate override schedule generation, so only updates that
126      * change or cancel the nextScheduleTimeOverride will affect the override generation.
127      *
128      * This allows WorkSpec changes to be made mid-worker run, and WorkerWrapper can still correctly
129      * clear a previous nextScheduleTimeOverride upon conclusion by consulting the
130      * overrideGeneration instead of the overall generation.
131      */
132     @ColumnInfo(name = "next_schedule_time_override_generation", defaultValue = "0")
133     // If reset every min interval, would last 500 years.
134     var nextScheduleTimeOverrideGeneration: Int = 0,
135     @ColumnInfo(name = "stop_reason", defaultValue = "${WorkInfo.STOP_REASON_NOT_STOPPED}")
136     val stopReason: Int = WorkInfo.STOP_REASON_NOT_STOPPED,
137     @ColumnInfo(name = "trace_tag") var traceTag: String? = null,
138     @ColumnInfo(name = "backoff_on_system_interruptions")
139     var backOffOnSystemInterruptions: Boolean? = false,
140 ) {
141     constructor(
142         id: String,
143         workerClassName_: String
144     ) : this(id = id, workerClassName = workerClassName_)
145 
146     constructor(
147         newId: String,
148         other: WorkSpec
149     ) : this(
150         id = newId,
151         workerClassName = other.workerClassName,
152         state = other.state,
153         inputMergerClassName = other.inputMergerClassName,
154         input = Data(other.input),
155         output = Data(other.output),
156         initialDelay = other.initialDelay,
157         intervalDuration = other.intervalDuration,
158         flexDuration = other.flexDuration,
159         constraints = Constraints(other.constraints),
160         runAttemptCount = other.runAttemptCount,
161         backoffPolicy = other.backoffPolicy,
162         backoffDelayDuration = other.backoffDelayDuration,
163         lastEnqueueTime = other.lastEnqueueTime,
164         minimumRetentionDuration = other.minimumRetentionDuration,
165         scheduleRequestedAt = other.scheduleRequestedAt,
166         expedited = other.expedited,
167         outOfQuotaPolicy = other.outOfQuotaPolicy,
168         periodCount = other.periodCount,
169         nextScheduleTimeOverride = other.nextScheduleTimeOverride,
170         nextScheduleTimeOverrideGeneration = other.nextScheduleTimeOverrideGeneration,
171         stopReason = other.stopReason,
172         traceTag = other.traceTag,
173         backOffOnSystemInterruptions = other.backOffOnSystemInterruptions,
174     )
175 
176     /** @param backoffDelayDuration The backoff delay duration in milliseconds */
177     fun setBackoffDelayDuration(backoffDelayDuration: Long) {
178         if (backoffDelayDuration > WorkRequest.MAX_BACKOFF_MILLIS) {
179             Logger.get().warning(TAG, "Backoff delay duration exceeds maximum value")
180         }
181         if (backoffDelayDuration < WorkRequest.MIN_BACKOFF_MILLIS) {
182             Logger.get().warning(TAG, "Backoff delay duration less than minimum value")
183         }
184 
185         this.backoffDelayDuration =
186             backoffDelayDuration.coerceIn(
187                 WorkRequest.MIN_BACKOFF_MILLIS,
188                 WorkRequest.MAX_BACKOFF_MILLIS
189             )
190     }
191 
192     val isPeriodic: Boolean
193         get() = intervalDuration != 0L
194 
195     val isBackedOff: Boolean
196         get() = state == WorkInfo.State.ENQUEUED && runAttemptCount > 0
197 
198     /**
199      * Sets the periodic interval for this unit of work.
200      *
201      * @param intervalDuration The interval in milliseconds
202      */
203     fun setPeriodic(intervalDuration: Long) {
204         if (intervalDuration < MIN_PERIODIC_INTERVAL_MILLIS) {
205             Logger.get()
206                 .warning(
207                     TAG,
208                     "Interval duration lesser than minimum allowed value; " +
209                         "Changed to $MIN_PERIODIC_INTERVAL_MILLIS"
210                 )
211         }
212         setPeriodic(
213             intervalDuration.coerceAtLeast(MIN_PERIODIC_INTERVAL_MILLIS),
214             intervalDuration.coerceAtLeast(MIN_PERIODIC_INTERVAL_MILLIS)
215         )
216     }
217 
218     /**
219      * Sets the periodic interval for this unit of work.
220      *
221      * @param intervalDuration The interval in milliseconds
222      * @param flexDuration The flex duration in milliseconds
223      */
224     fun setPeriodic(intervalDuration: Long, flexDuration: Long) {
225         if (intervalDuration < MIN_PERIODIC_INTERVAL_MILLIS) {
226             Logger.get()
227                 .warning(
228                     TAG,
229                     "Interval duration lesser than minimum allowed value; " +
230                         "Changed to $MIN_PERIODIC_INTERVAL_MILLIS"
231                 )
232         }
233 
234         this.intervalDuration = intervalDuration.coerceAtLeast(MIN_PERIODIC_INTERVAL_MILLIS)
235 
236         if (flexDuration < MIN_PERIODIC_FLEX_MILLIS) {
237             Logger.get()
238                 .warning(
239                     TAG,
240                     "Flex duration lesser than minimum allowed value; " +
241                         "Changed to $MIN_PERIODIC_FLEX_MILLIS"
242                 )
243         }
244         if (flexDuration > this.intervalDuration) {
245             Logger.get()
246                 .warning(
247                     TAG,
248                     "Flex duration greater than interval duration; Changed to $intervalDuration"
249                 )
250         }
251         this.flexDuration = flexDuration.coerceIn(MIN_PERIODIC_FLEX_MILLIS, this.intervalDuration)
252     }
253 
254     /**
255      * Calculates the UTC time at which this [WorkSpec] should be allowed to run. This method
256      * accounts for work that is backed off or periodic.
257      *
258      * If Backoff Policy is set to [BackoffPolicy.EXPONENTIAL], then delay increases at an
259      * exponential rate with respect to the run attempt count and is capped at
260      * [WorkRequest.MAX_BACKOFF_MILLIS].
261      *
262      * If Backoff Policy is set to [BackoffPolicy.LINEAR], then delay increases at an linear rate
263      * with respect to the run attempt count and is capped at [WorkRequest.MAX_BACKOFF_MILLIS].
264      *
265      * Based on {@see
266      * https://android.googlesource.com/platform/frameworks/base/+/master/services/core/java/com/android/server/job/JobSchedulerService.java#1125}
267      *
268      * Note that this runtime is for WorkManager internal use and may not match what the OS
269      * considers to be the next runtime.
270      *
271      * For jobs with constraints, this represents the earliest time at which constraints should be
272      * monitored for this work.
273      *
274      * For jobs without constraints, this represents the earliest time at which this work is allowed
275      * to run.
276      *
277      * @return UTC time at which this [WorkSpec] should be allowed to run.
278      */
279     fun calculateNextRunTime(): Long {
280         return calculateNextRunTime(
281             isBackedOff = isBackedOff,
282             runAttemptCount = runAttemptCount,
283             backoffPolicy = backoffPolicy,
284             backoffDelayDuration = backoffDelayDuration,
285             lastEnqueueTime = lastEnqueueTime,
286             periodCount = periodCount,
287             isPeriodic = isPeriodic,
288             initialDelay = initialDelay,
289             flexDuration = flexDuration,
290             intervalDuration = intervalDuration,
291             nextScheduleTimeOverride = nextScheduleTimeOverride
292         )
293     }
294 
295     /** @return `true` if the [WorkSpec] has constraints. */
296     fun hasConstraints(): Boolean {
297         return Constraints.NONE != constraints
298     }
299 
300     override fun toString(): String {
301         return "{WorkSpec: $id}"
302     }
303 
304     /** A POJO containing the ID and state of a WorkSpec. */
305     data class IdAndState(
306         @JvmField @ColumnInfo(name = "id") var id: String,
307         @JvmField @ColumnInfo(name = "state") var state: WorkInfo.State,
308     )
309 
310     /** A POJO containing externally queryable info for the WorkSpec. */
311     data class WorkInfoPojo(
312         @ColumnInfo(name = "id") val id: String,
313         @ColumnInfo(name = "state") val state: WorkInfo.State,
314         @ColumnInfo(name = "output") val output: Data,
315         @ColumnInfo(name = "initial_delay") val initialDelay: Long = 0,
316         @ColumnInfo(name = "interval_duration") val intervalDuration: Long = 0,
317         @ColumnInfo(name = "flex_duration") val flexDuration: Long = 0,
318         @Embedded val constraints: Constraints,
319         @ColumnInfo(name = "run_attempt_count") val runAttemptCount: Int,
320         @ColumnInfo(name = "backoff_policy")
321         var backoffPolicy: BackoffPolicy = BackoffPolicy.EXPONENTIAL,
322         @ColumnInfo(name = "backoff_delay_duration")
323         var backoffDelayDuration: Long = WorkRequest.DEFAULT_BACKOFF_DELAY_MILLIS,
324         @ColumnInfo(name = "last_enqueue_time") var lastEnqueueTime: Long = 0,
325         @ColumnInfo(name = "period_count", defaultValue = "0") var periodCount: Int = 0,
326         @ColumnInfo(name = "generation") val generation: Int,
327         @ColumnInfo(name = "next_schedule_time_override") val nextScheduleTimeOverride: Long,
328         @ColumnInfo(name = "stop_reason") val stopReason: Int,
329         @Relation(
330             parentColumn = "id",
331             entityColumn = "work_spec_id",
332             entity = WorkTag::class,
333             projection = ["tag"]
334         )
335         val tags: List<String>,
336 
337         // This is actually a 1-1 relationship. However Room 2.1 models the type as a List.
338         // This will change in Room 2.2
339         @Relation(
340             parentColumn = "id",
341             entityColumn = "work_spec_id",
342             entity = WorkProgress::class,
343             projection = ["progress"]
344         )
345         val progress: List<Data>,
346     ) {
347         val isPeriodic: Boolean
348             get() = intervalDuration != 0L
349 
350         val isBackedOff: Boolean
351             get() = state == WorkInfo.State.ENQUEUED && runAttemptCount > 0
352 
353         /**
354          * Converts this POJO to a [WorkInfo].
355          *
356          * @return The [WorkInfo] represented by this POJO
357          */
358         fun toWorkInfo(): WorkInfo {
359             val progress = if (progress.isNotEmpty()) progress[0] else Data.EMPTY
360             return WorkInfo(
361                 UUID.fromString(id),
362                 state,
363                 HashSet(tags),
364                 output,
365                 progress,
366                 runAttemptCount,
367                 generation,
368                 constraints,
369                 initialDelay,
370                 getPeriodicityOrNull(),
371                 calculateNextRunTimeMillis(),
372                 stopReason,
373             )
374         }
375 
376         private fun getPeriodicityOrNull() =
377             if (intervalDuration != 0L) WorkInfo.PeriodicityInfo(intervalDuration, flexDuration)
378             else null
379 
380         private fun calculateNextRunTimeMillis(): Long {
381             return if (state == WorkInfo.State.ENQUEUED)
382                 calculateNextRunTime(
383                     isBackedOff = isBackedOff,
384                     runAttemptCount = runAttemptCount,
385                     backoffPolicy = backoffPolicy,
386                     backoffDelayDuration = backoffDelayDuration,
387                     lastEnqueueTime = lastEnqueueTime,
388                     periodCount = periodCount,
389                     isPeriodic = isPeriodic,
390                     initialDelay = initialDelay,
391                     flexDuration = flexDuration,
392                     intervalDuration = intervalDuration,
393                     nextScheduleTimeOverride = nextScheduleTimeOverride
394                 )
395             else Long.MAX_VALUE
396         }
397     }
398 
399     companion object {
400         private val TAG = Logger.tagWithPrefix("WorkSpec")
401         const val SCHEDULE_NOT_REQUESTED_YET: Long = -1
402 
403         @JvmField
404         val WORK_INFO_MAPPER: Function<List<WorkInfoPojo>, List<WorkInfo>> = Function { input ->
405             input?.map { it.toWorkInfo() }
406         }
407 
408         fun calculateNextRunTime(
409             isBackedOff: Boolean,
410             runAttemptCount: Int,
411             backoffPolicy: BackoffPolicy,
412             backoffDelayDuration: Long,
413             lastEnqueueTime: Long,
414             periodCount: Int,
415             isPeriodic: Boolean,
416             initialDelay: Long,
417             flexDuration: Long,
418             intervalDuration: Long,
419             nextScheduleTimeOverride: Long,
420         ): Long {
421             // Override takes priority over backoff, but only applies to periodic work.
422             return if (nextScheduleTimeOverride != Long.MAX_VALUE && isPeriodic) {
423                 return if (periodCount == 0) nextScheduleTimeOverride
424                 else
425                     nextScheduleTimeOverride.coerceAtLeast(
426                         lastEnqueueTime + MIN_PERIODIC_INTERVAL_MILLIS
427                     )
428             } else if (isBackedOff) {
429                 val isLinearBackoff = backoffPolicy == BackoffPolicy.LINEAR
430                 val delay =
431                     if (isLinearBackoff) backoffDelayDuration * runAttemptCount
432                     else Math.scalb(backoffDelayDuration.toFloat(), runAttemptCount - 1).toLong()
433                 lastEnqueueTime + delay.coerceAtMost(WorkRequest.MAX_BACKOFF_MILLIS)
434             } else if (isPeriodic) {
435                 // The first run of a periodic work request is immediate in JobScheduler, so
436                 // don't apply intervalDuration to the first run.
437                 var schedule =
438                     if (periodCount == 0) lastEnqueueTime + initialDelay
439                     else lastEnqueueTime + intervalDuration
440 
441                 val isFlexApplicable = flexDuration != intervalDuration
442                 // Flex only applies to the first run of a Periodic worker, to avoid
443                 // repeatedly pushing the schedule forward on every period.
444                 if (isFlexApplicable && periodCount == 0) {
445                     // With flex, the first run does not run immediately, but instead respects
446                     // the first interval duration.
447                     schedule += (intervalDuration - flexDuration)
448                 }
449 
450                 schedule
451             } else if (lastEnqueueTime == NOT_ENQUEUED) {
452                 // If never enqueued, we aren't scheduled to run.
453                 Long.MAX_VALUE // 200 million years.
454             } else {
455                 lastEnqueueTime + initialDelay
456             }
457         }
458     }
459 }
460 
461 data class WorkGenerationalId(val workSpecId: String, val generation: Int)
462 
WorkSpecnull463 fun WorkSpec.generationalId() = WorkGenerationalId(id, generation)
464 
465 private const val NOT_ENQUEUED = -1L
466