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