• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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