1 /*
<lambda>null2  * 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.ui.viewmodel
18 
19 import com.android.settingslib.AccessibilityContentDescriptions
20 import com.android.systemui.Flags.statusBarStaticInoutIndicators
21 import com.android.systemui.common.shared.model.ContentDescription
22 import com.android.systemui.common.shared.model.Icon
23 import com.android.systemui.flags.FeatureFlagsClassic
24 import com.android.systemui.flags.Flags.NEW_NETWORK_SLICE_UI
25 import com.android.systemui.log.table.logDiffsForTable
26 import com.android.systemui.res.R
27 import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
28 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor
29 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
30 import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
31 import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
32 import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
33 import kotlinx.coroutines.CoroutineScope
34 import kotlinx.coroutines.ExperimentalCoroutinesApi
35 import kotlinx.coroutines.flow.Flow
36 import kotlinx.coroutines.flow.MutableStateFlow
37 import kotlinx.coroutines.flow.SharingStarted
38 import kotlinx.coroutines.flow.StateFlow
39 import kotlinx.coroutines.flow.combine
40 import kotlinx.coroutines.flow.distinctUntilChanged
41 import kotlinx.coroutines.flow.flatMapLatest
42 import kotlinx.coroutines.flow.flowOf
43 import kotlinx.coroutines.flow.map
44 import kotlinx.coroutines.flow.mapLatest
45 import kotlinx.coroutines.flow.stateIn
46 
47 /** Common interface for all of the location-based mobile icon view models. */
48 interface MobileIconViewModelCommon {
49     val subscriptionId: Int
50     /** True if this view should be visible at all. */
51     val isVisible: StateFlow<Boolean>
52     val icon: Flow<SignalIconModel>
53     val contentDescription: Flow<ContentDescription?>
54     val roaming: Flow<Boolean>
55     /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */
56     val networkTypeIcon: Flow<Icon.Resource?>
57     /** The slice attribution. Drawn as a background layer */
58     val networkTypeBackground: StateFlow<Icon.Resource?>
59     val activityInVisible: Flow<Boolean>
60     val activityOutVisible: Flow<Boolean>
61     val activityContainerVisible: Flow<Boolean>
62 }
63 
64 /**
65  * View model for the state of a single mobile icon. Each [MobileIconViewModel] will keep watch over
66  * a single line of service via [MobileIconInteractor] and update the UI based on that
67  * subscription's information.
68  *
69  * There will be exactly one [MobileIconViewModel] per filtered subscription offered from
70  * [MobileIconsInteractor.filteredSubscriptions].
71  *
72  * For the sake of keeping log spam in check, every flow funding the [MobileIconViewModelCommon]
73  * interface is implemented as a [StateFlow]. This ensures that each location-based mobile icon view
74  * model gets the exact same information, as well as allows us to log that unified state only once
75  * per icon.
76  */
77 @OptIn(ExperimentalCoroutinesApi::class)
78 class MobileIconViewModel(
79     override val subscriptionId: Int,
80     iconInteractor: MobileIconInteractor,
81     airplaneModeInteractor: AirplaneModeInteractor,
82     constants: ConnectivityConstants,
83     flags: FeatureFlagsClassic,
84     scope: CoroutineScope,
85 ) : MobileIconViewModelCommon {
<lambda>null86     private val cellProvider by lazy {
87         CellularIconViewModel(
88             subscriptionId,
89             iconInteractor,
90             airplaneModeInteractor,
91             constants,
92             flags,
93             scope,
94         )
95     }
96 
<lambda>null97     private val satelliteProvider by lazy {
98         CarrierBasedSatelliteViewModelImpl(
99             subscriptionId,
100             iconInteractor,
101         )
102     }
103 
104     /**
105      * Similar to repository switching, this allows us to split up the logic of satellite/cellular
106      * states, since they are different by nature
107      */
108     private val vmProvider: Flow<MobileIconViewModelCommon> =
109         iconInteractor.isNonTerrestrial
nonTerrestrialnull110             .mapLatest { nonTerrestrial ->
111                 if (nonTerrestrial) {
112                     satelliteProvider
113                 } else {
114                     cellProvider
115                 }
116             }
117             .stateIn(scope, SharingStarted.WhileSubscribed(), cellProvider)
118 
119     override val isVisible: StateFlow<Boolean> =
120         vmProvider
<lambda>null121             .flatMapLatest { it.isVisible }
122             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
123 
<lambda>null124     override val icon: Flow<SignalIconModel> = vmProvider.flatMapLatest { it.icon }
125 
126     override val contentDescription: Flow<ContentDescription?> =
<lambda>null127         vmProvider.flatMapLatest { it.contentDescription }
128 
<lambda>null129     override val roaming: Flow<Boolean> = vmProvider.flatMapLatest { it.roaming }
130 
131     override val networkTypeIcon: Flow<Icon.Resource?> =
<lambda>null132         vmProvider.flatMapLatest { it.networkTypeIcon }
133 
134     override val networkTypeBackground: StateFlow<Icon.Resource?> =
135         vmProvider
<lambda>null136             .flatMapLatest { it.networkTypeBackground }
137             .stateIn(scope, SharingStarted.WhileSubscribed(), null)
138 
139     override val activityInVisible: Flow<Boolean> =
<lambda>null140         vmProvider.flatMapLatest { it.activityInVisible }
141 
142     override val activityOutVisible: Flow<Boolean> =
<lambda>null143         vmProvider.flatMapLatest { it.activityOutVisible }
144 
145     override val activityContainerVisible: Flow<Boolean> =
<lambda>null146         vmProvider.flatMapLatest { it.activityContainerVisible }
147 }
148 
149 /** Representation of this network when it is non-terrestrial (e.g., satellite) */
150 private class CarrierBasedSatelliteViewModelImpl(
151     override val subscriptionId: Int,
152     interactor: MobileIconInteractor,
153 ) : MobileIconViewModelCommon {
154     override val isVisible: StateFlow<Boolean> = MutableStateFlow(true)
155     override val icon: Flow<SignalIconModel> = interactor.signalLevelIcon
156 
157     override val contentDescription: Flow<ContentDescription> =
158         MutableStateFlow(ContentDescription.Loaded(""))
159 
160     /** These fields are not used for satellite icons currently */
161     override val roaming: Flow<Boolean> = flowOf(false)
162     override val networkTypeIcon: Flow<Icon.Resource?> = flowOf(null)
163     override val networkTypeBackground: StateFlow<Icon.Resource?> = MutableStateFlow(null)
164     override val activityInVisible: Flow<Boolean> = flowOf(false)
165     override val activityOutVisible: Flow<Boolean> = flowOf(false)
166     override val activityContainerVisible: Flow<Boolean> = flowOf(false)
167 }
168 
169 /** Terrestrial (cellular) icon. */
170 @Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
171 @OptIn(ExperimentalCoroutinesApi::class)
172 private class CellularIconViewModel(
173     override val subscriptionId: Int,
174     iconInteractor: MobileIconInteractor,
175     airplaneModeInteractor: AirplaneModeInteractor,
176     constants: ConnectivityConstants,
177     flags: FeatureFlagsClassic,
178     scope: CoroutineScope,
179 ) : MobileIconViewModelCommon {
180     override val isVisible: StateFlow<Boolean> =
181         if (!constants.hasDataCapabilities) {
182                 flowOf(false)
183             } else {
184                 combine(
185                     airplaneModeInteractor.isAirplaneMode,
186                     iconInteractor.isAllowedDuringAirplaneMode,
187                     iconInteractor.isForceHidden,
isAllowedDuringAirplaneModenull188                 ) { isAirplaneMode, isAllowedDuringAirplaneMode, isForceHidden ->
189                     if (isForceHidden) {
190                         false
191                     } else if (isAirplaneMode) {
192                         isAllowedDuringAirplaneMode
193                     } else {
194                         true
195                     }
196                 }
197             }
198             .distinctUntilChanged()
199             .logDiffsForTable(
200                 iconInteractor.tableLogBuffer,
201                 columnPrefix = "",
202                 columnName = "visible",
203                 initialValue = false,
204             )
205             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
206 
207     override val icon: Flow<SignalIconModel> = iconInteractor.signalLevelIcon
208 
209     override val contentDescription: Flow<ContentDescription?> =
210         iconInteractor.signalLevelIcon
<lambda>null211             .map {
212                 // We expect the signal icon to be cellular here since this is the cellular vm
213                 if (it !is SignalIconModel.Cellular) {
214                     null
215                 } else {
216                     val resId =
217                         AccessibilityContentDescriptions.getDescriptionForLevel(
218                             it.level,
219                             it.numberOfLevels
220                         )
221                     if (resId != 0) {
222                         ContentDescription.Resource(resId)
223                     } else {
224                         null
225                     }
226                 }
227             }
228             .stateIn(scope, SharingStarted.WhileSubscribed(), null)
229 
230     private val showNetworkTypeIcon: Flow<Boolean> =
231         combine(
232                 iconInteractor.isDataConnected,
233                 iconInteractor.isDataEnabled,
234                 iconInteractor.alwaysShowDataRatIcon,
235                 iconInteractor.mobileIsDefault,
236                 iconInteractor.carrierNetworkChangeActive,
dataConnectednull237             ) { dataConnected, dataEnabled, alwaysShow, mobileIsDefault, carrierNetworkChange ->
238                 alwaysShow ||
239                     (!carrierNetworkChange && (dataEnabled && dataConnected && mobileIsDefault))
240             }
241             .distinctUntilChanged()
242             .logDiffsForTable(
243                 iconInteractor.tableLogBuffer,
244                 columnPrefix = "",
245                 columnName = "showNetworkTypeIcon",
246                 initialValue = false,
247             )
248             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
249 
250     override val networkTypeIcon: Flow<Icon.Resource?> =
251         combine(
252                 iconInteractor.networkTypeIconGroup,
253                 showNetworkTypeIcon,
networkTypeIconGroupnull254             ) { networkTypeIconGroup, shouldShow ->
255                 val desc =
256                     if (networkTypeIconGroup.contentDescription != 0)
257                         ContentDescription.Resource(networkTypeIconGroup.contentDescription)
258                     else null
259                 val icon =
260                     if (networkTypeIconGroup.iconId != 0)
261                         Icon.Resource(networkTypeIconGroup.iconId, desc)
262                     else null
263                 return@combine when {
264                     !shouldShow -> null
265                     else -> icon
266                 }
267             }
268             .distinctUntilChanged()
269             .stateIn(scope, SharingStarted.WhileSubscribed(), null)
270 
271     override val networkTypeBackground =
272         if (!flags.isEnabled(NEW_NETWORK_SLICE_UI)) {
273                 flowOf(null)
274             } else {
<lambda>null275                 iconInteractor.showSliceAttribution.map {
276                     if (it) {
277                         Icon.Resource(R.drawable.mobile_network_type_background, null)
278                     } else {
279                         null
280                     }
281                 }
282             }
283             .stateIn(scope, SharingStarted.WhileSubscribed(), null)
284 
285     override val roaming: StateFlow<Boolean> =
286         iconInteractor.isRoaming
287             .logDiffsForTable(
288                 iconInteractor.tableLogBuffer,
289                 columnPrefix = "",
290                 columnName = "roaming",
291                 initialValue = false,
292             )
293             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
294 
295     private val activity: Flow<DataActivityModel?> =
296         if (!constants.shouldShowActivityConfig) {
297             flowOf(null)
298         } else {
299             iconInteractor.activity
300         }
301 
302     override val activityInVisible: Flow<Boolean> =
303         activity
<lambda>null304             .map { it?.hasActivityIn ?: false }
305             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
306 
307     override val activityOutVisible: Flow<Boolean> =
308         activity
<lambda>null309             .map { it?.hasActivityOut ?: false }
310             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
311 
312     override val activityContainerVisible: Flow<Boolean> =
313         if (statusBarStaticInoutIndicators()) {
314                 flowOf(constants.shouldShowActivityConfig)
315             } else {
<lambda>null316                 activity.map { it != null && (it.hasActivityIn || it.hasActivityOut) }
317             }
318             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
319 }
320