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