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.domain.interactor 18 19 import android.content.Context 20 import android.telephony.CarrierConfigManager 21 import android.telephony.SubscriptionManager 22 import android.telephony.SubscriptionManager.PROFILE_CLASS_PROVISIONING 23 import com.android.settingslib.SignalIcon.MobileIconGroup 24 import com.android.settingslib.mobile.TelephonyIcons 25 import com.android.systemui.dagger.SysUISingleton 26 import com.android.systemui.dagger.qualifiers.Background 27 import com.android.systemui.flags.FeatureFlagsClassic 28 import com.android.systemui.flags.Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS 29 import com.android.systemui.log.table.TableLogBuffer 30 import com.android.systemui.log.table.logDiffsForTable 31 import com.android.systemui.statusbar.core.NewStatusBarIcons 32 import com.android.systemui.statusbar.core.StatusBarRootModernization 33 import com.android.systemui.statusbar.pipeline.dagger.MobileSummaryLog 34 import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel 35 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository 36 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository 37 import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel 38 import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot 39 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository 40 import com.android.systemui.statusbar.policy.data.repository.UserSetupRepository 41 import com.android.systemui.util.CarrierConfigTracker 42 import java.lang.ref.WeakReference 43 import javax.inject.Inject 44 import kotlinx.coroutines.CoroutineScope 45 import kotlinx.coroutines.ExperimentalCoroutinesApi 46 import kotlinx.coroutines.delay 47 import kotlinx.coroutines.flow.Flow 48 import kotlinx.coroutines.flow.SharingStarted 49 import kotlinx.coroutines.flow.StateFlow 50 import kotlinx.coroutines.flow.combine 51 import kotlinx.coroutines.flow.distinctUntilChanged 52 import kotlinx.coroutines.flow.filter 53 import kotlinx.coroutines.flow.flatMapLatest 54 import kotlinx.coroutines.flow.flowOf 55 import kotlinx.coroutines.flow.map 56 import kotlinx.coroutines.flow.mapLatest 57 import kotlinx.coroutines.flow.stateIn 58 import kotlinx.coroutines.flow.transformLatest 59 60 /** 61 * Business layer logic for the set of mobile subscription icons. 62 * 63 * This interactor represents known set of mobile subscriptions (represented by [SubscriptionInfo]). 64 * The list of subscriptions is filtered based on the opportunistic flags on the infos. 65 * 66 * It provides the default mapping between the telephony display info and the icon group that 67 * represents each RAT (LTE, 3G, etc.), as well as can produce an interactor for each individual 68 * icon 69 */ 70 interface MobileIconsInteractor { 71 /** See [MobileConnectionsRepository.mobileIsDefault]. */ 72 val mobileIsDefault: StateFlow<Boolean> 73 74 /** List of subscriptions, potentially filtered for CBRS */ 75 val filteredSubscriptions: Flow<List<SubscriptionModel>> 76 77 /** Subscription ID of the current default data subscription */ 78 val defaultDataSubId: Flow<Int?> 79 80 /** 81 * The current list of [MobileIconInteractor]s associated with the current list of 82 * [filteredSubscriptions] 83 */ 84 val icons: StateFlow<List<MobileIconInteractor>> 85 86 /** Whether the mobile icons can be stacked vertically. */ 87 val isStackable: Flow<Boolean> 88 89 /** 90 * Observable for the subscriptionId of the current mobile data connection. Null if we don't 91 * have a valid subscription id 92 */ 93 val activeMobileDataSubscriptionId: StateFlow<Int?> 94 95 /** True if the active mobile data subscription has data enabled */ 96 val activeDataConnectionHasDataEnabled: StateFlow<Boolean> 97 98 /** 99 * Flow providing a reference to the Interactor for the active data subId. This represents the 100 * [MobileIconInteractor] responsible for the active data connection, if any. 101 */ 102 val activeDataIconInteractor: StateFlow<MobileIconInteractor?> 103 104 /** True if the RAT icon should always be displayed and false otherwise. */ 105 val alwaysShowDataRatIcon: StateFlow<Boolean> 106 107 /** True if the CDMA level should be preferred over the primary level. */ 108 val alwaysUseCdmaLevel: StateFlow<Boolean> 109 110 /** True if there is only one active subscription. */ 111 val isSingleCarrier: StateFlow<Boolean> 112 113 /** The icon mapping from network type to [MobileIconGroup] for the default subscription */ 114 val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>> 115 116 /** Fallback [MobileIconGroup] in the case where there is no icon in the mapping */ 117 val defaultMobileIconGroup: StateFlow<MobileIconGroup> 118 119 /** True only if the default network is mobile, and validation also failed */ 120 val isDefaultConnectionFailed: StateFlow<Boolean> 121 122 /** True once the user has been set up */ 123 val isUserSetUp: StateFlow<Boolean> 124 125 /** True if we're configured to force-hide the mobile icons and false otherwise. */ 126 val isForceHidden: Flow<Boolean> 127 128 /** 129 * True if the device-level service state (with -1 subscription id) reports emergency calls 130 * only. This value is only useful when there are no other subscriptions OR all existing 131 * subscriptions report that they are not in service. 132 */ 133 val isDeviceInEmergencyCallsOnlyMode: Flow<Boolean> 134 135 /** 136 * Vends out a [MobileIconInteractor] tracking the [MobileConnectionRepository] for the given 137 * subId. 138 */ 139 fun getMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor 140 } 141 142 @OptIn(ExperimentalCoroutinesApi::class) 143 @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") 144 @SysUISingleton 145 class MobileIconsInteractorImpl 146 @Inject 147 constructor( 148 private val mobileConnectionsRepo: MobileConnectionsRepository, 149 private val carrierConfigTracker: CarrierConfigTracker, 150 @MobileSummaryLog private val tableLogger: TableLogBuffer, 151 connectivityRepository: ConnectivityRepository, 152 userSetupRepo: UserSetupRepository, 153 @Background private val scope: CoroutineScope, 154 private val context: Context, 155 private val featureFlagsClassic: FeatureFlagsClassic, 156 ) : MobileIconsInteractor { 157 158 // Weak reference lookup for created interactors 159 private val reuseCache = mutableMapOf<Int, WeakReference<MobileIconInteractor>>() 160 161 override val mobileIsDefault = 162 combine( 163 mobileConnectionsRepo.mobileIsDefault, 164 mobileConnectionsRepo.hasCarrierMergedConnection, hasCarrierMergedConnectionnull165 ) { mobileIsDefault, hasCarrierMergedConnection -> 166 // Because carrier merged networks are displayed as mobile networks, they're part of 167 // the `isDefault` calculation. See b/272586234. 168 mobileIsDefault || hasCarrierMergedConnection 169 } 170 .logDiffsForTable( 171 tableLogger, 172 LOGGING_PREFIX, 173 columnName = "mobileIsDefault", 174 initialValue = false, 175 ) 176 .stateIn(scope, SharingStarted.WhileSubscribed(), false) 177 178 override val activeMobileDataSubscriptionId: StateFlow<Int?> = 179 mobileConnectionsRepo.activeMobileDataSubscriptionId 180 181 override val activeDataConnectionHasDataEnabled: StateFlow<Boolean> = 182 mobileConnectionsRepo.activeMobileDataRepository <lambda>null183 .flatMapLatest { it?.dataEnabled ?: flowOf(false) } 184 .stateIn(scope, SharingStarted.WhileSubscribed(), false) 185 186 override val activeDataIconInteractor: StateFlow<MobileIconInteractor?> = 187 mobileConnectionsRepo.activeMobileDataSubscriptionId <lambda>null188 .mapLatest { 189 if (it != null) { 190 getMobileConnectionInteractorForSubId(it) 191 } else { 192 null 193 } 194 } 195 .stateIn(scope, SharingStarted.WhileSubscribed(), null) 196 197 private val unfilteredSubscriptions: Flow<List<SubscriptionModel>> = 198 mobileConnectionsRepo.subscriptions 199 200 /** Any filtering that we can do based purely on the info of each subscription individually. */ 201 private val subscriptionsBasedFilteredSubs = 202 unfilteredSubscriptions <lambda>null203 .map { it.filterBasedOnProvisioning().filterBasedOnNtn() } 204 .distinctUntilChanged() 205 filterBasedOnProvisioningnull206 private fun List<SubscriptionModel>.filterBasedOnProvisioning(): List<SubscriptionModel> = 207 if (!featureFlagsClassic.isEnabled(FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS)) { 208 this 209 } else { <lambda>null210 this.filter { it.profileClass != PROFILE_CLASS_PROVISIONING } 211 } 212 213 /** 214 * Subscriptions that exclusively support non-terrestrial networks should **never** directly 215 * show any iconography in the status bar. These subscriptions only exist to provide a backing 216 * for the device-based satellite connections, and the iconography for those connections are 217 * already being handled in 218 * [com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository]. We 219 * need to filter out those subscriptions here so we guarantee the subscription never turns into 220 * an icon. See b/336881301. 221 */ Listnull222 private fun List<SubscriptionModel>.filterBasedOnNtn(): List<SubscriptionModel> { 223 return this.filter { !it.isExclusivelyNonTerrestrial } 224 } 225 226 /** 227 * Generally, SystemUI wants to show iconography for each subscription that is listed by 228 * [SubscriptionManager]. However, in the case of opportunistic subscriptions, we want to only 229 * show a single representation of the pair of subscriptions. The docs define opportunistic as: 230 * 231 * "A subscription is opportunistic (if) the network it connects to has limited coverage" 232 * https://developer.android.com/reference/android/telephony/SubscriptionManager#setOpportunistic(boolean,%20int) 233 * 234 * In the case of opportunistic networks (typically CBRS), we will filter out one of the 235 * subscriptions based on 236 * [CarrierConfigManager.KEY_ALWAYS_SHOW_PRIMARY_SIGNAL_BAR_IN_OPPORTUNISTIC_NETWORK_BOOLEAN], 237 * and by checking which subscription is opportunistic, or which one is active. 238 */ 239 override val filteredSubscriptions: Flow<List<SubscriptionModel>> = 240 combine( 241 subscriptionsBasedFilteredSubs, 242 mobileConnectionsRepo.activeMobileDataSubscriptionId, 243 connectivityRepository.vcnSubId, activeIdnull244 ) { preFilteredSubs, activeId, vcnSubId -> 245 filterSubsBasedOnOpportunistic(preFilteredSubs, activeId, vcnSubId) 246 } 247 .distinctUntilChanged() 248 .logDiffsForTable( 249 tableLogger, 250 LOGGING_PREFIX, 251 columnName = "filteredSubscriptions", 252 initialValue = listOf(), 253 ) 254 .stateIn(scope, SharingStarted.WhileSubscribed(), listOf()) 255 filterSubsBasedOnOpportunisticnull256 private fun filterSubsBasedOnOpportunistic( 257 subList: List<SubscriptionModel>, 258 activeId: Int?, 259 vcnSubId: Int?, 260 ): List<SubscriptionModel> { 261 // Based on the old logic, 262 if (subList.size != 2) { 263 return subList 264 } 265 266 val info1 = subList[0] 267 val info2 = subList[1] 268 269 // Filtering only applies to subscriptions in the same group 270 if (info1.groupUuid == null || info1.groupUuid != info2.groupUuid) { 271 return subList 272 } 273 274 // If both subscriptions are primary, show both 275 if (!info1.isOpportunistic && !info2.isOpportunistic) { 276 return subList 277 } 278 279 // NOTE: at this point, we are now returning a single SubscriptionInfo 280 281 // If carrier required, always show the icon of the primary subscription. 282 // Otherwise, show whichever subscription is currently active for internet. 283 if (carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) { 284 // return the non-opportunistic info 285 return if (info1.isOpportunistic) listOf(info2) else listOf(info1) 286 } else { 287 // It's possible for the subId of the VCN to disagree with the active subId in 288 // cases where the system has tried to switch but found no connection. In these 289 // scenarios, VCN will always have the subId that we want to use, so use that 290 // value instead of the activeId reported by telephony 291 val subIdToKeep = vcnSubId ?: activeId 292 293 return if (info1.subscriptionId == subIdToKeep) { 294 listOf(info1) 295 } else { 296 listOf(info2) 297 } 298 } 299 } 300 301 override val defaultDataSubId = mobileConnectionsRepo.defaultDataSubId 302 303 override val icons = 304 filteredSubscriptions subsnull305 .mapLatest { subs -> 306 subs.map { getMobileConnectionInteractorForSubId(it.subscriptionId) } 307 } 308 .stateIn(scope, SharingStarted.WhileSubscribed(), emptyList()) 309 310 override val isStackable = 311 if (NewStatusBarIcons.isEnabled && StatusBarRootModernization.isEnabled) { iconsnull312 icons.flatMapLatest { icons -> 313 combine(icons.map { it.signalLevelIcon }) { signalLevelIcons -> 314 // These are only stackable if: 315 // - They are cellular 316 // - There's exactly two 317 // - They have the same number of levels 318 signalLevelIcons.filterIsInstance<SignalIconModel.Cellular>().let { 319 it.size == 2 && it[0].numberOfLevels == it[1].numberOfLevels 320 } 321 } 322 } 323 } else { 324 flowOf(false) 325 } 326 327 /** 328 * Copied from the old pipeline. We maintain a 2s period of time where we will keep the 329 * validated bit from the old active network (A) while data is changing to the new one (B). 330 * 331 * This condition only applies if 332 * 1. A and B are in the same subscription group (e.g. for CBRS data switching) and 333 * 2. A was validated before the switch 334 * 335 * The goal of this is to minimize the flickering in the UI of the cellular indicator 336 */ 337 private val forcingCellularValidation = 338 mobileConnectionsRepo.activeSubChangedInGroupEvent <lambda>null339 .filter { mobileConnectionsRepo.defaultConnectionIsValidated.value } <lambda>null340 .transformLatest { 341 emit(true) 342 delay(2000) 343 emit(false) 344 } 345 .logDiffsForTable( 346 tableLogger, 347 LOGGING_PREFIX, 348 columnName = "forcingValidation", 349 initialValue = false, 350 ) 351 .stateIn(scope, SharingStarted.WhileSubscribed(), false) 352 353 /** 354 * Mapping from network type to [MobileIconGroup] using the config generated for the default 355 * subscription Id. This mapping is the same for every subscription. 356 */ 357 override val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>> = 358 mobileConnectionsRepo.defaultMobileIconMapping.stateIn( 359 scope, 360 SharingStarted.WhileSubscribed(), 361 initialValue = mapOf(), 362 ) 363 364 override val alwaysShowDataRatIcon: StateFlow<Boolean> = 365 mobileConnectionsRepo.defaultDataSubRatConfig <lambda>null366 .mapLatest { it.alwaysShowDataRatIcon } 367 .stateIn(scope, SharingStarted.WhileSubscribed(), false) 368 369 override val alwaysUseCdmaLevel: StateFlow<Boolean> = 370 mobileConnectionsRepo.defaultDataSubRatConfig <lambda>null371 .mapLatest { it.alwaysShowCdmaRssi } 372 .stateIn(scope, SharingStarted.WhileSubscribed(), false) 373 374 override val isSingleCarrier: StateFlow<Boolean> = 375 mobileConnectionsRepo.subscriptions <lambda>null376 .map { it.size == 1 } 377 .logDiffsForTable( 378 tableLogger, 379 columnPrefix = LOGGING_PREFIX, 380 columnName = "isSingleCarrier", 381 initialValue = false, 382 ) 383 .stateIn(scope, SharingStarted.WhileSubscribed(), false) 384 385 /** If there is no mapping in [defaultMobileIconMapping], then use this default icon group */ 386 override val defaultMobileIconGroup: StateFlow<MobileIconGroup> = 387 mobileConnectionsRepo.defaultMobileIconGroup.stateIn( 388 scope, 389 SharingStarted.WhileSubscribed(), 390 initialValue = TelephonyIcons.G, 391 ) 392 393 /** 394 * We want to show an error state when cellular has actually failed to validate, but not if some 395 * other transport type is active, because then we expect there not to be validation. 396 */ 397 override val isDefaultConnectionFailed: StateFlow<Boolean> = 398 combine( 399 mobileIsDefault, 400 mobileConnectionsRepo.defaultConnectionIsValidated, 401 forcingCellularValidation, forcingCellularValidationnull402 ) { mobileIsDefault, defaultConnectionIsValidated, forcingCellularValidation -> 403 when { 404 !mobileIsDefault -> false 405 forcingCellularValidation -> false 406 else -> !defaultConnectionIsValidated 407 } 408 } 409 .logDiffsForTable( 410 tableLogger, 411 LOGGING_PREFIX, 412 columnName = "isDefaultConnectionFailed", 413 initialValue = false, 414 ) 415 .stateIn(scope, SharingStarted.WhileSubscribed(), false) 416 417 override val isUserSetUp: StateFlow<Boolean> = userSetupRepo.isUserSetUp 418 419 override val isForceHidden: Flow<Boolean> = 420 connectivityRepository.forceHiddenSlots <lambda>null421 .map { it.contains(ConnectivitySlot.MOBILE) } 422 .stateIn(scope, SharingStarted.WhileSubscribed(), false) 423 424 override val isDeviceInEmergencyCallsOnlyMode: Flow<Boolean> = 425 mobileConnectionsRepo.isDeviceEmergencyCallCapable 426 427 /** Vends out new [MobileIconInteractor] for a particular subId */ getMobileConnectionInteractorForSubIdnull428 override fun getMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor = 429 reuseCache[subId]?.get() ?: createMobileConnectionInteractorForSubId(subId) 430 431 private fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor = 432 MobileIconInteractorImpl( 433 scope, 434 activeDataConnectionHasDataEnabled, 435 alwaysShowDataRatIcon, 436 alwaysUseCdmaLevel, 437 isSingleCarrier, 438 mobileIsDefault, 439 defaultMobileIconMapping, 440 defaultMobileIconGroup, 441 isDefaultConnectionFailed, 442 isForceHidden, 443 mobileConnectionsRepo.getRepoForSubId(subId), 444 context, 445 ) 446 .also { reuseCache[subId] = WeakReference(it) } 447 448 companion object { 449 private const val LOGGING_PREFIX = "Intr" 450 } 451 } 452