1 /* <lambda>null2 * Copyright (C) 2024 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.systemui.Flags 25 import com.android.systemui.KairosActivatable 26 import com.android.systemui.KairosBuilder 27 import com.android.systemui.activated 28 import com.android.systemui.dagger.SysUISingleton 29 import com.android.systemui.flags.FeatureFlagsClassic 30 import com.android.systemui.flags.Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS 31 import com.android.systemui.kairos.BuildScope 32 import com.android.systemui.kairos.ExperimentalKairosApi 33 import com.android.systemui.kairos.Incremental 34 import com.android.systemui.kairos.State 35 import com.android.systemui.kairos.asyncEvent 36 import com.android.systemui.kairos.buildSpec 37 import com.android.systemui.kairos.combine 38 import com.android.systemui.kairos.filter 39 import com.android.systemui.kairos.flatMap 40 import com.android.systemui.kairos.flatten 41 import com.android.systemui.kairos.map 42 import com.android.systemui.kairos.mapValues 43 import com.android.systemui.kairos.stateOf 44 import com.android.systemui.kairosBuilder 45 import com.android.systemui.log.table.TableLogBuffer 46 import com.android.systemui.log.table.logDiffsForTable 47 import com.android.systemui.statusbar.core.NewStatusBarIcons 48 import com.android.systemui.statusbar.core.StatusBarRootModernization 49 import com.android.systemui.statusbar.pipeline.dagger.MobileSummaryLog 50 import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel 51 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepositoryKairos 52 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepositoryKairos 53 import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel 54 import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot 55 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository 56 import com.android.systemui.statusbar.policy.data.repository.UserSetupRepository 57 import com.android.systemui.util.CarrierConfigTracker 58 import dagger.Binds 59 import dagger.Provides 60 import dagger.multibindings.ElementsIntoSet 61 import javax.inject.Inject 62 import javax.inject.Provider 63 import kotlin.time.Duration.Companion.seconds 64 import kotlinx.coroutines.delay 65 66 /** 67 * Business layer logic for the set of mobile subscription icons. 68 * 69 * This interactor represents known set of mobile subscriptions (represented by [SubscriptionInfo]). 70 * The list of subscriptions is filtered based on the opportunistic flags on the infos. 71 * 72 * It provides the default mapping between the telephony display info and the icon group that 73 * represents each RAT (LTE, 3G, etc.), as well as can produce an interactor for each individual 74 * icon 75 */ 76 @ExperimentalKairosApi 77 interface MobileIconsInteractorKairos { 78 /** See [MobileConnectionsRepository.mobileIsDefault]. */ 79 val mobileIsDefault: State<Boolean> 80 81 /** List of subscriptions, potentially filtered for CBRS */ 82 val filteredSubscriptions: State<List<SubscriptionModel>> 83 84 /** 85 * The current list of [MobileIconInteractor]s associated with the current list of 86 * [filteredSubscriptions] 87 */ 88 val icons: Incremental<Int, MobileIconInteractorKairos> 89 90 /** Whether the mobile icons can be stacked vertically. */ 91 val isStackable: State<Boolean> 92 93 /** True if the active mobile data subscription has data enabled */ 94 val activeDataConnectionHasDataEnabled: State<Boolean> 95 96 /** 97 * Flow providing a reference to the Interactor for the active data subId. This represents the 98 * [MobileIconInteractorKairos] responsible for the active data connection, if any. 99 */ 100 val activeDataIconInteractor: State<MobileIconInteractorKairos?> 101 102 /** True if the RAT icon should always be displayed and false otherwise. */ 103 val alwaysShowDataRatIcon: State<Boolean> 104 105 /** True if the CDMA level should be preferred over the primary level. */ 106 val alwaysUseCdmaLevel: State<Boolean> 107 108 /** True if there is only one active subscription. */ 109 val isSingleCarrier: State<Boolean> 110 111 /** The icon mapping from network type to [MobileIconGroup] for the default subscription */ 112 val defaultMobileIconMapping: State<Map<String, MobileIconGroup>> 113 114 /** Fallback [MobileIconGroup] in the case where there is no icon in the mapping */ 115 val defaultMobileIconGroup: State<MobileIconGroup> 116 117 /** True only if the default network is mobile, and validation also failed */ 118 val isDefaultConnectionFailed: State<Boolean> 119 120 /** True once the user has been set up */ 121 val isUserSetUp: State<Boolean> 122 123 /** True if we're configured to force-hide the mobile icons and false otherwise. */ 124 val isForceHidden: State<Boolean> 125 126 /** 127 * True if the device-level service state (with -1 subscription id) reports emergency calls 128 * only. This value is only useful when there are no other subscriptions OR all existing 129 * subscriptions report that they are not in service. 130 */ 131 val isDeviceInEmergencyCallsOnlyMode: State<Boolean> 132 } 133 134 @ExperimentalKairosApi 135 @SysUISingleton 136 class MobileIconsInteractorKairosImpl 137 @Inject 138 constructor( 139 private val mobileConnectionsRepo: MobileConnectionsRepositoryKairos, 140 private val carrierConfigTracker: CarrierConfigTracker, 141 @MobileSummaryLog private val tableLogger: TableLogBuffer, 142 connectivityRepository: ConnectivityRepository, 143 userSetupRepo: UserSetupRepository, 144 private val context: Context, 145 private val featureFlagsClassic: FeatureFlagsClassic, 146 ) : MobileIconsInteractorKairos, KairosBuilder by kairosBuilder() { 147 148 override val mobileIsDefault: State<Boolean> = 149 combine( 150 mobileConnectionsRepo.mobileIsDefault, 151 mobileConnectionsRepo.hasCarrierMergedConnection, hasCarrierMergedConnectionnull152 ) { mobileIsDefault, hasCarrierMergedConnection -> 153 // Because carrier merged networks are displayed as mobile networks, they're part of 154 // the `isDefault` calculation. See b/272586234. 155 mobileIsDefault || hasCarrierMergedConnection 156 } <lambda>null157 .also { 158 onActivated { 159 logDiffsForTable( 160 it, 161 tableLogger, 162 LOGGING_PREFIX, 163 columnName = "mobileIsDefault", 164 ) 165 } 166 } 167 168 override val activeDataConnectionHasDataEnabled: State<Boolean> = <lambda>null169 mobileConnectionsRepo.activeMobileDataRepository.flatMap { 170 it?.dataEnabled ?: stateOf(false) 171 } 172 173 private val unfilteredSubscriptions: State<Collection<SubscriptionModel>> 174 get() = mobileConnectionsRepo.subscriptions 175 176 /** Any filtering that we can do based purely on the info of each subscription individually. */ 177 private val subscriptionsBasedFilteredSubs: State<List<SubscriptionModel>> = <lambda>null178 unfilteredSubscriptions.map { 179 it.asSequence().filterBasedOnProvisioning().filterBasedOnNtn().toList() 180 } 181 filterBasedOnProvisioningnull182 private fun Sequence<SubscriptionModel>.filterBasedOnProvisioning() = 183 if (!featureFlagsClassic.isEnabled(FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS)) { 184 this 185 } else { <lambda>null186 filter { it.profileClass != PROFILE_CLASS_PROVISIONING } 187 } 188 189 /** 190 * Subscriptions that exclusively support non-terrestrial networks should **never** directly 191 * show any iconography in the status bar. These subscriptions only exist to provide a backing 192 * for the device-based satellite connections, and the iconography for those connections are 193 * already being handled in 194 * [com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository]. We 195 * need to filter out those subscriptions here so we guarantee the subscription never turns into 196 * an icon. See b/336881301. 197 */ Sequencenull198 private fun Sequence<SubscriptionModel>.filterBasedOnNtn(): Sequence<SubscriptionModel> = 199 filter { 200 !it.isExclusivelyNonTerrestrial 201 } 202 203 /** 204 * Generally, SystemUI wants to show iconography for each subscription that is listed by 205 * [SubscriptionManager]. However, in the case of opportunistic subscriptions, we want to only 206 * show a single representation of the pair of subscriptions. The docs define opportunistic as: 207 * 208 * "A subscription is opportunistic (if) the network it connects to has limited coverage" 209 * https://developer.android.com/reference/android/telephony/SubscriptionManager#setOpportunistic(boolean,%20int) 210 * 211 * In the case of opportunistic networks (typically CBRS), we will filter out one of the 212 * subscriptions based on 213 * [CarrierConfigManager.KEY_ALWAYS_SHOW_PRIMARY_SIGNAL_BAR_IN_OPPORTUNISTIC_NETWORK_BOOLEAN], 214 * and by checking which subscription is opportunistic, or which one is active. 215 */ <lambda>null216 override val filteredSubscriptions: State<List<SubscriptionModel>> = buildState { 217 combine( 218 subscriptionsBasedFilteredSubs, 219 mobileConnectionsRepo.activeMobileDataSubscriptionId, 220 connectivityRepository.vcnSubId.toState(), 221 ) { preFilteredSubs, activeId, vcnSubId -> 222 filterSubsBasedOnOpportunistic(preFilteredSubs, activeId, vcnSubId) 223 } 224 .also { 225 logDiffsForTable( 226 it, 227 tableLogger, 228 LOGGING_PREFIX, 229 columnName = "filteredSubscriptions", 230 ) 231 } 232 } 233 filterSubsBasedOnOpportunisticnull234 private fun filterSubsBasedOnOpportunistic( 235 subList: List<SubscriptionModel>, 236 activeId: Int?, 237 vcnSubId: Int?, 238 ): List<SubscriptionModel> { 239 // Based on the old logic, 240 if (subList.size != 2) { 241 return subList 242 } 243 244 val info1 = subList[0] 245 val info2 = subList[1] 246 247 // Filtering only applies to subscriptions in the same group 248 if (info1.groupUuid == null || info1.groupUuid != info2.groupUuid) { 249 return subList 250 } 251 252 // If both subscriptions are primary, show both 253 if (!info1.isOpportunistic && !info2.isOpportunistic) { 254 return subList 255 } 256 257 // NOTE: at this point, we are now returning a single SubscriptionInfo 258 259 // If carrier required, always show the icon of the primary subscription. 260 // Otherwise, show whichever subscription is currently active for internet. 261 if (carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) { 262 // return the non-opportunistic info 263 return if (info1.isOpportunistic) listOf(info2) else listOf(info1) 264 } else { 265 // It's possible for the subId of the VCN to disagree with the active subId in 266 // cases where the system has tried to switch but found no connection. In these 267 // scenarios, VCN will always have the subId that we want to use, so use that 268 // value instead of the activeId reported by telephony 269 val subIdToKeep = vcnSubId ?: activeId 270 271 return if (info1.subscriptionId == subIdToKeep) { 272 listOf(info1) 273 } else { 274 listOf(info2) 275 } 276 } 277 } 278 <lambda>null279 override val icons: Incremental<Int, MobileIconInteractorKairos> = buildIncremental { 280 val filteredSubIds = 281 filteredSubscriptions.map { it.asSequence().map { sub -> sub.subscriptionId }.toSet() } 282 mobileConnectionsRepo.mobileConnectionsBySubId 283 .filterIncrementally { (subId, _) -> 284 // Filter out repo if subId is not present in the filtered set 285 filteredSubIds.map { subId in it } 286 } 287 // Just map the repos to interactors 288 .mapValues { (subId, repo) -> buildSpec { mobileConnection(repo) } } 289 .applyLatestSpecForKey() 290 } 291 292 override val isStackable: State<Boolean> = 293 if (NewStatusBarIcons.isEnabled && StatusBarRootModernization.isEnabled) { iconsBySubIdnull294 icons.flatMap { iconsBySubId: Map<Int, MobileIconInteractorKairos> -> 295 iconsBySubId.values 296 .map { it.signalLevelIcon } 297 .combine { signalLevelIcons -> 298 // These are only stackable if: 299 // - They are cellular 300 // - There's exactly two 301 // - They have the same number of levels 302 signalLevelIcons.filterIsInstance<SignalIconModel.Cellular>().let { 303 it.size == 2 && it[0].numberOfLevels == it[1].numberOfLevels 304 } 305 } 306 } 307 } else { 308 stateOf(false) 309 } 310 311 override val activeDataIconInteractor: State<MobileIconInteractorKairos?> = activeSubIdnull312 combine(mobileConnectionsRepo.activeMobileDataSubscriptionId, icons) { activeSubId, icons -> 313 activeSubId?.let { icons[activeSubId] } 314 } 315 316 /** 317 * Copied from the old pipeline. We maintain a 2s period of time where we will keep the 318 * validated bit from the old active network (A) while data is changing to the new one (B). 319 * 320 * This condition only applies if 321 * 1. A and B are in the same subscription group (e.g. for CBRS data switching) and 322 * 2. A was validated before the switch 323 * 324 * The goal of this is to minimize the flickering in the UI of the cellular indicator 325 */ <lambda>null326 private val forcingCellularValidation: State<Boolean> = buildState { 327 mobileConnectionsRepo.activeSubChangedInGroupEvent 328 .filter(mobileConnectionsRepo.defaultConnectionIsValidated) 329 .mapLatestBuild { 330 asyncEvent { 331 delay(2.seconds) 332 false 333 } 334 .holdState(true) 335 } 336 .holdState(stateOf(false)) 337 .flatten() 338 .also { 339 logDiffsForTable(it, tableLogger, LOGGING_PREFIX, columnName = "forcingValidation") 340 } 341 } 342 343 /** 344 * Mapping from network type to [MobileIconGroup] using the config generated for the default 345 * subscription Id. This mapping is the same for every subscription. 346 */ 347 override val defaultMobileIconMapping: State<Map<String, MobileIconGroup>> 348 get() = mobileConnectionsRepo.defaultMobileIconMapping 349 350 override val alwaysShowDataRatIcon: State<Boolean> = <lambda>null351 mobileConnectionsRepo.defaultDataSubRatConfig.map { it.alwaysShowDataRatIcon } 352 353 override val alwaysUseCdmaLevel: State<Boolean> = <lambda>null354 mobileConnectionsRepo.defaultDataSubRatConfig.map { it.alwaysShowCdmaRssi } 355 356 override val isSingleCarrier: State<Boolean> = 357 mobileConnectionsRepo.subscriptions <lambda>null358 .map { it.size == 1 } <lambda>null359 .also { 360 onActivated { 361 logDiffsForTable( 362 it, 363 tableLogger, 364 columnPrefix = LOGGING_PREFIX, 365 columnName = "isSingleCarrier", 366 ) 367 } 368 } 369 370 /** If there is no mapping in [defaultMobileIconMapping], then use this default icon group */ 371 override val defaultMobileIconGroup: State<MobileIconGroup> 372 get() = mobileConnectionsRepo.defaultMobileIconGroup 373 374 /** 375 * We want to show an error state when cellular has actually failed to validate, but not if some 376 * other transport type is active, because then we expect there not to be validation. 377 */ 378 override val isDefaultConnectionFailed: State<Boolean> = 379 combine( 380 mobileIsDefault, 381 mobileConnectionsRepo.defaultConnectionIsValidated, 382 forcingCellularValidation, forcingCellularValidationnull383 ) { mobileIsDefault, defaultConnectionIsValidated, forcingCellularValidation -> 384 when { 385 !mobileIsDefault -> false 386 forcingCellularValidation -> false 387 else -> !defaultConnectionIsValidated 388 } 389 } <lambda>null390 .also { 391 onActivated { 392 logDiffsForTable( 393 it, 394 tableLogger, 395 LOGGING_PREFIX, 396 columnName = "isDefaultConnectionFailed", 397 ) 398 } 399 } 400 <lambda>null401 override val isUserSetUp: State<Boolean> = buildState { userSetupRepo.isUserSetUp.toState() } 402 <lambda>null403 override val isForceHidden: State<Boolean> = buildState { 404 connectivityRepository.forceHiddenSlots.toState().map { 405 it.contains(ConnectivitySlot.MOBILE) 406 } 407 } 408 409 override val isDeviceInEmergencyCallsOnlyMode: State<Boolean> 410 get() = mobileConnectionsRepo.isDeviceEmergencyCallCapable 411 412 /** Vends out a new [MobileIconInteractorKairos] for a particular subId */ BuildScopenull413 private fun BuildScope.mobileConnection( 414 repo: MobileConnectionRepositoryKairos 415 ): MobileIconInteractorKairos = activated { 416 MobileIconInteractorKairosImpl( 417 activeDataConnectionHasDataEnabled, 418 alwaysShowDataRatIcon, 419 alwaysUseCdmaLevel, 420 isSingleCarrier, 421 mobileIsDefault, 422 defaultMobileIconMapping, 423 defaultMobileIconGroup, 424 isDefaultConnectionFailed, 425 isForceHidden, 426 repo, 427 context, 428 ) 429 } 430 431 companion object { 432 private const val LOGGING_PREFIX = "Intr" 433 } 434 435 @dagger.Module 436 interface Module { 437 bindImplnull438 @Binds fun bindImpl(impl: MobileIconsInteractorKairosImpl): MobileIconsInteractorKairos 439 440 companion object { 441 @Provides 442 @ElementsIntoSet 443 fun kairosActivatable( 444 impl: Provider<MobileIconsInteractorKairosImpl> 445 ): Set<@JvmSuppressWildcards KairosActivatable> = 446 if (Flags.statusBarMobileIconKairos()) setOf(impl.get()) else emptySet() 447 } 448 } 449 } 450