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