Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit be25e8f1 authored by Evan Laird's avatar Evan Laird
Browse files

[SB refactor] Track the default subscription and its connection status

This CL adds the ability for `MobileIconsInteractor` to track which
`MobileConnectionRepository` represents the active data subscription
connection, and tracks its disabled status.

In the case of DSDS, for instance, data will not be enabled on the
stanby SIM, but we will only want to show the disconnected state (the
exclamation mark on the triangle) when the active mobile data
subscription has data turned off.

Also, added `ContentObserver`s for the `MOBILE_DATA` setting, which is
used to trigger telephony polling events. This mimics behavior from the
old `MobileSignalController` and help us to update the icon once the
data enabled state changes.

Test: atest MobileConnectionsRepositoryTest
MobileConnectionRepositoryTest MobileIconsInteractorTest
MobileIconInteractorTest MobileIconViewModelTest

Bug: 240492102

Change-Id: I6f614d615930dca1d0c69ef361ecc05ce394dbfc
parent 4046d6b3
Loading
Loading
Loading
Loading
+54 −2
Original line number Diff line number Diff line
@@ -16,11 +16,15 @@

package com.android.systemui.statusbar.pipeline.mobile.data.repository

import android.content.Context
import android.database.ContentObserver
import android.provider.Settings.Global
import android.telephony.CellSignalStrength
import android.telephony.CellSignalStrengthCdma
import android.telephony.ServiceState
import android.telephony.SignalStrength
import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager
import android.telephony.TelephonyCallback
import android.telephony.TelephonyDisplayInfo
import android.telephony.TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE
@@ -34,6 +38,7 @@ import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetwork
import com.android.systemui.statusbar.pipeline.mobile.data.model.toDataConnectionType
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
import com.android.systemui.util.settings.GlobalSettings
import java.lang.IllegalStateException
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
@@ -45,6 +50,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.stateIn

/**
@@ -66,13 +72,22 @@ interface MobileConnectionRepository {
    val subscriptionModelFlow: Flow<MobileSubscriptionModel>
    /** Observable tracking [TelephonyManager.isDataConnectionAllowed] */
    val dataEnabled: Flow<Boolean>
    /**
     * True if this connection represents the default subscription per
     * [SubscriptionManager.getDefaultDataSubscriptionId]
     */
    val isDefaultDataSubscription: Flow<Boolean>
}

