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