• 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.telephony.CarrierConfigManager
20 import android.telephony.SubscriptionManager
21 import com.android.settingslib.SignalIcon.MobileIconGroup
22 import com.android.settingslib.mobile.TelephonyIcons
23 import com.android.systemui.dagger.SysUISingleton
24 import com.android.systemui.dagger.qualifiers.Application
25 import com.android.systemui.log.table.TableLogBuffer
26 import com.android.systemui.log.table.logDiffsForTable
27 import com.android.systemui.statusbar.pipeline.dagger.MobileSummaryLog
28 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
29 import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
30 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
31 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
32 import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository
33 import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
34 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
35 import com.android.systemui.util.CarrierConfigTracker
36 import javax.inject.Inject
37 import kotlinx.coroutines.CoroutineScope
38 import kotlinx.coroutines.ExperimentalCoroutinesApi
39 import kotlinx.coroutines.delay
40 import kotlinx.coroutines.flow.Flow
41 import kotlinx.coroutines.flow.SharingStarted
42 import kotlinx.coroutines.flow.StateFlow
43 import kotlinx.coroutines.flow.combine
44 import kotlinx.coroutines.flow.distinctUntilChanged
45 import kotlinx.coroutines.flow.filter
46 import kotlinx.coroutines.flow.flatMapLatest
47 import kotlinx.coroutines.flow.flowOf
48 import kotlinx.coroutines.flow.map
49 import kotlinx.coroutines.flow.mapLatest
50 import kotlinx.coroutines.flow.stateIn
51 import kotlinx.coroutines.flow.transformLatest
52 
53 /**
54  * Business layer logic for the set of mobile subscription icons.
55  *
56  * This interactor represents known set of mobile subscriptions (represented by [SubscriptionInfo]).
57  * The list of subscriptions is filtered based on the opportunistic flags on the infos.
58  *
59  * It provides the default mapping between the telephony display info and the icon group that
60  * represents each RAT (LTE, 3G, etc.), as well as can produce an interactor for each individual
61  * icon
62  */
63 interface MobileIconsInteractor {
64     /** List of subscriptions, potentially filtered for CBRS */
65     val filteredSubscriptions: Flow<List<SubscriptionModel>>
66     /** True if the active mobile data subscription has data enabled */
67     val activeDataConnectionHasDataEnabled: StateFlow<Boolean>
68 
69     /** True if the RAT icon should always be displayed and false otherwise. */
70     val alwaysShowDataRatIcon: StateFlow<Boolean>
71 
72     /** True if the CDMA level should be preferred over the primary level. */
73     val alwaysUseCdmaLevel: StateFlow<Boolean>
74 
75     /** Tracks the subscriptionId set as the default for data connections */
76     val defaultDataSubId: StateFlow<Int>
77 
78     /**
79      * The connectivity of the default mobile network. Note that this can differ from what is
80      * reported from [MobileConnectionsRepository] in some cases. E.g., when the active subscription
81      * changes but the groupUuid remains the same, we keep the old validation information for 2
82      * seconds to avoid icon flickering.
83      */
84     val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel>
85 
86     /** The icon mapping from network type to [MobileIconGroup] for the default subscription */
87     val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>>
88     /** Fallback [MobileIconGroup] in the case where there is no icon in the mapping */
89     val defaultMobileIconGroup: StateFlow<MobileIconGroup>
90     /** True only if the default network is mobile, and validation also failed */
91     val isDefaultConnectionFailed: StateFlow<Boolean>
92     /** True once the user has been set up */
93     val isUserSetup: StateFlow<Boolean>
94 
95     /** True if we're configured to force-hide the mobile icons and false otherwise. */
96     val isForceHidden: Flow<Boolean>
97 
98     /**
99      * Vends out a [MobileIconInteractor] tracking the [MobileConnectionRepository] for the given
100      * subId. Will throw if the ID is invalid
101      */
102     fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor
103 }
104 
105 @Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
106 @OptIn(ExperimentalCoroutinesApi::class)
107 @SysUISingleton
108 class MobileIconsInteractorImpl
109 @Inject
110 constructor(
111     private val mobileConnectionsRepo: MobileConnectionsRepository,
112     private val carrierConfigTracker: CarrierConfigTracker,
113     @MobileSummaryLog private val tableLogger: TableLogBuffer,
114     connectivityRepository: ConnectivityRepository,
115     userSetupRepo: UserSetupRepository,
116     @Application private val scope: CoroutineScope,
117 ) : MobileIconsInteractor {
118     override val activeDataConnectionHasDataEnabled: StateFlow<Boolean> =
119         mobileConnectionsRepo.activeMobileDataRepository
<lambda>null120             .flatMapLatest { it?.dataEnabled ?: flowOf(false) }
121             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
122 
123     private val unfilteredSubscriptions: Flow<List<SubscriptionModel>> =
124         mobileConnectionsRepo.subscriptions
125 
126     /**
127      * Generally, SystemUI wants to show iconography for each subscription that is listed by
128      * [SubscriptionManager]. However, in the case of opportunistic subscriptions, we want to only
129      * show a single representation of the pair of subscriptions. The docs define opportunistic as:
130      *
131      * "A subscription is opportunistic (if) the network it connects to has limited coverage"
132      * https://developer.android.com/reference/android/telephony/SubscriptionManager#setOpportunistic(boolean,%20int)
133      *
134      * In the case of opportunistic networks (typically CBRS), we will filter out one of the
135      * subscriptions based on
136      * [CarrierConfigManager.KEY_ALWAYS_SHOW_PRIMARY_SIGNAL_BAR_IN_OPPORTUNISTIC_NETWORK_BOOLEAN],
137      * and by checking which subscription is opportunistic, or which one is active.
138      */
139     override val filteredSubscriptions: Flow<List<SubscriptionModel>> =
140         combine(
141                 unfilteredSubscriptions,
142                 mobileConnectionsRepo.activeMobileDataSubscriptionId,
activeIdnull143             ) { unfilteredSubs, activeId ->
144                 // Based on the old logic,
145                 if (unfilteredSubs.size != 2) {
146                     return@combine unfilteredSubs
147                 }
148 
149                 val info1 = unfilteredSubs[0]
150                 val info2 = unfilteredSubs[1]
151 
152                 // Filtering only applies to subscriptions in the same group
153                 if (info1.groupUuid == null || info1.groupUuid != info2.groupUuid) {
154                     return@combine unfilteredSubs
155                 }
156 
157                 // If both subscriptions are primary, show both
158                 if (!info1.isOpportunistic && !info2.isOpportunistic) {
159                     return@combine unfilteredSubs
160                 }
161 
162                 // NOTE: at this point, we are now returning a single SubscriptionInfo
163 
164                 // If carrier required, always show the icon of the primary subscription.
165                 // Otherwise, show whichever subscription is currently active for internet.
166                 if (carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) {
167                     // return the non-opportunistic info
168                     return@combine if (info1.isOpportunistic) listOf(info2) else listOf(info1)
169                 } else {
170                     return@combine if (info1.subscriptionId == activeId) {
171                         listOf(info1)
172                     } else {
173                         listOf(info2)
174                     }
175                 }
176             }
177             .distinctUntilChanged()
178             .logDiffsForTable(
179                 tableLogger,
180                 LOGGING_PREFIX,
181                 columnName = "filteredSubscriptions",
182                 initialValue = listOf(),
183             )
184             .stateIn(scope, SharingStarted.WhileSubscribed(), listOf())
185 
186     override val defaultDataSubId = mobileConnectionsRepo.defaultDataSubId
187 
188     /**
189      * Copied from the old pipeline. We maintain a 2s period of time where we will keep the
190      * validated bit from the old active network (A) while data is changing to the new one (B).
191      *
192      * This condition only applies if
193      * 1. A and B are in the same subscription group (e.g. for CBRS data switching) and
194      * 2. A was validated before the switch
195      *
196      * The goal of this is to minimize the flickering in the UI of the cellular indicator
197      */
198     private val forcingCellularValidation =
199         mobileConnectionsRepo.activeSubChangedInGroupEvent
<lambda>null200             .filter { mobileConnectionsRepo.defaultMobileNetworkConnectivity.value.isValidated }
<lambda>null201             .transformLatest {
202                 emit(true)
203                 delay(2000)
204                 emit(false)
205             }
206             .logDiffsForTable(
207                 tableLogger,
208                 LOGGING_PREFIX,
209                 columnName = "forcingValidation",
210                 initialValue = false,
211             )
212             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
213 
214     override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> =
215         combine(
216                 mobileConnectionsRepo.defaultMobileNetworkConnectivity,
217                 forcingCellularValidation,
networkConnectivitynull218             ) { networkConnectivity, forceValidation ->
219                 return@combine if (forceValidation) {
220                     MobileConnectivityModel(
221                         isValidated = true,
222                         isConnected = networkConnectivity.isConnected
223                     )
224                 } else {
225                     networkConnectivity
226                 }
227             }
228             .distinctUntilChanged()
229             .logDiffsForTable(
230                 tableLogger,
231                 columnPrefix = "$LOGGING_PREFIX.defaultConnection",
232                 initialValue = mobileConnectionsRepo.defaultMobileNetworkConnectivity.value,
233             )
234             .stateIn(
235                 scope,
236                 SharingStarted.WhileSubscribed(),
237                 mobileConnectionsRepo.defaultMobileNetworkConnectivity.value
238             )
239 
240     /**
241      * Mapping from network type to [MobileIconGroup] using the config generated for the default
242      * subscription Id. This mapping is the same for every subscription.
243      */
244     override val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>> =
245         mobileConnectionsRepo.defaultMobileIconMapping.stateIn(
246             scope,
247             SharingStarted.WhileSubscribed(),
248             initialValue = mapOf()
249         )
250 
251     override val alwaysShowDataRatIcon: StateFlow<Boolean> =
252         mobileConnectionsRepo.defaultDataSubRatConfig
<lambda>null253             .mapLatest { it.alwaysShowDataRatIcon }
254             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
255 
256     override val alwaysUseCdmaLevel: StateFlow<Boolean> =
257         mobileConnectionsRepo.defaultDataSubRatConfig
<lambda>null258             .mapLatest { it.alwaysShowCdmaRssi }
259             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
260 
261     /** If there is no mapping in [defaultMobileIconMapping], then use this default icon group */
262     override val defaultMobileIconGroup: StateFlow<MobileIconGroup> =
263         mobileConnectionsRepo.defaultMobileIconGroup.stateIn(
264             scope,
265             SharingStarted.WhileSubscribed(),
266             initialValue = TelephonyIcons.G
267         )
268 
269     /**
270      * We want to show an error state when cellular has actually failed to validate, but not if some
271      * other transport type is active, because then we expect there not to be validation.
272      */
273     override val isDefaultConnectionFailed: StateFlow<Boolean> =
274         mobileConnectionsRepo.defaultMobileNetworkConnectivity
connectivityModelnull275             .mapLatest { connectivityModel ->
276                 if (!connectivityModel.isConnected) {
277                     false
278                 } else {
279                     !connectivityModel.isValidated
280                 }
281             }
282             .logDiffsForTable(
283                 tableLogger,
284                 LOGGING_PREFIX,
285                 columnName = "isDefaultConnectionFailed",
286                 initialValue = false,
287             )
288             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
289 
290     override val isUserSetup: StateFlow<Boolean> = userSetupRepo.isUserSetupFlow
291 
292     override val isForceHidden: Flow<Boolean> =
293         connectivityRepository.forceHiddenSlots
<lambda>null294             .map { it.contains(ConnectivitySlot.MOBILE) }
295             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
296 
297     /** Vends out new [MobileIconInteractor] for a particular subId */
createMobileConnectionInteractorForSubIdnull298     override fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor =
299         MobileIconInteractorImpl(
300             scope,
301             activeDataConnectionHasDataEnabled,
302             alwaysShowDataRatIcon,
303             alwaysUseCdmaLevel,
304             defaultMobileNetworkConnectivity,
305             defaultMobileIconMapping,
306             defaultMobileIconGroup,
307             defaultDataSubId,
308             isDefaultConnectionFailed,
309             isForceHidden,
310             mobileConnectionsRepo.getRepoForSubId(subId),
311         )
312 
313     companion object {
314         private const val LOGGING_PREFIX = "Intr"
315     }
316 }
317