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.data.repository.prod 18 19 import android.annotation.SuppressLint 20 import android.content.Context 21 import android.content.IntentFilter 22 import android.telephony.CarrierConfigManager 23 import android.telephony.SubscriptionInfo 24 import android.telephony.SubscriptionManager 25 import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID 26 import android.telephony.TelephonyCallback 27 import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener 28 import android.telephony.TelephonyManager 29 import androidx.annotation.VisibleForTesting 30 import com.android.internal.telephony.PhoneConstants 31 import com.android.settingslib.SignalIcon.MobileIconGroup 32 import com.android.settingslib.mobile.MobileMappings.Config 33 import com.android.systemui.R 34 import com.android.systemui.broadcast.BroadcastDispatcher 35 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow 36 import com.android.systemui.dagger.SysUISingleton 37 import com.android.systemui.dagger.qualifiers.Application 38 import com.android.systemui.dagger.qualifiers.Background 39 import com.android.systemui.log.table.TableLogBuffer 40 import com.android.systemui.log.table.logDiffsForTable 41 import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository 42 import com.android.systemui.statusbar.pipeline.dagger.MobileSummaryLog 43 import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger 44 import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel 45 import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel 46 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository 47 import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy 48 import com.android.systemui.statusbar.pipeline.mobile.util.SubscriptionManagerProxy 49 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository 50 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository 51 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel 52 import com.android.systemui.util.kotlin.pairwise 53 import javax.inject.Inject 54 import kotlinx.coroutines.CoroutineDispatcher 55 import kotlinx.coroutines.CoroutineScope 56 import kotlinx.coroutines.ExperimentalCoroutinesApi 57 import kotlinx.coroutines.asExecutor 58 import kotlinx.coroutines.channels.awaitClose 59 import kotlinx.coroutines.flow.Flow 60 import kotlinx.coroutines.flow.SharingStarted 61 import kotlinx.coroutines.flow.StateFlow 62 import kotlinx.coroutines.flow.combine 63 import kotlinx.coroutines.flow.distinctUntilChanged 64 import kotlinx.coroutines.flow.flowOn 65 import kotlinx.coroutines.flow.map 66 import kotlinx.coroutines.flow.mapLatest 67 import kotlinx.coroutines.flow.mapNotNull 68 import kotlinx.coroutines.flow.merge 69 import kotlinx.coroutines.flow.onEach 70 import kotlinx.coroutines.flow.onStart 71 import kotlinx.coroutines.flow.stateIn 72 import kotlinx.coroutines.withContext 73 74 @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") 75 @OptIn(ExperimentalCoroutinesApi::class) 76 @SysUISingleton 77 class MobileConnectionsRepositoryImpl 78 @Inject 79 constructor( 80 connectivityRepository: ConnectivityRepository, 81 private val subscriptionManager: SubscriptionManager, 82 private val subscriptionManagerProxy: SubscriptionManagerProxy, 83 private val telephonyManager: TelephonyManager, 84 private val logger: MobileInputLogger, 85 @MobileSummaryLog private val tableLogger: TableLogBuffer, 86 mobileMappingsProxy: MobileMappingsProxy, 87 broadcastDispatcher: BroadcastDispatcher, 88 private val context: Context, 89 @Background private val bgDispatcher: CoroutineDispatcher, 90 @Application private val scope: CoroutineScope, 91 airplaneModeRepository: AirplaneModeRepository, 92 // Some "wifi networks" should be rendered as a mobile connection, which is why the wifi 93 // repository is an input to the mobile repository. 94 // See [CarrierMergedConnectionRepository] for details. 95 wifiRepository: WifiRepository, 96 private val fullMobileRepoFactory: FullMobileConnectionRepository.Factory, 97 ) : MobileConnectionsRepository { 98 private var subIdRepositoryCache: MutableMap<Int, FullMobileConnectionRepository> = 99 mutableMapOf() 100 101 private val defaultNetworkName = 102 NetworkNameModel.Default( 103 context.getString(com.android.internal.R.string.lockscreen_carrier_default) 104 ) 105 106 private val networkNameSeparator: String = 107 context.getString(R.string.status_bar_network_name_separator) 108 109 private val carrierMergedSubId: StateFlow<Int?> = 110 combine( 111 wifiRepository.wifiNetwork, 112 connectivityRepository.defaultConnections, 113 airplaneModeRepository.isAirplaneMode, 114 ) { wifiNetwork, defaultConnections, isAirplaneMode -> 115 // The carrier merged connection should only be used if it's also the default 116 // connection or mobile connections aren't available because of airplane mode. 117 val defaultConnectionIsNonMobile = 118 defaultConnections.carrierMerged.isDefault || 119 defaultConnections.wifi.isDefault || 120 isAirplaneMode 121 122 if (wifiNetwork is WifiNetworkModel.CarrierMerged && defaultConnectionIsNonMobile) { 123 wifiNetwork.subscriptionId 124 } else { 125 null 126 } 127 } 128 .distinctUntilChanged() 129 .logDiffsForTable( 130 tableLogger, 131 LOGGING_PREFIX, 132 columnName = "carrierMergedSubId", 133 initialValue = null, 134 ) 135 .stateIn(scope, started = SharingStarted.WhileSubscribed(), null) 136 137 private val mobileSubscriptionsChangeEvent: Flow<Unit> = conflatedCallbackFlow { 138 val callback = 139 object : SubscriptionManager.OnSubscriptionsChangedListener() { 140 override fun onSubscriptionsChanged() { 141 logger.logOnSubscriptionsChanged() 142 trySend(Unit) 143 } 144 } 145 146 subscriptionManager.addOnSubscriptionsChangedListener( 147 bgDispatcher.asExecutor(), 148 callback, 149 ) 150 151 awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) } 152 } 153 154 /** 155 * State flow that emits the set of mobile data subscriptions, each represented by its own 156 * [SubscriptionModel]. 157 */ 158 override val subscriptions: StateFlow<List<SubscriptionModel>> = 159 merge(mobileSubscriptionsChangeEvent, carrierMergedSubId) 160 .mapLatest { fetchSubscriptionsList().map { it.toSubscriptionModel() } } 161 .onEach { infos -> updateRepos(infos) } 162 .distinctUntilChanged() 163 .logDiffsForTable( 164 tableLogger, 165 LOGGING_PREFIX, 166 columnName = "subscriptions", 167 initialValue = listOf(), 168 ) 169 .stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf()) 170 171 override val activeMobileDataSubscriptionId: StateFlow<Int?> = 172 conflatedCallbackFlow { 173 val callback = 174 object : TelephonyCallback(), ActiveDataSubscriptionIdListener { 175 override fun onActiveDataSubscriptionIdChanged(subId: Int) { 176 if (subId != INVALID_SUBSCRIPTION_ID) { 177 trySend(subId) 178 } else { 179 trySend(null) 180 } 181 } 182 } 183 184 telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback) 185 awaitClose { telephonyManager.unregisterTelephonyCallback(callback) } 186 } 187 .distinctUntilChanged() 188 .logDiffsForTable( 189 tableLogger, 190 LOGGING_PREFIX, 191 columnName = "activeSubId", 192 initialValue = INVALID_SUBSCRIPTION_ID, 193 ) 194 .stateIn(scope, started = SharingStarted.WhileSubscribed(), null) 195 196 override val activeMobileDataRepository = 197 activeMobileDataSubscriptionId 198 .map { activeSubId -> 199 if (activeSubId == null) { 200 null 201 } else { 202 getOrCreateRepoForSubId(activeSubId) 203 } 204 } 205 .stateIn(scope, SharingStarted.WhileSubscribed(), null) 206 207 override val defaultDataSubId: StateFlow<Int> = 208 broadcastDispatcher 209 .broadcastFlow( 210 IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED), 211 ) { intent, _ -> 212 intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID) 213 } 214 .distinctUntilChanged() 215 .logDiffsForTable( 216 tableLogger, 217 LOGGING_PREFIX, 218 columnName = "defaultSubId", 219 initialValue = INVALID_SUBSCRIPTION_ID, 220 ) 221 .onStart { emit(subscriptionManagerProxy.getDefaultDataSubscriptionId()) } 222 .stateIn(scope, SharingStarted.WhileSubscribed(), INVALID_SUBSCRIPTION_ID) 223 224 private val carrierConfigChangedEvent = 225 broadcastDispatcher 226 .broadcastFlow(IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)) 227 .onEach { logger.logActionCarrierConfigChanged() } 228 229 override val defaultDataSubRatConfig: StateFlow<Config> = 230 merge(defaultDataSubId, carrierConfigChangedEvent) 231 .onStart { emit(Unit) } 232 .mapLatest { Config.readConfig(context) } 233 .distinctUntilChanged() 234 .onEach { logger.logDefaultDataSubRatConfig(it) } 235 .stateIn( 236 scope, 237 SharingStarted.WhileSubscribed(), 238 initialValue = Config.readConfig(context) 239 ) 240 241 override val defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>> = 242 defaultDataSubRatConfig 243 .map { mobileMappingsProxy.mapIconSets(it) } 244 .distinctUntilChanged() 245 .onEach { logger.logDefaultMobileIconMapping(it) } 246 247 override val defaultMobileIconGroup: Flow<MobileIconGroup> = 248 defaultDataSubRatConfig 249 .map { mobileMappingsProxy.getDefaultIcons(it) } 250 .distinctUntilChanged() 251 .onEach { logger.logDefaultMobileIconGroup(it) } 252 253 override fun getRepoForSubId(subId: Int): FullMobileConnectionRepository = 254 getOrCreateRepoForSubId(subId) 255 256 private fun getOrCreateRepoForSubId(subId: Int) = 257 subIdRepositoryCache[subId] 258 ?: createRepositoryForSubId(subId).also { subIdRepositoryCache[subId] = it } 259 260 override val mobileIsDefault: StateFlow<Boolean> = 261 connectivityRepository.defaultConnections 262 .map { it.mobile.isDefault } 263 .distinctUntilChanged() 264 .logDiffsForTable( 265 tableLogger, 266 columnPrefix = LOGGING_PREFIX, 267 columnName = "mobileIsDefault", 268 initialValue = false, 269 ) 270 .stateIn(scope, SharingStarted.WhileSubscribed(), false) 271 272 override val hasCarrierMergedConnection: StateFlow<Boolean> = 273 carrierMergedSubId 274 .map { it != null } 275 .distinctUntilChanged() 276 .logDiffsForTable( 277 tableLogger, 278 columnPrefix = LOGGING_PREFIX, 279 columnName = "hasCarrierMergedConnection", 280 initialValue = false, 281 ) 282 .stateIn(scope, SharingStarted.WhileSubscribed(), false) 283 284 override val defaultConnectionIsValidated: StateFlow<Boolean> = 285 connectivityRepository.defaultConnections 286 .map { it.isValidated } 287 .distinctUntilChanged() 288 .logDiffsForTable( 289 tableLogger, 290 columnPrefix = "", 291 columnName = "defaultConnectionIsValidated", 292 initialValue = false, 293 ) 294 .stateIn(scope, SharingStarted.WhileSubscribed(), false) 295 296 /** 297 * Flow that tracks the active mobile data subscriptions. Emits `true` whenever the active data 298 * subscription Id changes but the subscription group remains the same. In these cases, we want 299 * to retain the previous subscription's validation status for up to 2s to avoid flickering the 300 * icon. 301 * 302 * TODO(b/265164432): we should probably expose all change events, not just same group 303 */ 304 @SuppressLint("MissingPermission") 305 override val activeSubChangedInGroupEvent = 306 activeMobileDataSubscriptionId 307 .pairwise() 308 .mapNotNull { (prevVal: Int?, newVal: Int?) -> 309 if (prevVal == null || newVal == null) return@mapNotNull null 310 311 val prevSub = subscriptionManager.getActiveSubscriptionInfo(prevVal)?.groupUuid 312 val nextSub = subscriptionManager.getActiveSubscriptionInfo(newVal)?.groupUuid 313 314 if (prevSub != null && prevSub == nextSub) Unit else null 315 } 316 .flowOn(bgDispatcher) 317 318 private fun isValidSubId(subId: Int): Boolean = checkSub(subId, subscriptions.value) 319 320 @VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache 321 322 private fun subscriptionModelForSubId(subId: Int): StateFlow<SubscriptionModel?> { 323 return subscriptions 324 .map { list -> list.firstOrNull { model -> model.subscriptionId == subId } } 325 .stateIn(scope, SharingStarted.WhileSubscribed(), null) 326 } 327 328 private fun createRepositoryForSubId(subId: Int): FullMobileConnectionRepository { 329 return fullMobileRepoFactory.build( 330 subId, 331 isCarrierMerged(subId), 332 subscriptionModelForSubId(subId), 333 defaultNetworkName, 334 networkNameSeparator, 335 ) 336 } 337 338 private fun updateRepos(newInfos: List<SubscriptionModel>) { 339 dropUnusedReposFromCache(newInfos) 340 subIdRepositoryCache.forEach { (subId, repo) -> 341 repo.setIsCarrierMerged(isCarrierMerged(subId)) 342 } 343 } 344 345 private fun isCarrierMerged(subId: Int): Boolean { 346 return subId == carrierMergedSubId.value 347 } 348 349 private fun dropUnusedReposFromCache(newInfos: List<SubscriptionModel>) { 350 // Remove any connection repository from the cache that isn't in the new set of IDs. They 351 // will get garbage collected once their subscribers go away 352 subIdRepositoryCache = 353 subIdRepositoryCache.filter { checkSub(it.key, newInfos) }.toMutableMap() 354 } 355 356 /** 357 * True if the checked subId is in the list of current subs or the active mobile data subId 358 * 359 * @param checkedSubs the list to validate [subId] against. To invalidate the cache, pass in the 360 * new subscription list. Otherwise use [subscriptions.value] to validate a subId against the 361 * current known subscriptions 362 */ 363 private fun checkSub(subId: Int, checkedSubs: List<SubscriptionModel>): Boolean { 364 if (activeMobileDataSubscriptionId.value == subId) return true 365 366 checkedSubs.forEach { 367 if (it.subscriptionId == subId) { 368 return true 369 } 370 } 371 372 return false 373 } 374 375 private suspend fun fetchSubscriptionsList(): List<SubscriptionInfo> = 376 withContext(bgDispatcher) { subscriptionManager.completeActiveSubscriptionInfoList } 377 378 private fun SubscriptionInfo.toSubscriptionModel(): SubscriptionModel = 379 SubscriptionModel( 380 subscriptionId = subscriptionId, 381 isOpportunistic = isOpportunistic, 382 groupUuid = groupUuid, 383 carrierName = carrierName.toString(), 384 ) 385 386 companion object { 387 private const val LOGGING_PREFIX = "Repo" 388 } 389 } 390