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.constraints
17 
18 import android.content.Context
19 import android.net.ConnectivityManager
20 import android.net.Network
21 import android.net.NetworkCapabilities
22 import android.os.Build
23 import androidx.annotation.RequiresApi
24 import androidx.work.Constraints
25 import androidx.work.Logger
26 import androidx.work.StopReason
27 import androidx.work.WorkInfo.Companion.STOP_REASON_CONSTRAINT_CONNECTIVITY
28 import androidx.work.impl.constraints.ConstraintsState.ConstraintsMet
29 import androidx.work.impl.constraints.ConstraintsState.ConstraintsNotMet
30 import androidx.work.impl.constraints.controllers.BatteryChargingController
31 import androidx.work.impl.constraints.controllers.BatteryNotLowController
32 import androidx.work.impl.constraints.controllers.ConstraintController
33 import androidx.work.impl.constraints.controllers.NetworkConnectedController
34 import androidx.work.impl.constraints.controllers.NetworkMeteredController
35 import androidx.work.impl.constraints.controllers.NetworkNotRoamingController
36 import androidx.work.impl.constraints.controllers.NetworkUnmeteredController
37 import androidx.work.impl.constraints.controllers.StorageNotLowController
38 import androidx.work.impl.constraints.trackers.Trackers
39 import androidx.work.impl.model.WorkSpec
40 import kotlinx.coroutines.CoroutineDispatcher
41 import kotlinx.coroutines.CoroutineScope
42 import kotlinx.coroutines.Job
43 import kotlinx.coroutines.channels.awaitClose
44 import kotlinx.coroutines.delay
45 import kotlinx.coroutines.flow.Flow
46 import kotlinx.coroutines.flow.callbackFlow
47 import kotlinx.coroutines.flow.combine
48 import kotlinx.coroutines.flow.distinctUntilChanged
49 import kotlinx.coroutines.launch
50 
51 sealed class ConstraintsState {
52     object ConstraintsMet : ConstraintsState()
53 
54     data class ConstraintsNotMet(@StopReason val reason: Int) : ConstraintsState()
55 }
56 
WorkConstraintsTrackernull57 fun WorkConstraintsTracker.listen(
58     spec: WorkSpec,
59     dispatcher: CoroutineDispatcher,
60     listener: OnConstraintsStateChangedListener
61 ): Job {
62     val job = Job()
63     CoroutineScope(dispatcher + job).launch {
64         track(spec).collect { listener.onConstraintsStateChanged(spec, it) }
65     }
66     return job
67 }
68 
interfacenull69 fun interface OnConstraintsStateChangedListener {
70     fun onConstraintsStateChanged(workSpec: WorkSpec, state: ConstraintsState)
71 }
72 
73 class WorkConstraintsTracker(private val controllers: List<ConstraintController>) {
74     /** @param trackers Constraints trackers */
75     constructor(
76         trackers: Trackers,
77     ) : this(
78         listOfNotNull(
79             BatteryChargingController(trackers.batteryChargingTracker),
80             BatteryNotLowController(trackers.batteryNotLowTracker),
81             StorageNotLowController(trackers.storageNotLowTracker),
82             NetworkConnectedController(trackers.networkStateTracker),
83             NetworkUnmeteredController(trackers.networkStateTracker),
84             NetworkNotRoamingController(trackers.networkStateTracker),
85             NetworkMeteredController(trackers.networkStateTracker),
86             if (Build.VERSION.SDK_INT >= 28) NetworkRequestConstraintController(trackers.context)
87             else null,
88         )
89     )
90 
tracknull91     fun track(spec: WorkSpec): Flow<ConstraintsState> {
92         val flows = controllers.filter { it.hasConstraint(spec) }.map { it.track(spec.constraints) }
93         return combine(flows) { states ->
94                 states.firstOrNull { it != ConstraintsMet } ?: ConstraintsMet
95             }
96             .distinctUntilChanged()
97     }
98 
areAllConstraintsMetnull99     fun areAllConstraintsMet(workSpec: WorkSpec): Boolean {
100         val controllers = controllers.filter { it.isCurrentlyConstrained(workSpec) }
101 
102         if (controllers.isNotEmpty()) {
103             Logger.get()
104                 .debug(
105                     TAG,
106                     "Work ${workSpec.id} constrained by " +
107                         controllers.joinToString { it.javaClass.simpleName }
108                 )
109         }
110         return controllers.isEmpty()
111     }
112 }
113 
114 private val TAG = Logger.tagWithPrefix("WorkConstraintsTracker")
115 
116 @RequiresApi(28)
NetworkRequestConstraintControllernull117 fun NetworkRequestConstraintController(context: Context): NetworkRequestConstraintController {
118     val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
119     return NetworkRequestConstraintController(manager)
120 }
121 
122 private val DefaultNetworkRequestTimeoutMs = 1000L
123 
124 // So we don't have a tracker that is shared, because we rely on
125 // registerNetworkCallback with specific NetworkRequest to get a signal that
126 // required Network is available. Alternatively we could have used a tracker with
127 // registerDefaultNetwork and check if network satisfies requirement via
128 // `request.canBeSatisfiedBy()`. However this method available only since API level 30,
129 // that would significantly limit the feature availability. While we can simply rely on JobScheduler
130 // to kick off the workers on API level 28-30, we also need to track constraint for
131 // foreground workers, thus we need still controller on levels 28-30.
132 @RequiresApi(28)
133 class NetworkRequestConstraintController(
134     private val connManager: ConnectivityManager,
135     private val timeoutMs: Long = DefaultNetworkRequestTimeoutMs,
136 ) : ConstraintController {
<lambda>null137     override fun track(constraints: Constraints): Flow<ConstraintsState> = callbackFlow {
138         val networkRequest = constraints.requiredNetworkRequest
139         if (networkRequest == null) {
140             channel.close()
141             return@callbackFlow
142         }
143         // we don't want immediately send ConstraintsNotMet, because it will immediately
144         // stop the work in case foreground worker, even though network could be present
145         // However, we need to send it eventually, because otherwise we won't stop foreground
146         // worker at all, if there is no available network.
147         val job = launch {
148             delay(timeoutMs)
149             Logger.get()
150                 .debug(
151                     TAG,
152                     "NetworkRequestConstraintController didn't receive " +
153                         "neither  onCapabilitiesChanged/onLost callback, sending " +
154                         "`ConstraintsNotMet` after $timeoutMs ms"
155                 )
156             trySend(ConstraintsNotMet(STOP_REASON_CONSTRAINT_CONNECTIVITY))
157         }
158 
159         val networkCallback =
160             object : ConnectivityManager.NetworkCallback() {
161                 override fun onCapabilitiesChanged(
162                     network: Network,
163                     networkCapabilities: NetworkCapabilities
164                 ) {
165                     job.cancel()
166                     Logger.get()
167                         .debug(
168                             TAG,
169                             "NetworkRequestConstraintController onCapabilitiesChanged callback"
170                         )
171                     trySend(ConstraintsMet)
172                 }
173 
174                 override fun onLost(network: Network) {
175                     job.cancel()
176                     Logger.get().debug(TAG, "NetworkRequestConstraintController onLost callback")
177                     trySend(ConstraintsNotMet(STOP_REASON_CONSTRAINT_CONNECTIVITY))
178                 }
179             }
180         Logger.get().debug(TAG, "NetworkRequestConstraintController register callback")
181         connManager.registerNetworkCallback(networkRequest, networkCallback)
182         awaitClose {
183             Logger.get().debug(TAG, "NetworkRequestConstraintController unregister callback")
184             connManager.unregisterNetworkCallback(networkCallback)
185         }
186     }
187 
hasConstraintnull188     override fun hasConstraint(workSpec: WorkSpec): Boolean =
189         workSpec.constraints.requiredNetworkRequest != null
190 
191     override fun isCurrentlyConstrained(workSpec: WorkSpec): Boolean {
192         // It happens because ConstraintTrackingWorker can still run on API level 28
193         // after OS upgrade, because we're wrapping workers as ConstraintTrackingWorker at
194         // the enqueue time instead of execution time.
195         // However, ConstraintTrackingWorker won't have requiredNetworkRequest set
196         // because they were enqueued on APIs 23..25, in this case we don't throw.
197         if (!hasConstraint(workSpec)) return false
198         throw IllegalStateException(
199             "isCurrentlyConstrained() must never be called on" +
200                 "NetworkRequestConstraintController. isCurrentlyConstrained() is called only " +
201                 "on older platforms where NetworkRequest isn't supported"
202         )
203     }
204 }
205