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