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