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