• 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.domain.interactor
18 
19 import android.content.Context
20 import android.telephony.CarrierConfigManager
21 import android.telephony.SubscriptionManager
22 import android.telephony.SubscriptionManager.PROFILE_CLASS_PROVISIONING
23 import com.android.settingslib.SignalIcon.MobileIconGroup
24 import com.android.settingslib.mobile.TelephonyIcons
25 import com.android.systemui.dagger.SysUISingleton
26 import com.android.systemui.dagger.qualifiers.Background
27 import com.android.systemui.flags.FeatureFlagsClassic
28 import com.android.systemui.flags.Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS
29 import com.android.systemui.log.table.TableLogBuffer
30 import com.android.systemui.log.table.logDiffsForTable
31 import com.android.systemui.statusbar.core.NewStatusBarIcons
32 import com.android.systemui.statusbar.core.StatusBarRootModernization
33 import com.android.systemui.statusbar.pipeline.dagger.MobileSummaryLog
34 import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
35 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
36 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
37 import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
38 import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
39 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
40 import com.android.systemui.statusbar.policy.data.repository.UserSetupRepository
41 import com.android.systemui.util.CarrierConfigTracker
42 import java.lang.ref.WeakReference
43 import javax.inject.Inject
44 import kotlinx.coroutines.CoroutineScope
45 import kotlinx.coroutines.ExperimentalCoroutinesApi
46 import kotlinx.coroutines.delay
47 import kotlinx.coroutines.flow.Flow
48 import kotlinx.coroutines.flow.SharingStarted
49 import kotlinx.coroutines.flow.StateFlow
50 import kotlinx.coroutines.flow.combine
51 import kotlinx.coroutines.flow.distinctUntilChanged
52 import kotlinx.coroutines.flow.filter
53 import kotlinx.coroutines.flow.flatMapLatest
54 import kotlinx.coroutines.flow.flowOf
55 import kotlinx.coroutines.flow.map
56 import kotlinx.coroutines.flow.mapLatest
57 import kotlinx.coroutines.flow.stateIn
58 import kotlinx.coroutines.flow.transformLatest
59 
60 /**
61  * Business layer logic for the set of mobile subscription icons.
62  *
63  * This interactor represents known set of mobile subscriptions (represented by [SubscriptionInfo]).
64  * The list of subscriptions is filtered based on the opportunistic flags on the infos.
65  *
66  * It provides the default mapping between the telephony display info and the icon group that
67  * represents each RAT (LTE, 3G, etc.), as well as can produce an interactor for each individual
68  * icon
69  */
70 interface MobileIconsInteractor {
71     /** See [MobileConnectionsRepository.mobileIsDefault]. */
72     val mobileIsDefault: StateFlow<Boolean>
73 
74     /** List of subscriptions, potentially filtered for CBRS */
75     val filteredSubscriptions: Flow<List<SubscriptionModel>>
76 
77     /** Subscription ID of the current default data subscription */
78     val defaultDataSubId: Flow<Int?>
79 
80     /**
81      * The current list of [MobileIconInteractor]s associated with the current list of
82      * [filteredSubscriptions]
83      */
84     val icons: StateFlow<List<MobileIconInteractor>>
85 
86     /** Whether the mobile icons can be stacked vertically. */
87     val isStackable: Flow<Boolean>
88 
89     /**
90      * Observable for the subscriptionId of the current mobile data connection. Null if we don't
91      * have a valid subscription id
92      */
93     val activeMobileDataSubscriptionId: StateFlow<Int?>
94 
95     /** True if the active mobile data subscription has data enabled */
96     val activeDataConnectionHasDataEnabled: StateFlow<Boolean>
97 
98     /**
99      * Flow providing a reference to the Interactor for the active data subId. This represents the
100      * [MobileIconInteractor] responsible for the active data connection, if any.
101      */
102     val activeDataIconInteractor: StateFlow<MobileIconInteractor?>
103 
104     /** True if the RAT icon should always be displayed and false otherwise. */
105     val alwaysShowDataRatIcon: StateFlow<Boolean>
106 
107     /** True if the CDMA level should be preferred over the primary level. */
108     val alwaysUseCdmaLevel: StateFlow<Boolean>
109 
110     /** True if there is only one active subscription. */
111     val isSingleCarrier: StateFlow<Boolean>
112 
113     /** The icon mapping from network type to [MobileIconGroup] for the default subscription */
114     val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>>
115 
116     /** Fallback [MobileIconGroup] in the case where there is no icon in the mapping */
117     val defaultMobileIconGroup: StateFlow<MobileIconGroup>
118 
119     /** True only if the default network is mobile, and validation also failed */
120     val isDefaultConnectionFailed: StateFlow<Boolean>
121 
122     /** True once the user has been set up */
123     val isUserSetUp: StateFlow<Boolean>
124 
125     /** True if we're configured to force-hide the mobile icons and false otherwise. */
126     val isForceHidden: Flow<Boolean>
127 
128     /**
129      * True if the device-level service state (with -1 subscription id) reports emergency calls
130      * only. This value is only useful when there are no other subscriptions OR all existing
131      * subscriptions report that they are not in service.
132      */
133     val isDeviceInEmergencyCallsOnlyMode: Flow<Boolean>
134 
135     /**
136      * Vends out a [MobileIconInteractor] tracking the [MobileConnectionRepository] for the given
137      * subId.
138      */
139     fun getMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor
140 }
141 
142 @OptIn(ExperimentalCoroutinesApi::class)
143 @Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
144 @SysUISingleton
145 class MobileIconsInteractorImpl
146 @Inject
147 constructor(
148     private val mobileConnectionsRepo: MobileConnectionsRepository,
149     private val carrierConfigTracker: CarrierConfigTracker,
150     @MobileSummaryLog private val tableLogger: TableLogBuffer,
151     connectivityRepository: ConnectivityRepository,
152     userSetupRepo: UserSetupRepository,
153     @Background private val scope: CoroutineScope,
154     private val context: Context,
155     private val featureFlagsClassic: FeatureFlagsClassic,
156 ) : MobileIconsInteractor {
157 
158     // Weak reference lookup for created interactors
159     private val reuseCache = mutableMapOf<Int, WeakReference<MobileIconInteractor>>()
160 
161     override val mobileIsDefault =
162         combine(
163                 mobileConnectionsRepo.mobileIsDefault,
164                 mobileConnectionsRepo.hasCarrierMergedConnection,
hasCarrierMergedConnectionnull165             ) { mobileIsDefault, hasCarrierMergedConnection ->
166                 // Because carrier merged networks are displayed as mobile networks, they're part of
167                 // the `isDefault` calculation. See b/272586234.
168                 mobileIsDefault || hasCarrierMergedConnection
169             }
170             .logDiffsForTable(
171                 tableLogger,
172                 LOGGING_PREFIX,
173                 columnName = "mobileIsDefault",
174                 initialValue = false,
175             )
176             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
177 
178     override val activeMobileDataSubscriptionId: StateFlow<Int?> =
179         mobileConnectionsRepo.activeMobileDataSubscriptionId
180 
181     override val activeDataConnectionHasDataEnabled: StateFlow<Boolean> =
182         mobileConnectionsRepo.activeMobileDataRepository
<lambda>null183             .flatMapLatest { it?.dataEnabled ?: flowOf(false) }
184             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
185 
186     override val activeDataIconInteractor: StateFlow<MobileIconInteractor?> =
187         mobileConnectionsRepo.activeMobileDataSubscriptionId
<lambda>null188             .mapLatest {
189                 if (it != null) {
190                     getMobileConnectionInteractorForSubId(it)
191                 } else {
192                     null
193                 }
194             }
195             .stateIn(scope, SharingStarted.WhileSubscribed(), null)
196 
197     private val unfilteredSubscriptions: Flow<List<SubscriptionModel>> =
198         mobileConnectionsRepo.subscriptions
199 
200     /** Any filtering that we can do based purely on the info of each subscription individually. */
201     private val subscriptionsBasedFilteredSubs =
202         unfilteredSubscriptions
<lambda>null203             .map { it.filterBasedOnProvisioning().filterBasedOnNtn() }
204             .distinctUntilChanged()
205 
filterBasedOnProvisioningnull206     private fun List<SubscriptionModel>.filterBasedOnProvisioning(): List<SubscriptionModel> =
207         if (!featureFlagsClassic.isEnabled(FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS)) {
208             this
209         } else {
<lambda>null210             this.filter { it.profileClass != PROFILE_CLASS_PROVISIONING }
211         }
212 
213     /**
214      * Subscriptions that exclusively support non-terrestrial networks should **never** directly
215      * show any iconography in the status bar. These subscriptions only exist to provide a backing
216      * for the device-based satellite connections, and the iconography for those connections are
217      * already being handled in
218      * [com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository]. We
219      * need to filter out those subscriptions here so we guarantee the subscription never turns into
220      * an icon. See b/336881301.
221      */
Listnull222     private fun List<SubscriptionModel>.filterBasedOnNtn(): List<SubscriptionModel> {
223         return this.filter { !it.isExclusivelyNonTerrestrial }
224     }
225 
226     /**
227      * Generally, SystemUI wants to show iconography for each subscription that is listed by
228      * [SubscriptionManager]. However, in the case of opportunistic subscriptions, we want to only
229      * show a single representation of the pair of subscriptions. The docs define opportunistic as:
230      *
231      * "A subscription is opportunistic (if) the network it connects to has limited coverage"
232      * https://developer.android.com/reference/android/telephony/SubscriptionManager#setOpportunistic(boolean,%20int)
233      *
234      * In the case of opportunistic networks (typically CBRS), we will filter out one of the
235      * subscriptions based on
236      * [CarrierConfigManager.KEY_ALWAYS_SHOW_PRIMARY_SIGNAL_BAR_IN_OPPORTUNISTIC_NETWORK_BOOLEAN],
237      * and by checking which subscription is opportunistic, or which one is active.
238      */
239     override val filteredSubscriptions: Flow<List<SubscriptionModel>> =
240         combine(
241                 subscriptionsBasedFilteredSubs,
242                 mobileConnectionsRepo.activeMobileDataSubscriptionId,
243                 connectivityRepository.vcnSubId,
activeIdnull244             ) { preFilteredSubs, activeId, vcnSubId ->
245                 filterSubsBasedOnOpportunistic(preFilteredSubs, activeId, vcnSubId)
246             }
247             .distinctUntilChanged()
248             .logDiffsForTable(
249                 tableLogger,
250                 LOGGING_PREFIX,
251                 columnName = "filteredSubscriptions",
252                 initialValue = listOf(),
253             )
254             .stateIn(scope, SharingStarted.WhileSubscribed(), listOf())
255 
filterSubsBasedOnOpportunisticnull256     private fun filterSubsBasedOnOpportunistic(
257         subList: List<SubscriptionModel>,
258         activeId: Int?,
259         vcnSubId: Int?,
260     ): List<SubscriptionModel> {
261         // Based on the old logic,
262         if (subList.size != 2) {
263             return subList
264         }
265 
266         val info1 = subList[0]
267         val info2 = subList[1]
268 
269         // Filtering only applies to subscriptions in the same group
270         if (info1.groupUuid == null || info1.groupUuid != info2.groupUuid) {
271             return subList
272         }
273 
274         // If both subscriptions are primary, show both
275         if (!info1.isOpportunistic && !info2.isOpportunistic) {
276             return subList
277         }
278 
279         // NOTE: at this point, we are now returning a single SubscriptionInfo
280 
281         // If carrier required, always show the icon of the primary subscription.
282         // Otherwise, show whichever subscription is currently active for internet.
283         if (carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) {
284             // return the non-opportunistic info
285             return if (info1.isOpportunistic) listOf(info2) else listOf(info1)
286         } else {
287             // It's possible for the subId of the VCN to disagree with the active subId in
288             // cases where the system has tried to switch but found no connection. In these
289             // scenarios, VCN will always have the subId that we want to use, so use that
290             // value instead of the activeId reported by telephony
291             val subIdToKeep = vcnSubId ?: activeId
292 
293             return if (info1.subscriptionId == subIdToKeep) {
294                 listOf(info1)
295             } else {
296                 listOf(info2)
297             }
298         }
299     }
300 
301     override val defaultDataSubId = mobileConnectionsRepo.defaultDataSubId
302 
303     override val icons =
304         filteredSubscriptions
subsnull305             .mapLatest { subs ->
306                 subs.map { getMobileConnectionInteractorForSubId(it.subscriptionId) }
307             }
308             .stateIn(scope, SharingStarted.WhileSubscribed(), emptyList())
309 
310     override val isStackable =
311         if (NewStatusBarIcons.isEnabled && StatusBarRootModernization.isEnabled) {
iconsnull312             icons.flatMapLatest { icons ->
313                 combine(icons.map { it.signalLevelIcon }) { signalLevelIcons ->
314                     // These are only stackable if:
315                     // - They are cellular
316                     // - There's exactly two
317                     // - They have the same number of levels
318                     signalLevelIcons.filterIsInstance<SignalIconModel.Cellular>().let {
319                         it.size == 2 && it[0].numberOfLevels == it[1].numberOfLevels
320                     }
321                 }
322             }
323         } else {
324             flowOf(false)
325         }
326 
327     /**
328      * Copied from the old pipeline. We maintain a 2s period of time where we will keep the
329      * validated bit from the old active network (A) while data is changing to the new one (B).
330      *
331      * This condition only applies if
332      * 1. A and B are in the same subscription group (e.g. for CBRS data switching) and
333      * 2. A was validated before the switch
334      *
335      * The goal of this is to minimize the flickering in the UI of the cellular indicator
336      */
337     private val forcingCellularValidation =
338         mobileConnectionsRepo.activeSubChangedInGroupEvent
<lambda>null339             .filter { mobileConnectionsRepo.defaultConnectionIsValidated.value }
<lambda>null340             .transformLatest {
341                 emit(true)
342                 delay(2000)
343                 emit(false)
344             }
345             .logDiffsForTable(
346                 tableLogger,
347                 LOGGING_PREFIX,
348                 columnName = "forcingValidation",
349                 initialValue = false,
350             )
351             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
352 
353     /**
354      * Mapping from network type to [MobileIconGroup] using the config generated for the default
355      * subscription Id. This mapping is the same for every subscription.
356      */
357     override val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>> =
358         mobileConnectionsRepo.defaultMobileIconMapping.stateIn(
359             scope,
360             SharingStarted.WhileSubscribed(),
361             initialValue = mapOf(),
362         )
363 
364     override val alwaysShowDataRatIcon: StateFlow<Boolean> =
365         mobileConnectionsRepo.defaultDataSubRatConfig
<lambda>null366             .mapLatest { it.alwaysShowDataRatIcon }
367             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
368 
369     override val alwaysUseCdmaLevel: StateFlow<Boolean> =
370         mobileConnectionsRepo.defaultDataSubRatConfig
<lambda>null371             .mapLatest { it.alwaysShowCdmaRssi }
372             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
373 
374     override val isSingleCarrier: StateFlow<Boolean> =
375         mobileConnectionsRepo.subscriptions
<lambda>null376             .map { it.size == 1 }
377             .logDiffsForTable(
378                 tableLogger,
379                 columnPrefix = LOGGING_PREFIX,
380                 columnName = "isSingleCarrier",
381                 initialValue = false,
382             )
383             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
384 
385     /** If there is no mapping in [defaultMobileIconMapping], then use this default icon group */
386     override val defaultMobileIconGroup: StateFlow<MobileIconGroup> =
387         mobileConnectionsRepo.defaultMobileIconGroup.stateIn(
388             scope,
389             SharingStarted.WhileSubscribed(),
390             initialValue = TelephonyIcons.G,
391         )
392 
393     /**
394      * We want to show an error state when cellular has actually failed to validate, but not if some
395      * other transport type is active, because then we expect there not to be validation.
396      */
397     override val isDefaultConnectionFailed: StateFlow<Boolean> =
398         combine(
399                 mobileIsDefault,
400                 mobileConnectionsRepo.defaultConnectionIsValidated,
401                 forcingCellularValidation,
forcingCellularValidationnull402             ) { mobileIsDefault, defaultConnectionIsValidated, forcingCellularValidation ->
403                 when {
404                     !mobileIsDefault -> false
405                     forcingCellularValidation -> false
406                     else -> !defaultConnectionIsValidated
407                 }
408             }
409             .logDiffsForTable(
410                 tableLogger,
411                 LOGGING_PREFIX,
412                 columnName = "isDefaultConnectionFailed",
413                 initialValue = false,
414             )
415             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
416 
417     override val isUserSetUp: StateFlow<Boolean> = userSetupRepo.isUserSetUp
418 
419     override val isForceHidden: Flow<Boolean> =
420         connectivityRepository.forceHiddenSlots
<lambda>null421             .map { it.contains(ConnectivitySlot.MOBILE) }
422             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
423 
424     override val isDeviceInEmergencyCallsOnlyMode: Flow<Boolean> =
425         mobileConnectionsRepo.isDeviceEmergencyCallCapable
426 
427     /** Vends out new [MobileIconInteractor] for a particular subId */
getMobileConnectionInteractorForSubIdnull428     override fun getMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor =
429         reuseCache[subId]?.get() ?: createMobileConnectionInteractorForSubId(subId)
430 
431     private fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor =
432         MobileIconInteractorImpl(
433                 scope,
434                 activeDataConnectionHasDataEnabled,
435                 alwaysShowDataRatIcon,
436                 alwaysUseCdmaLevel,
437                 isSingleCarrier,
438                 mobileIsDefault,
439                 defaultMobileIconMapping,
440                 defaultMobileIconGroup,
441                 isDefaultConnectionFailed,
442                 isForceHidden,
443                 mobileConnectionsRepo.getRepoForSubId(subId),
444                 context,
445             )
446             .also { reuseCache[subId] = WeakReference(it) }
447 
448     companion object {
449         private const val LOGGING_PREFIX = "Intr"
450     }
451 }
452