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.impl.workers
17
18 import android.content.Context
19 import android.os.Build
20 import androidx.annotation.RestrictTo
21 import androidx.concurrent.futures.await
22 import androidx.work.CoroutineWorker
23 import androidx.work.ListenableWorker
24 import androidx.work.Logger
25 import androidx.work.WorkInfo.Companion.STOP_REASON_NOT_STOPPED
26 import androidx.work.WorkInfo.Companion.STOP_REASON_UNKNOWN
27 import androidx.work.WorkerExceptionInfo
28 import androidx.work.WorkerParameters
29 import androidx.work.impl.WorkManagerImpl
30 import androidx.work.impl.constraints.ConstraintsState.ConstraintsNotMet
31 import androidx.work.impl.constraints.WorkConstraintsTracker
32 import androidx.work.impl.model.WorkSpec
33 import androidx.work.impl.utils.safeAccept
34 import androidx.work.logd
35 import androidx.work.loge
36 import java.util.concurrent.atomic.AtomicInteger
37 import kotlinx.coroutines.CancellationException
38 import kotlinx.coroutines.asCoroutineDispatcher
39 import kotlinx.coroutines.coroutineScope
40 import kotlinx.coroutines.flow.filterIsInstance
41 import kotlinx.coroutines.flow.first
42 import kotlinx.coroutines.flow.onEach
43 import kotlinx.coroutines.launch
44 import kotlinx.coroutines.withContext
45
46 /**
47 * Is an implementation of a [androidx.work.Worker] that can delegate to a different
48 * [androidx.work.Worker] when the constraints are met.
49 */
50 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
51 class ConstraintTrackingWorker(
52 appContext: Context,
53 private val workerParameters: WorkerParameters
54 ) : CoroutineWorker(appContext, workerParameters) {
55
doWorknull56 override suspend fun doWork(): Result {
57 return withContext(backgroundExecutor.asCoroutineDispatcher()) {
58 setupAndRunConstraintTrackingWork()
59 }
60 }
61
setupAndRunConstraintTrackingWorknull62 private suspend fun setupAndRunConstraintTrackingWork(): Result {
63 val className = inputData.getString(ARGUMENT_CLASS_NAME)
64 if (className.isNullOrEmpty()) {
65 loge(TAG) { "No worker to delegate to." }
66 return Result.failure()
67 }
68 val workManagerImpl = WorkManagerImpl.getInstance(applicationContext)
69 // We need to know what the real constraints are for the delegate.
70 val workSpec =
71 workManagerImpl.workDatabase.workSpecDao().getWorkSpec(id.toString())
72 ?: return Result.failure()
73 val workConstraintsTracker = WorkConstraintsTracker(workManagerImpl.trackers)
74 if (!workConstraintsTracker.areAllConstraintsMet(workSpec)) {
75 logd(TAG) { "Constraints not met for delegate $className. Requesting retry." }
76 return Result.retry()
77 }
78 logd(TAG) { "Constraints met for delegate $className" }
79 val delegate =
80 try {
81 workerFactory.createWorkerWithDefaultFallback(
82 applicationContext,
83 className,
84 workerParameters
85 )
86 } catch (e: Throwable) {
87 logd(TAG) { "No worker to delegate to." }
88
89 workManagerImpl.configuration.workerInitializationExceptionHandler?.safeAccept(
90 WorkerExceptionInfo(className, workerParameters, e),
91 TAG
92 )
93 return Result.failure()
94 }
95 val mainThreadExecutor = workerParameters.taskExecutor.mainThreadExecutor
96 return try {
97 withContext(mainThreadExecutor.asCoroutineDispatcher()) {
98 runWorker(delegate, workConstraintsTracker, workSpec)
99 }
100 } catch (cancelled: CancellationException) {
101 // there are two cases when we should propagate stop call:
102 // 1. ConstraintTrackingWorker itself is cancelled
103 // 2. Local constraint tracking failed
104 if (isStopped || cancelled is ConstraintUnsatisfiedException) {
105 val reason =
106 when {
107 Build.VERSION.SDK_INT < Build.VERSION_CODES.S -> STOP_REASON_UNKNOWN
108 isStopped -> stopReason
109 cancelled is ConstraintUnsatisfiedException -> cancelled.stopReason
110 else -> throw IllegalStateException("Unreachable")
111 }
112 delegate.stop(reason)
113 }
114 // if `ConstraintUnsatisfiedException` was thrown, then we should
115 // manually request Retry-ing, because ConstraintTrackingWorker itself
116 // isn't cancelled
117 if (cancelled is ConstraintUnsatisfiedException) Result.retry() else throw cancelled
118 }
119 }
120
runWorkernull121 private suspend fun runWorker(
122 delegate: ListenableWorker,
123 workConstraintsTracker: WorkConstraintsTracker,
124 workSpec: WorkSpec
125 ): Result = coroutineScope {
126 val atomicReason = AtomicInteger(STOP_REASON_NOT_STOPPED)
127 val future = delegate.startWork()
128 val constraintTrackingJob = launch {
129 val reason = workConstraintsTracker.awaitConstraintsNotMet(workSpec)
130 atomicReason.set(reason)
131 future.cancel(true)
132 }
133 try {
134 val result = future.await()
135 result
136 } catch (cancellation: CancellationException) {
137 logd(TAG, cancellation) { "Delegated worker ${delegate.javaClass} was cancelled" }
138 val constraintFailed = atomicReason.get() != STOP_REASON_NOT_STOPPED
139 if (future.isCancelled && constraintFailed) {
140 throw ConstraintUnsatisfiedException(atomicReason.get())
141 }
142 throw cancellation
143 } catch (throwable: Throwable) {
144 logd(TAG, throwable) {
145 "Delegated worker ${delegate.javaClass} threw exception in startWork."
146 }
147 throw throwable
148 } finally {
149 constraintTrackingJob.cancel()
150 }
151 }
152
153 private class ConstraintUnsatisfiedException(val stopReason: Int) : CancellationException()
154 }
155
awaitConstraintsNotMetnull156 private suspend fun WorkConstraintsTracker.awaitConstraintsNotMet(workSpec: WorkSpec) =
157 track(workSpec)
158 .onEach { logd(TAG) { "Constraints changed for $workSpec" } }
159 .filterIsInstance<ConstraintsNotMet>()
160 .first()
161 .reason
162
163 private val TAG = Logger.tagWithPrefix("ConstraintTrkngWrkr")
164
165 /** The `className` of the [androidx.work.Worker] to delegate to. */
166 internal const val ARGUMENT_CLASS_NAME =
167 "androidx.work.impl.workers.ConstraintTrackingWorker.ARGUMENT_CLASS_NAME"
168