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