@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@OptIn(ExperimentalCoroutinesApi::class)
class MobileConnectionRepositoryImpl(
    private val context: Context,
    private val subId: Int,
    private val telephonyManager: TelephonyManager,
    private val globalSettings: GlobalSettings,
    defaultDataSubId: Flow<Int>,
    globalMobileDataSettingChangedEvent: Flow<Unit>,
    bgDispatcher: CoroutineDispatcher,
    logger: ConnectivityPipelineLogger,
    scope: CoroutineScope,
@@ -169,29 +184,66 @@ class MobileConnectionRepositoryImpl(
            .stateIn(scope, SharingStarted.WhileSubscribed(), state)
    }

    private val telephonyCallbackEvent = subscriptionModelFlow.map {}

    /** Produces whenever the mobile data setting changes for this subId */
    private val localMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow {
        val observer =
            object : ContentObserver(null) {
                override fun onChange(selfChange: Boolean) {
                    trySend(Unit)
                }
            }

        globalSettings.registerContentObserver(
            globalSettings.getUriFor("${Global.MOBILE_DATA}$subId"),
            /* notifyForDescendants */ true,
            observer
        )

        awaitClose { context.contentResolver.unregisterContentObserver(observer) }
    }

    /**
     * There are a few cases where we will need to poll [TelephonyManager] so we can update some
     * internal state where callbacks aren't provided. Any of those events should be merged into
     * this flow, which can be used to trigger the polling.
     */
    private val telephonyPollingEvent: Flow<Unit> = subscriptionModelFlow.map {}
    private val telephonyPollingEvent: Flow<Unit> =
        merge(
            telephonyCallbackEvent,
            localMobileDataSettingChangedEvent,
            globalMobileDataSettingChangedEvent,
        )

    override val dataEnabled: Flow<Boolean> = telephonyPollingEvent.map { dataConnectionAllowed() }

    private fun dataConnectionAllowed(): Boolean = telephonyManager.isDataConnectionAllowed

    override val isDefaultDataSubscription: Flow<Boolean> = defaultDataSubId.map { it == subId }

    class Factory
    @Inject
    constructor(
        private val context: Context,
        private val telephonyManager: TelephonyManager,
        private val logger: ConnectivityPipelineLogger,
        private val globalSettings: GlobalSettings,
        @Background private val bgDispatcher: CoroutineDispatcher,
        @Application private val scope: CoroutineScope,
    ) {
        fun build(subId: Int): MobileConnectionRepository {
        fun build(
            subId: Int,
            defaultDataSubId: Flow<Int>,
            globalMobileDataSettingChangedEvent: Flow<Unit>,
        ): MobileConnectionRepository {
            return MobileConnectionRepositoryImpl(
                context,
                subId,
                telephonyManager.createForSubscriptionId(subId),
                globalSettings,
                defaultDataSubId,
                globalMobileDataSettingChangedEvent,
                bgDispatcher,
                logger,
                scope,
+60 −12
Original line number Diff line number Diff line
@@ -18,13 +18,18 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository

import android.content.Context
import android.content.IntentFilter
import android.database.ContentObserver
import android.provider.Settings
import android.provider.Settings.Global.MOBILE_DATA
import android.telephony.CarrierConfigManager
import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager
import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
import android.telephony.TelephonyCallback
import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
import android.telephony.TelephonyManager
import androidx.annotation.VisibleForTesting
import com.android.internal.telephony.PhoneConstants
import com.android.settingslib.mobile.MobileMappings
import com.android.settingslib.mobile.MobileMappings.Config
import com.android.systemui.broadcast.BroadcastDispatcher
@@ -33,6 +38,7 @@ import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.util.settings.GlobalSettings
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
@@ -40,10 +46,12 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
@@ -62,8 +70,14 @@ interface MobileConnectionsRepository {
    /** Observable for [MobileMappings.Config] tracking the defaults */
    val defaultDataSubRatConfig: StateFlow<Config>

    /** Tracks [SubscriptionManager.getDefaultDataSubscriptionId] */
    val defaultDataSubId: StateFlow<Int>

    /** Get or create a repository for the line of service for the given subscription ID */
    fun getRepoForSubId(subId: Int): MobileConnectionRepository

    /** Observe changes to the [Settings.Global.MOBILE_DATA] setting */
    val globalMobileDataSettingChangedEvent: Flow<Unit>
}

@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@@ -76,6 +90,7 @@ constructor(
    private val telephonyManager: TelephonyManager,
    private val logger: ConnectivityPipelineLogger,
    broadcastDispatcher: BroadcastDispatcher,
    private val globalSettings: GlobalSettings,
    private val context: Context,
    @Background private val bgDispatcher: CoroutineDispatcher,
    @Application private val scope: CoroutineScope,
@@ -121,15 +136,24 @@ constructor(
                telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
                awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
            }
            .stateIn(
                scope,
                started = SharingStarted.WhileSubscribed(),
                SubscriptionManager.INVALID_SUBSCRIPTION_ID
            )
            .stateIn(scope, started = SharingStarted.WhileSubscribed(), INVALID_SUBSCRIPTION_ID)

    private val defaultDataSubChangedEvent =
        broadcastDispatcher.broadcastFlow(
    private val defaultDataSubIdChangeEvent: MutableSharedFlow<Unit> =
        MutableSharedFlow(extraBufferCapacity = 1)

    override val defaultDataSubId: StateFlow<Int> =
        broadcastDispatcher
            .broadcastFlow(
                IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
            ) { intent, _ ->
                intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID)
            }
            .distinctUntilChanged()
            .onEach { defaultDataSubIdChangeEvent.tryEmit(Unit) }
            .stateIn(
                scope,
                SharingStarted.WhileSubscribed(),
                SubscriptionManager.getDefaultDataSubscriptionId()
            )

    private val carrierConfigChangedEvent =
@@ -148,9 +172,8 @@ constructor(
     * This flow will produce whenever the default data subscription or the carrier config changes.
     */
    override val defaultDataSubRatConfig: StateFlow<Config> =
        combine(defaultDataSubChangedEvent, carrierConfigChangedEvent) { _, _ ->
                Config.readConfig(context)
            }
        merge(defaultDataSubIdChangeEvent, carrierConfigChangedEvent)
            .mapLatest { Config.readConfig(context) }
            .stateIn(
                scope,
                SharingStarted.WhileSubscribed(),
@@ -168,6 +191,27 @@ constructor(
            ?: createRepositoryForSubId(subId).also { subIdRepositoryCache[subId] = it }
    }

    /**
     * In single-SIM devices, the [MOBILE_DATA] setting is phone-wide. For multi-SIM, the individual
     * connection repositories also observe the URI for [MOBILE_DATA] + subId.
     */
    override val globalMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow {
        val observer =
            object : ContentObserver(null) {
                override fun onChange(selfChange: Boolean) {
                    trySend(Unit)
                }
            }

        globalSettings.registerContentObserver(
            globalSettings.getUriFor(MOBILE_DATA),
            true,
            observer
        )

        awaitClose { context.contentResolver.unregisterContentObserver(observer) }
    }

    private fun isValidSubId(subId: Int): Boolean {
        subscriptionsFlow.value.forEach {
            if (it.subscriptionId == subId) {
@@ -181,7 +225,11 @@ constructor(
    @VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache

    private fun createRepositoryForSubId(subId: Int): MobileConnectionRepository {
        return mobileConnectionRepositoryFactory.build(subId)
        return mobileConnectionRepositoryFactory.build(
            subId,
            defaultDataSubId,
            globalMobileDataSettingChangedEvent,
        )
    }

    private fun dropUnusedReposFromCache(newInfos: List<SubscriptionInfo>) {
+7 −7
Original line number Diff line number Diff line
@@ -29,6 +29,10 @@ import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map

interface MobileIconInteractor {
    // TODO(b/256839546): clarify naming of default vs active
    /** True if we want to consider the data connection enabled */
    val isDefaultDataEnabled: Flow<Boolean>

    /** Observable for the data enabled state of this connection */
    val isDataEnabled: Flow<Boolean>

@@ -43,13 +47,11 @@ interface MobileIconInteractor {

    /** Based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL], either 4 or 5 */
    val numberOfLevels: Flow<Int>

    /** True when we want to draw an icon that makes room for the exclamation mark */
    val cutOut: Flow<Boolean>
}

/** Interactor for a single mobile connection. This connection _should_ have one subscription ID */
class MobileIconInteractorImpl(
    defaultSubscriptionHasDataEnabled: Flow<Boolean>,
    defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>>,
    defaultMobileIconGroup: Flow<MobileIconGroup>,
    mobileMappingsProxy: MobileMappingsProxy,
@@ -59,6 +61,8 @@ class MobileIconInteractorImpl(

    override val isDataEnabled: Flow<Boolean> = connectionRepository.dataEnabled

    override val isDefaultDataEnabled = defaultSubscriptionHasDataEnabled

    /** Observable for the current RAT indicator icon ([MobileIconGroup]) */
    override val networkTypeIconGroup: Flow<MobileIconGroup> =
        combine(
@@ -91,8 +95,4 @@ class MobileIconInteractorImpl(
     * once it's wired up inside of [CarrierConfigTracker]
     */
    override val numberOfLevels: Flow<Int> = flowOf(4)

    /** Whether or not to draw the mobile triangle as "cut out", i.e., with the exclamation mark */
    // TODO: find a better name for this?
    override val cutOut: Flow<Boolean> = flowOf(false)
}
+18 −0
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
import android.telephony.CarrierConfigManager
import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager
import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.settingslib.mobile.TelephonyIcons
import com.android.systemui.dagger.SysUISingleton
@@ -35,6 +36,8 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn

@@ -51,6 +54,8 @@ import kotlinx.coroutines.flow.stateIn
interface MobileIconsInteractor {
    /** List of subscriptions, potentially filtered for CBRS */
    val filteredSubscriptions: Flow<List<SubscriptionInfo>>
    /** True if the active mobile data subscription has data enabled */
    val activeDataConnectionHasDataEnabled: Flow<Boolean>
    /** The icon mapping from network type to [MobileIconGroup] for the default subscription */
    val defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>>
    /** Fallback [MobileIconGroup] in the case where there is no icon in the mapping */
@@ -79,6 +84,18 @@ constructor(
    private val activeMobileDataSubscriptionId =
        mobileConnectionsRepo.activeMobileDataSubscriptionId

    private val activeMobileDataConnectionRepo: Flow<MobileConnectionRepository?> =
        activeMobileDataSubscriptionId.map { activeId ->
            if (activeId == INVALID_SUBSCRIPTION_ID) {
                null
            } else {
                mobileConnectionsRepo.getRepoForSubId(activeId)
            }
        }

    override val activeDataConnectionHasDataEnabled: Flow<Boolean> =
        activeMobileDataConnectionRepo.flatMapLatest { it?.dataEnabled ?: flowOf(false) }

    private val unfilteredSubscriptions: Flow<List<SubscriptionInfo>> =
        mobileConnectionsRepo.subscriptionsFlow

@@ -146,6 +163,7 @@ constructor(
    /** Vends out new [MobileIconInteractor] for a particular subId */
    override fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor =
        MobileIconInteractorImpl(
            activeDataConnectionHasDataEnabled,
            defaultMobileIconMapping,
            defaultMobileIconGroup,
            mobileMappingsProxy,
+14 −6
Original line number Diff line number Diff line
@@ -24,10 +24,12 @@ import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIc
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.mapLatest

/**
 * View model for the state of a single mobile icon. Each [MobileIconViewModel] will keep watch over
@@ -39,25 +41,31 @@ import kotlinx.coroutines.flow.flowOf
 *
 * TODO: figure out where carrier merged and VCN models go (probably here?)
 */
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@OptIn(ExperimentalCoroutinesApi::class)
class MobileIconViewModel
constructor(
    val subscriptionId: Int,
    iconInteractor: MobileIconInteractor,
    logger: ConnectivityPipelineLogger,
) {
    /** Whether or not to show the error state of [SignalDrawable] */
    private val showExclamationMark: Flow<Boolean> =
        iconInteractor.isDefaultDataEnabled.mapLatest { !it }

    /** An int consumable by [SignalDrawable] for display */
    var iconId: Flow<Int> =
        combine(iconInteractor.level, iconInteractor.numberOfLevels, iconInteractor.cutOut) {
    val iconId: Flow<Int> =
        combine(iconInteractor.level, iconInteractor.numberOfLevels, showExclamationMark) {
                level,
                numberOfLevels,
                cutOut ->
                SignalDrawable.getState(level, numberOfLevels, cutOut)
                showExclamationMark ->
                SignalDrawable.getState(level, numberOfLevels, showExclamationMark)
            }
            .distinctUntilChanged()
            .logOutputChange(logger, "iconId($subscriptionId)")

    /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */
    var networkTypeIcon: Flow<Icon?> =
    val networkTypeIcon: Flow<Icon?> =
        combine(iconInteractor.networkTypeIconGroup, iconInteractor.isDataEnabled) {
            networkTypeIconGroup,
            isDataEnabled ->
@@ -72,5 +80,5 @@ constructor(
            }
        }

    var tint: Flow<Int> = flowOf(Color.CYAN)
    val tint: Flow<Int> = flowOf(Color.CYAN)
}
Loading