1 /*
2  * Copyright 2021 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 
17 package androidx.work.impl.constraints.controllers
18 
19 import android.os.Build
20 import androidx.work.Constraints
21 import androidx.work.Logger
22 import androidx.work.NetworkType
23 import androidx.work.NetworkType.TEMPORARILY_UNMETERED
24 import androidx.work.NetworkType.UNMETERED
25 import androidx.work.StopReason
26 import androidx.work.WorkInfo
27 import androidx.work.impl.constraints.ConstraintListener
28 import androidx.work.impl.constraints.ConstraintsState
29 import androidx.work.impl.constraints.ConstraintsState.ConstraintsMet
30 import androidx.work.impl.constraints.ConstraintsState.ConstraintsNotMet
31 import androidx.work.impl.constraints.NetworkState
32 import androidx.work.impl.constraints.trackers.BatteryNotLowTracker
33 import androidx.work.impl.constraints.trackers.ConstraintTracker
34 import androidx.work.impl.model.WorkSpec
35 import kotlinx.coroutines.channels.awaitClose
36 import kotlinx.coroutines.flow.Flow
37 import kotlinx.coroutines.flow.callbackFlow
38 
39 interface ConstraintController {
tracknull40     fun track(constraints: Constraints): Flow<ConstraintsState>
41 
42     fun hasConstraint(workSpec: WorkSpec): Boolean
43 
44     fun isCurrentlyConstrained(workSpec: WorkSpec): Boolean
45 }
46 
47 abstract class BaseConstraintController<T>(private val tracker: ConstraintTracker<T>) :
48     ConstraintController {
49     @StopReason protected abstract val reason: Int
50 
51     protected open fun isConstrained(value: T): Boolean = false
52 
53     override fun track(constraints: Constraints): Flow<ConstraintsState> = callbackFlow {
54         val listener =
55             object : ConstraintListener<T> {
56                 override fun onConstraintChanged(newValue: T) {
57                     val value =
58                         if (isConstrained(newValue)) ConstraintsNotMet(reason) else ConstraintsMet
59                     channel.trySend(value)
60                 }
61             }
62         tracker.addListener(listener)
63         awaitClose { tracker.removeListener(listener) }
64     }
65 
66     override fun isCurrentlyConstrained(workSpec: WorkSpec): Boolean {
67         return hasConstraint(workSpec) && isConstrained(tracker.readSystemState())
68     }
69 }
70 
71 /** A [ConstraintController] for battery charging events. */
72 class BatteryChargingController(tracker: ConstraintTracker<Boolean>) :
73     BaseConstraintController<Boolean>(tracker) {
74     override val reason = WorkInfo.STOP_REASON_CONSTRAINT_CHARGING
75 
hasConstraintnull76     override fun hasConstraint(workSpec: WorkSpec) = workSpec.constraints.requiresCharging()
77 
78     override fun isConstrained(value: Boolean) = !value
79 }
80 
81 /** A [ConstraintController] for battery not low events. */
82 class BatteryNotLowController(tracker: BatteryNotLowTracker) :
83     BaseConstraintController<Boolean>(tracker) {
84     override val reason = WorkInfo.STOP_REASON_CONSTRAINT_BATTERY_NOT_LOW
85 
86     override fun hasConstraint(workSpec: WorkSpec) = workSpec.constraints.requiresBatteryNotLow()
87 
88     override fun isConstrained(value: Boolean) = !value
89 }
90 
91 /** A [ConstraintController] for monitoring that the network connection is unmetered. */
92 class NetworkUnmeteredController(tracker: ConstraintTracker<NetworkState>) :
93     BaseConstraintController<NetworkState>(tracker) {
94     override val reason = WorkInfo.STOP_REASON_CONSTRAINT_CONNECTIVITY
95 
hasConstraintnull96     override fun hasConstraint(workSpec: WorkSpec): Boolean {
97         val requiredNetworkType = workSpec.constraints.requiredNetworkType
98         return requiredNetworkType == UNMETERED ||
99             (Build.VERSION.SDK_INT >= 30 && requiredNetworkType == TEMPORARILY_UNMETERED)
100     }
101 
isConstrainednull102     override fun isConstrained(value: NetworkState) = !value.isConnected || value.isMetered
103 }
104 
105 /** A [ConstraintController] for storage not low events. */
106 class StorageNotLowController(tracker: ConstraintTracker<Boolean>) :
107     BaseConstraintController<Boolean>(tracker) {
108     override val reason = WorkInfo.STOP_REASON_CONSTRAINT_STORAGE_NOT_LOW
109 
110     override fun hasConstraint(workSpec: WorkSpec) = workSpec.constraints.requiresStorageNotLow()
111 
112     override fun isConstrained(value: Boolean) = !value
113 }
114 
115 /** A [ConstraintController] for monitoring that the network connection is not roaming. */
116 class NetworkNotRoamingController(tracker: ConstraintTracker<NetworkState>) :
117     BaseConstraintController<NetworkState>(tracker) {
118     override val reason = WorkInfo.STOP_REASON_CONSTRAINT_CONNECTIVITY
119 
hasConstraintnull120     override fun hasConstraint(workSpec: WorkSpec): Boolean {
121         return workSpec.constraints.requiredNetworkType == NetworkType.NOT_ROAMING
122     }
123 
124     /**
125      * Check for not-roaming constraint on API 24+, when JobInfo#NETWORK_TYPE_NOT_ROAMING was added,
126      * to be consistent with JobScheduler functionality.
127      */
isConstrainednull128     override fun isConstrained(value: NetworkState): Boolean {
129         return if (Build.VERSION.SDK_INT < 24) {
130             Logger.get()
131                 .debug(
132                     TAG,
133                     "Not-roaming network constraint is not supported before API 24, " +
134                         "only checking for connected state."
135                 )
136             !value.isConnected
137         } else !value.isConnected || !value.isNotRoaming
138     }
139 
140     companion object {
141         private val TAG = Logger.tagWithPrefix("NetworkNotRoamingCtrlr")
142     }
143 }
144 
145 /**
146  * A [ConstraintController] for monitoring that any usable network connection is available.
147  *
148  * For API 26 and above, usable means that the [NetworkState] is validated, i.e. it has a working
149  * internet connection.
150  *
151  * For API 25 and below, usable simply means that [NetworkState] is connected.
152  */
153 class NetworkConnectedController(tracker: ConstraintTracker<NetworkState>) :
154     BaseConstraintController<NetworkState>(tracker) {
155     override val reason = WorkInfo.STOP_REASON_CONSTRAINT_CONNECTIVITY
156 
hasConstraintnull157     override fun hasConstraint(workSpec: WorkSpec) =
158         workSpec.constraints.requiredNetworkType == NetworkType.CONNECTED
159 
160     override fun isConstrained(value: NetworkState) =
161         if (Build.VERSION.SDK_INT >= 26) {
162             !value.isConnected || !value.isValidated
163         } else {
164             !value.isConnected
165         }
166 }
167 
168 /** A [ConstraintController] for monitoring that the network connection is metered. */
169 class NetworkMeteredController(tracker: ConstraintTracker<NetworkState>) :
170     BaseConstraintController<NetworkState>(tracker) {
171     override val reason = WorkInfo.STOP_REASON_CONSTRAINT_CONNECTIVITY
172 
hasConstraintnull173     override fun hasConstraint(workSpec: WorkSpec) =
174         workSpec.constraints.requiredNetworkType == NetworkType.METERED
175 
176     /**
177      * Check for metered constraint on API 26+, when JobInfo#NETWORK_METERED was added, to be
178      * consistent with JobScheduler functionality.
179      */
180     override fun isConstrained(value: NetworkState): Boolean {
181         return if (Build.VERSION.SDK_INT < 26) {
182             Logger.get()
183                 .debug(
184                     TAG,
185                     "Metered network constraint is not supported before API 26, " +
186                         "only checking for connected state."
187                 )
188             !value.isConnected
189         } else !value.isConnected || !value.isMetered
190     }
191 
192     companion object {
193         private val TAG = Logger.tagWithPrefix("NetworkMeteredCtrlr")
194     }
195 }
196