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