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.data.repository.prod 18 19 import android.annotation.SuppressLint 20 import android.content.Context 21 import android.content.Intent 22 import android.content.IntentFilter 23 import android.telephony.CarrierConfigManager 24 import android.telephony.ServiceState 25 import android.telephony.SubscriptionInfo 26 import android.telephony.SubscriptionManager 27 import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID 28 import android.telephony.TelephonyCallback 29 import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener 30 import android.telephony.TelephonyManager 31 import android.util.IndentingPrintWriter 32 import androidx.annotation.VisibleForTesting 33 import com.android.internal.telephony.PhoneConstants 34 import com.android.keyguard.KeyguardUpdateMonitor 35 import com.android.keyguard.KeyguardUpdateMonitorCallback 36 import com.android.settingslib.SignalIcon.MobileIconGroup 37 import com.android.settingslib.mobile.MobileMappings.Config 38 import com.android.systemui.Dumpable 39 import com.android.systemui.broadcast.BroadcastDispatcher 40 import com.android.systemui.dagger.SysUISingleton 41 import com.android.systemui.dagger.qualifiers.Application 42 import com.android.systemui.dagger.qualifiers.Background 43 import com.android.systemui.dagger.qualifiers.Main 44 import com.android.systemui.dump.DumpManager 45 import com.android.systemui.log.table.TableLogBuffer 46 import com.android.systemui.log.table.logDiffsForTable 47 import com.android.systemui.res.R 48 import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository 49 import com.android.systemui.statusbar.pipeline.dagger.MobileSummaryLog 50 import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger 51 import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel 52 import com.android.systemui.statusbar.pipeline.mobile.data.model.ServiceStateModel 53 import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel 54 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository 55 import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy 56 import com.android.systemui.statusbar.pipeline.mobile.util.SubscriptionManagerProxy 57 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository 58 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository 59 import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel 60 import com.android.systemui.util.kotlin.pairwise 61 import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow 62 import java.io.PrintWriter 63 import java.lang.ref.WeakReference 64 import javax.inject.Inject 65 import kotlinx.coroutines.CoroutineDispatcher 66 import kotlinx.coroutines.CoroutineScope 67 import kotlinx.coroutines.ExperimentalCoroutinesApi 68 import kotlinx.coroutines.asExecutor 69 import kotlinx.coroutines.channels.awaitClose 70 import kotlinx.coroutines.flow.Flow 71 import kotlinx.coroutines.flow.SharingStarted 72 import kotlinx.coroutines.flow.StateFlow 73 import kotlinx.coroutines.flow.combine 74 import kotlinx.coroutines.flow.distinctUntilChanged 75 import kotlinx.coroutines.flow.filterNotNull 76 import kotlinx.coroutines.flow.flowOn 77 import kotlinx.coroutines.flow.map 78 import kotlinx.coroutines.flow.mapLatest 79 import kotlinx.coroutines.flow.mapNotNull 80 import kotlinx.coroutines.flow.merge 81 import kotlinx.coroutines.flow.onEach 82 import kotlinx.coroutines.flow.onStart 83 import kotlinx.coroutines.flow.stateIn 84 import kotlinx.coroutines.withContext 85 86 @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") 87 @OptIn(ExperimentalCoroutinesApi::class) 88 @SysUISingleton 89 class MobileConnectionsRepositoryImpl 90 @Inject 91 constructor( 92 connectivityRepository: ConnectivityRepository, 93 private val subscriptionManager: SubscriptionManager, 94 private val subscriptionManagerProxy: SubscriptionManagerProxy, 95 private val telephonyManager: TelephonyManager, 96 private val logger: MobileInputLogger, 97 @MobileSummaryLog private val tableLogger: TableLogBuffer, 98 mobileMappingsProxy: MobileMappingsProxy, 99 broadcastDispatcher: BroadcastDispatcher, 100 private val context: Context, 101 @Background private val bgDispatcher: CoroutineDispatcher, 102 @Application private val scope: CoroutineScope, 103 @Main private val mainDispatcher: CoroutineDispatcher, 104 airplaneModeRepository: AirplaneModeRepository, 105 // Some "wifi networks" should be rendered as a mobile connection, which is why the wifi 106 // repository is an input to the mobile repository. 107 // See [CarrierMergedConnectionRepository] for details. 108 wifiRepository: WifiRepository, 109 private val fullMobileRepoFactory: FullMobileConnectionRepository.Factory, 110 private val keyguardUpdateMonitor: KeyguardUpdateMonitor, 111 private val dumpManager: DumpManager, 112 ) : MobileConnectionsRepository, Dumpable { 113 114 // TODO(b/333912012): for now, we are never invalidating the cache. We can do better though 115 private var subIdRepositoryCache: 116 MutableMap<Int, WeakReference<FullMobileConnectionRepository>> = 117 mutableMapOf() 118 119 private val defaultNetworkName = 120 NetworkNameModel.Default( 121 context.getString(com.android.internal.R.string.lockscreen_carrier_default) 122 ) 123 124 private val networkNameSeparator: String = 125 context.getString(R.string.status_bar_network_name_separator) 126 127 init { 128 dumpManager.registerNormalDumpable("MobileConnectionsRepository", this) 129 } 130 131 private val carrierMergedSubId: StateFlow<Int?> = 132 combine( 133 wifiRepository.wifiNetwork, 134 connectivityRepository.defaultConnections, 135 airplaneModeRepository.isAirplaneMode, 136 ) { wifiNetwork, defaultConnections, isAirplaneMode -> 137 // The carrier merged connection should only be used if it's also the default 138 // connection or mobile connections aren't available because of airplane mode. 139 val defaultConnectionIsNonMobile = 140 defaultConnections.carrierMerged.isDefault || 141 defaultConnections.wifi.isDefault || 142 isAirplaneMode 143 144 if (wifiNetwork is WifiNetworkModel.CarrierMerged && defaultConnectionIsNonMobile) { 145 wifiNetwork.subscriptionId 146 } else { 147 null 148 } 149 } 150 .distinctUntilChanged() 151 .logDiffsForTable( 152 tableLogger, 153 LOGGING_PREFIX, 154 columnName = "carrierMergedSubId", 155 initialValue = null, 156 ) 157 .stateIn(scope, started = SharingStarted.WhileSubscribed(), null) 158 159 private val mobileSubscriptionsChangeEvent: Flow<Unit> = 160 conflatedCallbackFlow { 161 val callback = 162 object : SubscriptionManager.OnSubscriptionsChangedListener() { 163 override fun onSubscriptionsChanged() { 164 logger.logOnSubscriptionsChanged() 165 trySend(Unit) 166 } 167 } 168 169 subscriptionManager.addOnSubscriptionsChangedListener( 170 bgDispatcher.asExecutor(), 171 callback, 172 ) 173 174 awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) } 175 } 176 .flowOn(bgDispatcher) 177 178 /** Note that this flow is eager, so we don't miss any state */ 179 override val deviceServiceState: StateFlow<ServiceStateModel?> = 180 broadcastDispatcher 181 .broadcastFlow(IntentFilter(Intent.ACTION_SERVICE_STATE)) { intent, _ -> 182 val subId = 183 intent.getIntExtra( 184 SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX, 185 INVALID_SUBSCRIPTION_ID 186 ) 187 188 val extras = intent.extras 189 if (extras == null) { 190 logger.logTopLevelServiceStateBroadcastMissingExtras(subId) 191 return@broadcastFlow null 192 } 193 194 val serviceState = ServiceState.newFromBundle(extras) 195 logger.logTopLevelServiceStateBroadcastEmergencyOnly(subId, serviceState) 196 if (subId == INVALID_SUBSCRIPTION_ID) { 197 // Assume that -1 here is the device's service state. We don't care about 198 // other ones. 199 ServiceStateModel.fromServiceState(serviceState) 200 } else { 201 null 202 } 203 } 204 .filterNotNull() 205 .stateIn(scope, SharingStarted.Eagerly, null) 206 207 /** 208 * State flow that emits the set of mobile data subscriptions, each represented by its own 209 * [SubscriptionModel]. 210 */ 211 override val subscriptions: StateFlow<List<SubscriptionModel>> = 212 merge(mobileSubscriptionsChangeEvent, carrierMergedSubId) 213 .mapLatest { fetchSubscriptionsList().map { it.toSubscriptionModel() } } 214 .onEach { infos -> updateRepos(infos) } 215 .distinctUntilChanged() 216 .logDiffsForTable( 217 tableLogger, 218 LOGGING_PREFIX, 219 columnName = "subscriptions", 220 initialValue = listOf(), 221 ) 222 .stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf()) 223 224 override val activeMobileDataSubscriptionId: StateFlow<Int?> = 225 conflatedCallbackFlow { 226 val callback = 227 object : TelephonyCallback(), ActiveDataSubscriptionIdListener { 228 override fun onActiveDataSubscriptionIdChanged(subId: Int) { 229 if (subId != INVALID_SUBSCRIPTION_ID) { 230 trySend(subId) 231 } else { 232 trySend(null) 233 } 234 } 235 } 236 237 telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback) 238 awaitClose { telephonyManager.unregisterTelephonyCallback(callback) } 239 } 240 .flowOn(bgDispatcher) 241 .distinctUntilChanged() 242 .logDiffsForTable( 243 tableLogger, 244 LOGGING_PREFIX, 245 columnName = "activeSubId", 246 initialValue = INVALID_SUBSCRIPTION_ID, 247 ) 248 .stateIn(scope, started = SharingStarted.WhileSubscribed(), null) 249 250 override val activeMobileDataRepository = 251 activeMobileDataSubscriptionId 252 .map { activeSubId -> 253 if (activeSubId == null) { 254 null 255 } else { 256 getOrCreateRepoForSubId(activeSubId) 257 } 258 } 259 .stateIn(scope, SharingStarted.WhileSubscribed(), null) 260 261 override val defaultDataSubId: StateFlow<Int> = 262 broadcastDispatcher 263 .broadcastFlow( 264 IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED), 265 ) { intent, _ -> 266 intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID) 267 } 268 .distinctUntilChanged() 269 .logDiffsForTable( 270 tableLogger, 271 LOGGING_PREFIX, 272 columnName = "defaultSubId", 273 initialValue = INVALID_SUBSCRIPTION_ID, 274 ) 275 .onStart { emit(subscriptionManagerProxy.getDefaultDataSubscriptionId()) } 276 .stateIn(scope, SharingStarted.WhileSubscribed(), INVALID_SUBSCRIPTION_ID) 277 278 private val carrierConfigChangedEvent = 279 broadcastDispatcher 280 .broadcastFlow(IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)) 281 .onEach { logger.logActionCarrierConfigChanged() } 282 283 override val defaultDataSubRatConfig: StateFlow<Config> = 284 merge(defaultDataSubId, carrierConfigChangedEvent) 285 .onStart { emit(Unit) } 286 .mapLatest { Config.readConfig(context) } 287 .distinctUntilChanged() 288 .onEach { logger.logDefaultDataSubRatConfig(it) } 289 .stateIn( 290 scope, 291 SharingStarted.WhileSubscribed(), 292 initialValue = Config.readConfig(context) 293 ) 294 295 override val defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>> = 296 defaultDataSubRatConfig 297 .map { mobileMappingsProxy.mapIconSets(it) } 298 .distinctUntilChanged() 299 .onEach { logger.logDefaultMobileIconMapping(it) } 300 301 override val defaultMobileIconGroup: Flow<MobileIconGroup> = 302 defaultDataSubRatConfig 303 .map { mobileMappingsProxy.getDefaultIcons(it) } 304 .distinctUntilChanged() 305 .onEach { logger.logDefaultMobileIconGroup(it) } 306 307 override val isAnySimSecure: Flow<Boolean> = 308 conflatedCallbackFlow { 309 val callback = 310 object : KeyguardUpdateMonitorCallback() { 311 override fun onSimStateChanged(subId: Int, slotId: Int, simState: Int) { 312 logger.logOnSimStateChanged() 313 trySend(getIsAnySimSecure()) 314 } 315 } 316 keyguardUpdateMonitor.registerCallback(callback) 317 trySend(false) 318 awaitClose { keyguardUpdateMonitor.removeCallback(callback) } 319 } 320 .flowOn(mainDispatcher) 321 .logDiffsForTable( 322 tableLogger, 323 LOGGING_PREFIX, 324 columnName = "isAnySimSecure", 325 initialValue = false, 326 ) 327 .distinctUntilChanged() 328 329 override fun getIsAnySimSecure() = keyguardUpdateMonitor.isSimPinSecure 330 331 override fun getRepoForSubId(subId: Int): FullMobileConnectionRepository = 332 getOrCreateRepoForSubId(subId) 333 334 private fun getOrCreateRepoForSubId(subId: Int) = 335 subIdRepositoryCache[subId]?.get() 336 ?: createRepositoryForSubId(subId).also { 337 subIdRepositoryCache[subId] = WeakReference(it) 338 } 339 340 override val mobileIsDefault: StateFlow<Boolean> = 341 connectivityRepository.defaultConnections 342 .map { it.mobile.isDefault } 343 .distinctUntilChanged() 344 .logDiffsForTable( 345 tableLogger, 346 columnPrefix = LOGGING_PREFIX, 347 columnName = "mobileIsDefault", 348 initialValue = false, 349 ) 350 .stateIn(scope, SharingStarted.WhileSubscribed(), false) 351 352 override val hasCarrierMergedConnection: StateFlow<Boolean> = 353 carrierMergedSubId 354 .map { it != null } 355 .distinctUntilChanged() 356 .logDiffsForTable( 357 tableLogger, 358 columnPrefix = LOGGING_PREFIX, 359 columnName = "hasCarrierMergedConnection", 360 initialValue = false, 361 ) 362 .stateIn(scope, SharingStarted.WhileSubscribed(), false) 363 364 override val defaultConnectionIsValidated: StateFlow<Boolean> = 365 connectivityRepository.defaultConnections 366 .map { it.isValidated } 367 .distinctUntilChanged() 368 .logDiffsForTable( 369 tableLogger, 370 columnPrefix = "", 371 columnName = "defaultConnectionIsValidated", 372 initialValue = false, 373 ) 374 .stateIn(scope, SharingStarted.WhileSubscribed(), false) 375 376 /** 377 * Flow that tracks the active mobile data subscriptions. Emits `true` whenever the active data 378 * subscription Id changes but the subscription group remains the same. In these cases, we want 379 * to retain the previous subscription's validation status for up to 2s to avoid flickering the 380 * icon. 381 * 382 * TODO(b/265164432): we should probably expose all change events, not just same group 383 */ 384 @SuppressLint("MissingPermission") 385 override val activeSubChangedInGroupEvent = 386 activeMobileDataSubscriptionId 387 .pairwise() 388 .mapNotNull { (prevVal: Int?, newVal: Int?) -> 389 if (prevVal == null || newVal == null) return@mapNotNull null 390 391 val prevSub = subscriptionManager.getActiveSubscriptionInfo(prevVal)?.groupUuid 392 val nextSub = subscriptionManager.getActiveSubscriptionInfo(newVal)?.groupUuid 393 394 if (prevSub != null && prevSub == nextSub) Unit else null 395 } 396 .flowOn(bgDispatcher) 397 398 override suspend fun isInEcmMode(): Boolean { 399 if (telephonyManager.emergencyCallbackMode) { 400 return true 401 } 402 return with(subscriptions.value) { 403 any { getOrCreateRepoForSubId(it.subscriptionId).isInEcmMode() } 404 } 405 } 406 407 private fun isValidSubId(subId: Int): Boolean = checkSub(subId, subscriptions.value) 408 409 @VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache 410 411 private fun subscriptionModelForSubId(subId: Int): Flow<SubscriptionModel?> { 412 return subscriptions.map { list -> 413 list.firstOrNull { model -> model.subscriptionId == subId } 414 } 415 } 416 417 private fun createRepositoryForSubId(subId: Int): FullMobileConnectionRepository { 418 return fullMobileRepoFactory.build( 419 subId, 420 isCarrierMerged(subId), 421 subscriptionModelForSubId(subId), 422 defaultNetworkName, 423 networkNameSeparator, 424 ) 425 } 426 427 private fun updateRepos(newInfos: List<SubscriptionModel>) { 428 subIdRepositoryCache.forEach { (subId, repo) -> 429 repo.get()?.setIsCarrierMerged(isCarrierMerged(subId)) 430 } 431 } 432 433 private fun isCarrierMerged(subId: Int): Boolean { 434 return subId == carrierMergedSubId.value 435 } 436 437 /** 438 * True if the checked subId is in the list of current subs or the active mobile data subId 439 * 440 * @param checkedSubs the list to validate [subId] against. To invalidate the cache, pass in the 441 * new subscription list. Otherwise use [subscriptions.value] to validate a subId against the 442 * current known subscriptions 443 */ 444 private fun checkSub(subId: Int, checkedSubs: List<SubscriptionModel>): Boolean { 445 if (activeMobileDataSubscriptionId.value == subId) return true 446 447 checkedSubs.forEach { 448 if (it.subscriptionId == subId) { 449 return true 450 } 451 } 452 453 return false 454 } 455 456 private suspend fun fetchSubscriptionsList(): List<SubscriptionInfo> = 457 withContext(bgDispatcher) { subscriptionManager.completeActiveSubscriptionInfoList } 458 459 private fun SubscriptionInfo.toSubscriptionModel(): SubscriptionModel = 460 SubscriptionModel( 461 subscriptionId = subscriptionId, 462 isOpportunistic = isOpportunistic, 463 isExclusivelyNonTerrestrial = isOnlyNonTerrestrialNetwork, 464 groupUuid = groupUuid, 465 carrierName = carrierName.toString(), 466 profileClass = profileClass, 467 ) 468 469 override fun dump(pw: PrintWriter, args: Array<String>) { 470 val ipw = IndentingPrintWriter(pw, " ") 471 ipw.println("Connection cache:") 472 473 ipw.increaseIndent() 474 subIdRepositoryCache.entries.forEach { (subId, repo) -> 475 ipw.println("$subId: ${repo.get()}") 476 } 477 ipw.decreaseIndent() 478 479 ipw.println("Connections (${subIdRepositoryCache.size} total):") 480 ipw.increaseIndent() 481 subIdRepositoryCache.values.forEach { it.get()?.dump(ipw) } 482 ipw.decreaseIndent() 483 } 484 485 companion object { 486 private const val LOGGING_PREFIX = "Repo" 487 } 488 } 489