1 /*
2  * Copyright (C) 2022 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 com.android.systemui.statusbar.pipeline.mobile.domain.interactor
18 
19 import android.content.Context
20 import com.android.settingslib.SignalIcon.MobileIconGroup
21 import com.android.settingslib.graph.SignalDrawable
22 import com.android.settingslib.mobile.MobileIconCarrierIdOverrides
23 import com.android.settingslib.mobile.MobileIconCarrierIdOverridesImpl
24 import com.android.systemui.dagger.qualifiers.Application
25 import com.android.systemui.log.table.TableLogBuffer
26 import com.android.systemui.log.table.logDiffsForTable
27 import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Connected
28 import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
29 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType
30 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
31 import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIconModel
32 import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIconModel.DefaultIcon
33 import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIconModel.OverriddenIcon
34 import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
35 import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
36 import kotlinx.coroutines.CoroutineScope
37 import kotlinx.coroutines.ExperimentalCoroutinesApi
38 import kotlinx.coroutines.flow.Flow
39 import kotlinx.coroutines.flow.SharingStarted
40 import kotlinx.coroutines.flow.StateFlow
41 import kotlinx.coroutines.flow.combine
42 import kotlinx.coroutines.flow.distinctUntilChanged
43 import kotlinx.coroutines.flow.map
44 import kotlinx.coroutines.flow.stateIn
45 
46 interface MobileIconInteractor {
47     /** The table log created for this connection */
48     val tableLogBuffer: TableLogBuffer
49 
50     /** The current mobile data activity */
51     val activity: Flow<DataActivityModel>
52 
53     /** See [MobileConnectionsRepository.mobileIsDefault]. */
54     val mobileIsDefault: Flow<Boolean>
55 
56     /**
57      * True when telephony tells us that the data state is CONNECTED. See
58      * [android.telephony.TelephonyCallback.DataConnectionStateListener] for more details. We
59      * consider this connection to be serving data, and thus want to show a network type icon, when
60      * data is connected. Other data connection states would typically cause us not to show the icon
61      */
62     val isDataConnected: StateFlow<Boolean>
63 
64     /** True if we consider this connection to be in service, i.e. can make calls */
65     val isInService: StateFlow<Boolean>
66 
67     /** Observable for the data enabled state of this connection */
68     val isDataEnabled: StateFlow<Boolean>
69 
70     /** True if the RAT icon should always be displayed and false otherwise. */
71     val alwaysShowDataRatIcon: StateFlow<Boolean>
72 
73     /** Canonical representation of the current mobile signal strength as a triangle. */
74     val signalLevelIcon: StateFlow<SignalIconModel>
75 
76     /** Observable for RAT type (network type) indicator */
77     val networkTypeIconGroup: StateFlow<NetworkTypeIconModel>
78 
79     /**
80      * Provider name for this network connection. The name can be one of 3 values:
81      * 1. The default network name, if one is configured
82      * 2. A derived name based off of the intent [ACTION_SERVICE_PROVIDERS_UPDATED]
83      * 3. Or, in the case where the repository sends us the default network name, we check for an
84      *    override in [connectionInfo.operatorAlphaShort], a value that is derived from
85      *    [ServiceState]
86      */
87     val networkName: StateFlow<NetworkNameModel>
88 
89     /**
90      * Provider name for this network connection. The name can be one of 3 values:
91      * 1. The default network name, if one is configured
92      * 2. A name provided by the [SubscriptionModel] of this network connection
93      * 3. Or, in the case where the repository sends us the default network name, we check for an
94      *    override in [connectionInfo.operatorAlphaShort], a value that is derived from
95      *    [ServiceState]
96      *
97      * TODO(b/296600321): De-duplicate this field with [networkName] after determining the data
98      *   provided is identical
99      */
100     val carrierName: StateFlow<String>
101 
102     /** True if there is only one active subscription. */
103     val isSingleCarrier: StateFlow<Boolean>
104 
105     /**
106      * True if this connection is considered roaming. The roaming bit can come from [ServiceState],
107      * or directly from the telephony manager's CDMA ERI number value. Note that we don't consider a
108      * connection to be roaming while carrier network change is active
109      */
110     val isRoaming: StateFlow<Boolean>
111 
112     /** See [MobileIconsInteractor.isForceHidden]. */
113     val isForceHidden: Flow<Boolean>
114 
115     /** See [MobileConnectionRepository.isAllowedDuringAirplaneMode]. */
116     val isAllowedDuringAirplaneMode: StateFlow<Boolean>
117 
118     /** True when in carrier network change mode */
119     val carrierNetworkChangeActive: StateFlow<Boolean>
120 }
121 
122 /** Interactor for a single mobile connection. This connection _should_ have one subscription ID */
123 @Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
124 @OptIn(ExperimentalCoroutinesApi::class)
125 class MobileIconInteractorImpl(
126     @Application scope: CoroutineScope,
127     defaultSubscriptionHasDataEnabled: StateFlow<Boolean>,
128     override val alwaysShowDataRatIcon: StateFlow<Boolean>,
129     alwaysUseCdmaLevel: StateFlow<Boolean>,
130     override val isSingleCarrier: StateFlow<Boolean>,
131     override val mobileIsDefault: StateFlow<Boolean>,
132     defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>>,
133     defaultMobileIconGroup: StateFlow<MobileIconGroup>,
134     isDefaultConnectionFailed: StateFlow<Boolean>,
135     override val isForceHidden: Flow<Boolean>,
136     connectionRepository: MobileConnectionRepository,
137     private val context: Context,
138     val carrierIdOverrides: MobileIconCarrierIdOverrides = MobileIconCarrierIdOverridesImpl()
139 ) : MobileIconInteractor {
140     override val tableLogBuffer: TableLogBuffer = connectionRepository.tableLogBuffer
141 
142     override val activity = connectionRepository.dataActivityDirection
143 
144     override val isDataEnabled: StateFlow<Boolean> = connectionRepository.dataEnabled
145 
146     override val carrierNetworkChangeActive: StateFlow<Boolean> =
147         connectionRepository.carrierNetworkChangeActive
148 
149     // True if there exists _any_ icon override for this carrierId. Note that overrides can include
150     // any or none of the icon groups defined in MobileMappings, so we still need to check on a
151     // per-network-type basis whether or not the given icon group is overridden
152     private val carrierIdIconOverrideExists =
153         connectionRepository.carrierId
154             .map { carrierIdOverrides.carrierIdEntryExists(it) }
155             .distinctUntilChanged()
156             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
157 
158     override val networkName =
159         combine(connectionRepository.operatorAlphaShort, connectionRepository.networkName) {
160                 operatorAlphaShort,
161                 networkName ->
162                 if (networkName is NetworkNameModel.Default && operatorAlphaShort != null) {
163                     NetworkNameModel.IntentDerived(operatorAlphaShort)
164                 } else {
165                     networkName
166                 }
167             }
168             .stateIn(
169                 scope,
170                 SharingStarted.WhileSubscribed(),
171                 connectionRepository.networkName.value
172             )
173 
174     override val carrierName =
175         combine(connectionRepository.operatorAlphaShort, connectionRepository.carrierName) {
176                 operatorAlphaShort,
177                 networkName ->
178                 if (networkName is NetworkNameModel.Default && operatorAlphaShort != null) {
179                     operatorAlphaShort
180                 } else {
181                     networkName.name
182                 }
183             }
184             .stateIn(
185                 scope,
186                 SharingStarted.WhileSubscribed(),
187                 connectionRepository.carrierName.value.name
188             )
189 
190     /** What the mobile icon would be before carrierId overrides */
191     private val defaultNetworkType: StateFlow<MobileIconGroup> =
192         combine(
193                 connectionRepository.resolvedNetworkType,
194                 defaultMobileIconMapping,
195                 defaultMobileIconGroup,
196             ) { resolvedNetworkType, mapping, defaultGroup ->
197                 when (resolvedNetworkType) {
198                     is ResolvedNetworkType.CarrierMergedNetworkType ->
199                         resolvedNetworkType.iconGroupOverride
200                     else -> {
201                         mapping[resolvedNetworkType.lookupKey] ?: defaultGroup
202                     }
203                 }
204             }
205             .stateIn(scope, SharingStarted.WhileSubscribed(), defaultMobileIconGroup.value)
206 
207     override val networkTypeIconGroup =
208         combine(
209                 defaultNetworkType,
210                 carrierIdIconOverrideExists,
211             ) { networkType, overrideExists ->
212                 // DefaultIcon comes out of the icongroup lookup, we check for overrides here
213                 if (overrideExists) {
214                     val iconOverride =
215                         carrierIdOverrides.getOverrideFor(
216                             connectionRepository.carrierId.value,
217                             networkType.name,
218                             context.resources,
219                         )
220                     if (iconOverride > 0) {
221                         OverriddenIcon(networkType, iconOverride)
222                     } else {
223                         DefaultIcon(networkType)
224                     }
225                 } else {
226                     DefaultIcon(networkType)
227                 }
228             }
229             .distinctUntilChanged()
230             .logDiffsForTable(
231                 tableLogBuffer = tableLogBuffer,
232                 columnPrefix = "",
233                 initialValue = DefaultIcon(defaultMobileIconGroup.value),
234             )
235             .stateIn(
236                 scope,
237                 SharingStarted.WhileSubscribed(),
238                 DefaultIcon(defaultMobileIconGroup.value),
239             )
240 
241     override val isRoaming: StateFlow<Boolean> =
242         combine(
243                 connectionRepository.carrierNetworkChangeActive,
244                 connectionRepository.isGsm,
245                 connectionRepository.isRoaming,
246                 connectionRepository.cdmaRoaming,
247             ) { carrierNetworkChangeActive, isGsm, isRoaming, cdmaRoaming ->
248                 if (carrierNetworkChangeActive) {
249                     false
250                 } else if (isGsm) {
251                     isRoaming
252                 } else {
253                     cdmaRoaming
254                 }
255             }
256             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
257 
258     private val level: StateFlow<Int> =
259         combine(
260                 connectionRepository.isGsm,
261                 connectionRepository.primaryLevel,
262                 connectionRepository.cdmaLevel,
263                 alwaysUseCdmaLevel,
264             ) { isGsm, primaryLevel, cdmaLevel, alwaysUseCdmaLevel ->
265                 when {
266                     // GSM connections should never use the CDMA level
267                     isGsm -> primaryLevel
268                     alwaysUseCdmaLevel -> cdmaLevel
269                     else -> primaryLevel
270                 }
271             }
272             .stateIn(scope, SharingStarted.WhileSubscribed(), 0)
273 
274     private val numberOfLevels: StateFlow<Int> = connectionRepository.numberOfLevels
275 
276     override val isDataConnected: StateFlow<Boolean> =
277         connectionRepository.dataConnectionState
278             .map { it == Connected }
279             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
280 
281     override val isInService = connectionRepository.isInService
282 
283     override val isAllowedDuringAirplaneMode = connectionRepository.isAllowedDuringAirplaneMode
284 
285     /** Whether or not to show the error state of [SignalDrawable] */
286     private val showExclamationMark: StateFlow<Boolean> =
287         combine(
288                 defaultSubscriptionHasDataEnabled,
289                 isDefaultConnectionFailed,
290                 isInService,
291             ) { isDefaultDataEnabled, isDefaultConnectionFailed, isInService ->
292                 !isDefaultDataEnabled || isDefaultConnectionFailed || !isInService
293             }
294             .stateIn(scope, SharingStarted.WhileSubscribed(), true)
295 
296     private val shownLevel: StateFlow<Int> =
297         combine(
298                 level,
299                 isInService,
300             ) { level, isInService ->
301                 if (isInService) level else 0
302             }
303             .stateIn(scope, SharingStarted.WhileSubscribed(), 0)
304 
305     override val signalLevelIcon: StateFlow<SignalIconModel> = run {
306         val initial =
307             SignalIconModel(
308                 level = shownLevel.value,
309                 numberOfLevels = numberOfLevels.value,
310                 showExclamationMark = showExclamationMark.value,
311                 carrierNetworkChange = carrierNetworkChangeActive.value,
312             )
313         combine(
314                 shownLevel,
315                 numberOfLevels,
316                 showExclamationMark,
317                 carrierNetworkChangeActive,
318             ) { shownLevel, numberOfLevels, showExclamationMark, carrierNetworkChange ->
319                 SignalIconModel(
320                     shownLevel,
321                     numberOfLevels,
322                     showExclamationMark,
323                     carrierNetworkChange,
324                 )
325             }
326             .distinctUntilChanged()
327             .logDiffsForTable(
328                 tableLogBuffer,
329                 columnPrefix = "icon",
330                 initialValue = initial,
331             )
332             .stateIn(scope, SharingStarted.WhileSubscribed(), initial)
333     }
334 }
335