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