• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2024 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.systemui.Flags
25 import com.android.systemui.KairosActivatable
26 import com.android.systemui.KairosBuilder
27 import com.android.systemui.activated
28 import com.android.systemui.dagger.SysUISingleton
29 import com.android.systemui.flags.FeatureFlagsClassic
30 import com.android.systemui.flags.Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS
31 import com.android.systemui.kairos.BuildScope
32 import com.android.systemui.kairos.ExperimentalKairosApi
33 import com.android.systemui.kairos.Incremental
34 import com.android.systemui.kairos.State
35 import com.android.systemui.kairos.asyncEvent
36 import com.android.systemui.kairos.buildSpec
37 import com.android.systemui.kairos.combine
38 import com.android.systemui.kairos.filter
39 import com.android.systemui.kairos.flatMap
40 import com.android.systemui.kairos.flatten
41 import com.android.systemui.kairos.map
42 import com.android.systemui.kairos.mapValues
43 import com.android.systemui.kairos.stateOf
44 import com.android.systemui.kairosBuilder
45 import com.android.systemui.log.table.TableLogBuffer
46 import com.android.systemui.log.table.logDiffsForTable
47 import com.android.systemui.statusbar.core.NewStatusBarIcons
48 import com.android.systemui.statusbar.core.StatusBarRootModernization
49 import com.android.systemui.statusbar.pipeline.dagger.MobileSummaryLog
50 import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
51 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepositoryKairos
52 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepositoryKairos
53 import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel
54 import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
55 import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
56 import com.android.systemui.statusbar.policy.data.repository.UserSetupRepository
57 import com.android.systemui.util.CarrierConfigTracker
58 import dagger.Binds
59 import dagger.Provides
60 import dagger.multibindings.ElementsIntoSet
61 import javax.inject.Inject
62 import javax.inject.Provider
63 import kotlin.time.Duration.Companion.seconds
64 import kotlinx.coroutines.delay
65 
66 /**
67  * Business layer logic for the set of mobile subscription icons.
68  *
69  * This interactor represents known set of mobile subscriptions (represented by [SubscriptionInfo]).
70  * The list of subscriptions is filtered based on the opportunistic flags on the infos.
71  *
72  * It provides the default mapping between the telephony display info and the icon group that
73  * represents each RAT (LTE, 3G, etc.), as well as can produce an interactor for each individual
74  * icon
75  */
76 @ExperimentalKairosApi
77 interface MobileIconsInteractorKairos {
78     /** See [MobileConnectionsRepository.mobileIsDefault]. */
79     val mobileIsDefault: State<Boolean>
80 
81     /** List of subscriptions, potentially filtered for CBRS */
82     val filteredSubscriptions: State<List<SubscriptionModel>>
83 
84     /**
85      * The current list of [MobileIconInteractor]s associated with the current list of
86      * [filteredSubscriptions]
87      */
88     val icons: Incremental<Int, MobileIconInteractorKairos>
89 
90     /** Whether the mobile icons can be stacked vertically. */
91     val isStackable: State<Boolean>
92 
93     /** True if the active mobile data subscription has data enabled */
94     val activeDataConnectionHasDataEnabled: State<Boolean>
95 
96     /**
97      * Flow providing a reference to the Interactor for the active data subId. This represents the
98      * [MobileIconInteractorKairos] responsible for the active data connection, if any.
99      */
100     val activeDataIconInteractor: State<MobileIconInteractorKairos?>
101 
102     /** True if the RAT icon should always be displayed and false otherwise. */
103     val alwaysShowDataRatIcon: State<Boolean>
104 
105     /** True if the CDMA level should be preferred over the primary level. */
106     val alwaysUseCdmaLevel: State<Boolean>
107 
108     /** True if there is only one active subscription. */
109     val isSingleCarrier: State<Boolean>
110 
111     /** The icon mapping from network type to [MobileIconGroup] for the default subscription */
112     val defaultMobileIconMapping: State<Map<String, MobileIconGroup>>
113 
114     /** Fallback [MobileIconGroup] in the case where there is no icon in the mapping */
115     val defaultMobileIconGroup: State<MobileIconGroup>
116 
117     /** True only if the default network is mobile, and validation also failed */
118     val isDefaultConnectionFailed: State<Boolean>
119 
120     /** True once the user has been set up */
121     val isUserSetUp: State<Boolean>
122 
123     /** True if we're configured to force-hide the mobile icons and false otherwise. */
124     val isForceHidden: State<Boolean>
125 
126     /**
127      * True if the device-level service state (with -1 subscription id) reports emergency calls
128      * only. This value is only useful when there are no other subscriptions OR all existing
129      * subscriptions report that they are not in service.
130      */
131     val isDeviceInEmergencyCallsOnlyMode: State<Boolean>
132 }
133 
134 @ExperimentalKairosApi
135 @SysUISingleton
136 class MobileIconsInteractorKairosImpl
137 @Inject
138 constructor(
139     private val mobileConnectionsRepo: MobileConnectionsRepositoryKairos,
140     private val carrierConfigTracker: CarrierConfigTracker,
141     @MobileSummaryLog private val tableLogger: TableLogBuffer,
142     connectivityRepository: ConnectivityRepository,
143     userSetupRepo: UserSetupRepository,
144     private val context: Context,
145     private val featureFlagsClassic: FeatureFlagsClassic,
146 ) : MobileIconsInteractorKairos, KairosBuilder by kairosBuilder() {
147 
148     override val mobileIsDefault: State<Boolean> =
149         combine(
150                 mobileConnectionsRepo.mobileIsDefault,
151                 mobileConnectionsRepo.hasCarrierMergedConnection,
hasCarrierMergedConnectionnull152             ) { mobileIsDefault, hasCarrierMergedConnection ->
153                 // Because carrier merged networks are displayed as mobile networks, they're part of
154                 // the `isDefault` calculation. See b/272586234.
155                 mobileIsDefault || hasCarrierMergedConnection
156             }
<lambda>null157             .also {
158                 onActivated {
159                     logDiffsForTable(
160                         it,
161                         tableLogger,
162                         LOGGING_PREFIX,
163                         columnName = "mobileIsDefault",
164                     )
165                 }
166             }
167 
168     override val activeDataConnectionHasDataEnabled: State<Boolean> =
<lambda>null169         mobileConnectionsRepo.activeMobileDataRepository.flatMap {
170             it?.dataEnabled ?: stateOf(false)
171         }
172 
173     private val unfilteredSubscriptions: State<Collection<SubscriptionModel>>
174         get() = mobileConnectionsRepo.subscriptions
175 
176     /** Any filtering that we can do based purely on the info of each subscription individually. */
177     private val subscriptionsBasedFilteredSubs: State<List<SubscriptionModel>> =
<lambda>null178         unfilteredSubscriptions.map {
179             it.asSequence().filterBasedOnProvisioning().filterBasedOnNtn().toList()
180         }
181 
filterBasedOnProvisioningnull182     private fun Sequence<SubscriptionModel>.filterBasedOnProvisioning() =
183         if (!featureFlagsClassic.isEnabled(FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS)) {
184             this
185         } else {
<lambda>null186             filter { it.profileClass != PROFILE_CLASS_PROVISIONING }
187         }
188 
189     /**
190      * Subscriptions that exclusively support non-terrestrial networks should **never** directly
191      * show any iconography in the status bar. These subscriptions only exist to provide a backing
192      * for the device-based satellite connections, and the iconography for those connections are
193      * already being handled in
194      * [com.android.systemui.statusbar.pipeline.satellite.data.DeviceBasedSatelliteRepository]. We
195      * need to filter out those subscriptions here so we guarantee the subscription never turns into
196      * an icon. See b/336881301.
197      */
Sequencenull198     private fun Sequence<SubscriptionModel>.filterBasedOnNtn(): Sequence<SubscriptionModel> =
199         filter {
200             !it.isExclusivelyNonTerrestrial
201         }
202 
203     /**
204      * Generally, SystemUI wants to show iconography for each subscription that is listed by
205      * [SubscriptionManager]. However, in the case of opportunistic subscriptions, we want to only
206      * show a single representation of the pair of subscriptions. The docs define opportunistic as:
207      *
208      * "A subscription is opportunistic (if) the network it connects to has limited coverage"
209      * https://developer.android.com/reference/android/telephony/SubscriptionManager#setOpportunistic(boolean,%20int)
210      *
211      * In the case of opportunistic networks (typically CBRS), we will filter out one of the
212      * subscriptions based on
213      * [CarrierConfigManager.KEY_ALWAYS_SHOW_PRIMARY_SIGNAL_BAR_IN_OPPORTUNISTIC_NETWORK_BOOLEAN],
214      * and by checking which subscription is opportunistic, or which one is active.
215      */
<lambda>null216     override val filteredSubscriptions: State<List<SubscriptionModel>> = buildState {
217         combine(
218                 subscriptionsBasedFilteredSubs,
219                 mobileConnectionsRepo.activeMobileDataSubscriptionId,
220                 connectivityRepository.vcnSubId.toState(),
221             ) { preFilteredSubs, activeId, vcnSubId ->
222                 filterSubsBasedOnOpportunistic(preFilteredSubs, activeId, vcnSubId)
223             }
224             .also {
225                 logDiffsForTable(
226                     it,
227                     tableLogger,
228                     LOGGING_PREFIX,
229                     columnName = "filteredSubscriptions",
230                 )
231             }
232     }
233 
filterSubsBasedOnOpportunisticnull234     private fun filterSubsBasedOnOpportunistic(
235         subList: List<SubscriptionModel>,
236         activeId: Int?,
237         vcnSubId: Int?,
238     ): List<SubscriptionModel> {
239         // Based on the old logic,
240         if (subList.size != 2) {
241             return subList
242         }
243 
244         val info1 = subList[0]
245         val info2 = subList[1]
246 
247         // Filtering only applies to subscriptions in the same group
248         if (info1.groupUuid == null || info1.groupUuid != info2.groupUuid) {
249             return subList
250         }
251 
252         // If both subscriptions are primary, show both
253         if (!info1.isOpportunistic && !info2.isOpportunistic) {
254             return subList
255         }
256 
257         // NOTE: at this point, we are now returning a single SubscriptionInfo
258 
259         // If carrier required, always show the icon of the primary subscription.
260         // Otherwise, show whichever subscription is currently active for internet.
261         if (carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault) {
262             // return the non-opportunistic info
263             return if (info1.isOpportunistic) listOf(info2) else listOf(info1)
264         } else {
265             // It's possible for the subId of the VCN to disagree with the active subId in
266             // cases where the system has tried to switch but found no connection. In these
267             // scenarios, VCN will always have the subId that we want to use, so use that
268             // value instead of the activeId reported by telephony
269             val subIdToKeep = vcnSubId ?: activeId
270 
271             return if (info1.subscriptionId == subIdToKeep) {
272                 listOf(info1)
273             } else {
274                 listOf(info2)
275             }
276         }
277     }
278 
<lambda>null279     override val icons: Incremental<Int, MobileIconInteractorKairos> = buildIncremental {
280         val filteredSubIds =
281             filteredSubscriptions.map { it.asSequence().map { sub -> sub.subscriptionId }.toSet() }
282         mobileConnectionsRepo.mobileConnectionsBySubId
283             .filterIncrementally { (subId, _) ->
284                 // Filter out repo if subId is not present in the filtered set
285                 filteredSubIds.map { subId in it }
286             }
287             // Just map the repos to interactors
288             .mapValues { (subId, repo) -> buildSpec { mobileConnection(repo) } }
289             .applyLatestSpecForKey()
290     }
291 
292     override val isStackable: State<Boolean> =
293         if (NewStatusBarIcons.isEnabled && StatusBarRootModernization.isEnabled) {
iconsBySubIdnull294             icons.flatMap { iconsBySubId: Map<Int, MobileIconInteractorKairos> ->
295                 iconsBySubId.values
296                     .map { it.signalLevelIcon }
297                     .combine { signalLevelIcons ->
298                         // These are only stackable if:
299                         // - They are cellular
300                         // - There's exactly two
301                         // - They have the same number of levels
302                         signalLevelIcons.filterIsInstance<SignalIconModel.Cellular>().let {
303                             it.size == 2 && it[0].numberOfLevels == it[1].numberOfLevels
304                         }
305                     }
306             }
307         } else {
308             stateOf(false)
309         }
310 
311     override val activeDataIconInteractor: State<MobileIconInteractorKairos?> =
activeSubIdnull312         combine(mobileConnectionsRepo.activeMobileDataSubscriptionId, icons) { activeSubId, icons ->
313             activeSubId?.let { icons[activeSubId] }
314         }
315 
316     /**
317      * Copied from the old pipeline. We maintain a 2s period of time where we will keep the
318      * validated bit from the old active network (A) while data is changing to the new one (B).
319      *
320      * This condition only applies if
321      * 1. A and B are in the same subscription group (e.g. for CBRS data switching) and
322      * 2. A was validated before the switch
323      *
324      * The goal of this is to minimize the flickering in the UI of the cellular indicator
325      */
<lambda>null326     private val forcingCellularValidation: State<Boolean> = buildState {
327         mobileConnectionsRepo.activeSubChangedInGroupEvent
328             .filter(mobileConnectionsRepo.defaultConnectionIsValidated)
329             .mapLatestBuild {
330                 asyncEvent {
331                         delay(2.seconds)
332                         false
333                     }
334                     .holdState(true)
335             }
336             .holdState(stateOf(false))
337             .flatten()
338             .also {
339                 logDiffsForTable(it, tableLogger, LOGGING_PREFIX, columnName = "forcingValidation")
340             }
341     }
342 
343     /**
344      * Mapping from network type to [MobileIconGroup] using the config generated for the default
345      * subscription Id. This mapping is the same for every subscription.
346      */
347     override val defaultMobileIconMapping: State<Map<String, MobileIconGroup>>
348         get() = mobileConnectionsRepo.defaultMobileIconMapping
349 
350     override val alwaysShowDataRatIcon: State<Boolean> =
<lambda>null351         mobileConnectionsRepo.defaultDataSubRatConfig.map { it.alwaysShowDataRatIcon }
352 
353     override val alwaysUseCdmaLevel: State<Boolean> =
<lambda>null354         mobileConnectionsRepo.defaultDataSubRatConfig.map { it.alwaysShowCdmaRssi }
355 
356     override val isSingleCarrier: State<Boolean> =
357         mobileConnectionsRepo.subscriptions
<lambda>null358             .map { it.size == 1 }
<lambda>null359             .also {
360                 onActivated {
361                     logDiffsForTable(
362                         it,
363                         tableLogger,
364                         columnPrefix = LOGGING_PREFIX,
365                         columnName = "isSingleCarrier",
366                     )
367                 }
368             }
369 
370     /** If there is no mapping in [defaultMobileIconMapping], then use this default icon group */
371     override val defaultMobileIconGroup: State<MobileIconGroup>
372         get() = mobileConnectionsRepo.defaultMobileIconGroup
373 
374     /**
375      * We want to show an error state when cellular has actually failed to validate, but not if some
376      * other transport type is active, because then we expect there not to be validation.
377      */
378     override val isDefaultConnectionFailed: State<Boolean> =
379         combine(
380                 mobileIsDefault,
381                 mobileConnectionsRepo.defaultConnectionIsValidated,
382                 forcingCellularValidation,
forcingCellularValidationnull383             ) { mobileIsDefault, defaultConnectionIsValidated, forcingCellularValidation ->
384                 when {
385                     !mobileIsDefault -> false
386                     forcingCellularValidation -> false
387                     else -> !defaultConnectionIsValidated
388                 }
389             }
<lambda>null390             .also {
391                 onActivated {
392                     logDiffsForTable(
393                         it,
394                         tableLogger,
395                         LOGGING_PREFIX,
396                         columnName = "isDefaultConnectionFailed",
397                     )
398                 }
399             }
400 
<lambda>null401     override val isUserSetUp: State<Boolean> = buildState { userSetupRepo.isUserSetUp.toState() }
402 
<lambda>null403     override val isForceHidden: State<Boolean> = buildState {
404         connectivityRepository.forceHiddenSlots.toState().map {
405             it.contains(ConnectivitySlot.MOBILE)
406         }
407     }
408 
409     override val isDeviceInEmergencyCallsOnlyMode: State<Boolean>
410         get() = mobileConnectionsRepo.isDeviceEmergencyCallCapable
411 
412     /** Vends out a new [MobileIconInteractorKairos] for a particular subId */
BuildScopenull413     private fun BuildScope.mobileConnection(
414         repo: MobileConnectionRepositoryKairos
415     ): MobileIconInteractorKairos = activated {
416         MobileIconInteractorKairosImpl(
417             activeDataConnectionHasDataEnabled,
418             alwaysShowDataRatIcon,
419             alwaysUseCdmaLevel,
420             isSingleCarrier,
421             mobileIsDefault,
422             defaultMobileIconMapping,
423             defaultMobileIconGroup,
424             isDefaultConnectionFailed,
425             isForceHidden,
426             repo,
427             context,
428         )
429     }
430 
431     companion object {
432         private const val LOGGING_PREFIX = "Intr"
433     }
434 
435     @dagger.Module
436     interface Module {
437 
bindImplnull438         @Binds fun bindImpl(impl: MobileIconsInteractorKairosImpl): MobileIconsInteractorKairos
439 
440         companion object {
441             @Provides
442             @ElementsIntoSet
443             fun kairosActivatable(
444                 impl: Provider<MobileIconsInteractorKairosImpl>
445             ): Set<@JvmSuppressWildcards KairosActivatable> =
446                 if (Flags.statusBarMobileIconKairos()) setOf(impl.get()) else emptySet()
447         }
448     }
449 }
450