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