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