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

Commit b91ae18c authored by Evan Laird's avatar Evan Laird Committed by Android (Google) Code Review
Browse files

Merge changes I37ab7767,I13890cdc,Idd5fa020,I6f614d61 into tm-qpr-dev

* changes:
  [SB refactor] Wire up DataConnectionState to the view model
  [SB refactor] Add connectivity tracking to the mobile pipeline
  [SB refactor] Upgrade most data sources to `StateFlow`
  [SB refactor] Track the default subscription and its connection status
parents e480be93 172b95c2
Loading
Loading
Loading
Loading
+27 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

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

import android.net.NetworkCapabilities

/** Provides information about a mobile network connection */
data class MobileConnectivityModel(
    /** Whether mobile is the connected transport see [NetworkCapabilities.TRANSPORT_CELLULAR] */
    val isConnected: Boolean = false,
    /** Whether the mobile transport is validated [NetworkCapabilities.NET_CAPABILITY_VALIDATED] */
    val isValidated: Boolean = false,
)
+66 −5
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
@@ -42,9 +47,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.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn

/**
@@ -65,14 +73,23 @@ interface MobileConnectionRepository {
     */
    val subscriptionModelFlow: Flow<MobileSubscriptionModel>
    /** Observable tracking [TelephonyManager.isDataConnectionAllowed] */
    val dataEnabled: Flow<Boolean>
    val dataEnabled: StateFlow<Boolean>
    /**
     * True if this connection represents the default subscription per
     * [SubscriptionManager.getDefaultDataSubscriptionId]
     */
    val isDefaultDataSubscription: StateFlow<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: StateFlow<Int>,
    globalMobileDataSettingChangedEvent: Flow<Unit>,
    bgDispatcher: CoroutineDispatcher,
    logger: ConnectivityPipelineLogger,
    scope: CoroutineScope,
@@ -86,6 +103,8 @@ class MobileConnectionRepositoryImpl(
        }
    }

    private val telephonyCallbackEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1)

    override val subscriptionModelFlow: StateFlow<MobileSubscriptionModel> = run {
        var state = MobileSubscriptionModel()
        conflatedCallbackFlow {
@@ -165,33 +184,75 @@ class MobileConnectionRepositoryImpl(
                telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
                awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
            }
            .onEach { telephonyCallbackEvent.tryEmit(Unit) }
            .logOutputChange(logger, "MobileSubscriptionModel")
            .stateIn(scope, SharingStarted.WhileSubscribed(), state)
    }

    /** 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() }
    override val dataEnabled: StateFlow<Boolean> =
        telephonyPollingEvent
            .mapLatest { dataConnectionAllowed() }
            .stateIn(scope, SharingStarted.WhileSubscribed(), dataConnectionAllowed())

    private fun dataConnectionAllowed(): Boolean = telephonyManager.isDataConnectionAllowed

    override val isDefaultDataSubscription: StateFlow<Boolean> =
        defaultDataSubId
            .mapLatest { it == subId }
            .stateIn(scope, SharingStarted.WhileSubscribed(), defaultDataSubId.value == 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: StateFlow<Int>,
            globalMobileDataSettingChangedEvent: Flow<Unit>,
        ): MobileConnectionRepository {
            return MobileConnectionRepositoryImpl(
                context,
                subId,
                telephonyManager.createForSubscriptionId(subId),
                globalSettings,
                defaultDataSubId,
                globalMobileDataSettingChangedEvent,
                bgDispatcher,
                logger,
                scope,
+103 −13
Original line number Diff line number Diff line
@@ -16,15 +16,27 @@

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

import android.annotation.SuppressLint
import android.content.Context
import android.content.IntentFilter
import android.database.ContentObserver
import android.net.ConnectivityManager
import android.net.ConnectivityManager.NetworkCallback
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
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
@@ -32,7 +44,9 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall
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.mobile.data.model.MobileConnectivityModel
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 +54,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
@@ -57,13 +73,22 @@ interface MobileConnectionsRepository {
    val subscriptionsFlow: Flow<List<SubscriptionInfo>>

    /** Observable for the subscriptionId of the current mobile data connection */
    val activeMobileDataSubscriptionId: Flow<Int>
    val activeMobileDataSubscriptionId: StateFlow<Int>

    /** Observable for [MobileMappings.Config] tracking the defaults */
    val defaultDataSubRatConfig: StateFlow<Config>

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

    /** The current connectivity status for the default mobile network connection */
    val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel>

    /** 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")
@@ -72,10 +97,12 @@ interface MobileConnectionsRepository {
class MobileConnectionsRepositoryImpl
@Inject
constructor(
    private val connectivityManager: ConnectivityManager,
    private val subscriptionManager: SubscriptionManager,
    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 +148,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 +184,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 +203,57 @@ 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) }
    }

    @SuppressLint("MissingPermission")
    override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> =
        conflatedCallbackFlow {
                val callback =
                    object : NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
                        override fun onLost(network: Network) {
                            // Send a disconnected model when lost. Maybe should create a sealed
                            // type or null here?
                            trySend(MobileConnectivityModel())
                        }

                        override fun onCapabilitiesChanged(
                            network: Network,
                            caps: NetworkCapabilities
                        ) {
                            trySend(
                                MobileConnectivityModel(
                                    isConnected = caps.hasTransport(TRANSPORT_CELLULAR),
                                    isValidated = caps.hasCapability(NET_CAPABILITY_VALIDATED),
                                )
                            )
                        }
                    }

                connectivityManager.registerDefaultNetworkCallback(callback)

                awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
            }
            .stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectivityModel())

    private fun isValidSubId(subId: Int): Boolean {
        subscriptionsFlow.value.forEach {
            if (it.subscriptionId == subId) {
@@ -181,7 +267,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>) {
+1 −2
Original line number Diff line number Diff line
@@ -26,7 +26,6 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.mapLatest
@@ -40,7 +39,7 @@ import kotlinx.coroutines.withContext
 */
interface UserSetupRepository {
    /** Observable tracking [DeviceProvisionedController.isUserSetup] */
    val isUserSetupFlow: Flow<Boolean>
    val isUserSetupFlow: StateFlow<Boolean>
}

@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+68 −40
Original line number Diff line number Diff line
@@ -18,49 +18,69 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor

import android.telephony.CarrierConfigManager
import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Connected
import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
import com.android.systemui.util.CarrierConfigTracker
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn

interface MobileIconInteractor {
    /** Only true if mobile is the default transport but is not validated, otherwise false */
    val isDefaultConnectionFailed: StateFlow<Boolean>

    /** True when telephony tells us that the data state is CONNECTED */
    val isDataConnected: StateFlow<Boolean>

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

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

    /** Observable for RAT type (network type) indicator */
    val networkTypeIconGroup: Flow<MobileIconGroup>
    val networkTypeIconGroup: StateFlow<MobileIconGroup>

    /** True if this line of service is emergency-only */
    val isEmergencyOnly: Flow<Boolean>
    val isEmergencyOnly: StateFlow<Boolean>

    /** Int describing the connection strength. 0-4 OR 1-5. See [numberOfLevels] */
    val level: Flow<Int>
    val level: StateFlow<Int>

    /** 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>
    val numberOfLevels: StateFlow<Int>
}

/** Interactor for a single mobile connection. This connection _should_ have one subscription ID */
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@OptIn(ExperimentalCoroutinesApi::class)
class MobileIconInteractorImpl(
    defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>>,
    defaultMobileIconGroup: Flow<MobileIconGroup>,
    @Application scope: CoroutineScope,
    defaultSubscriptionHasDataEnabled: StateFlow<Boolean>,
    defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>>,
    defaultMobileIconGroup: StateFlow<MobileIconGroup>,
    override val isDefaultConnectionFailed: StateFlow<Boolean>,
    mobileMappingsProxy: MobileMappingsProxy,
    connectionRepository: MobileConnectionRepository,
) : MobileIconInteractor {
    private val mobileStatusInfo = connectionRepository.subscriptionModelFlow

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

    override val isDefaultDataEnabled = defaultSubscriptionHasDataEnabled

    /** Observable for the current RAT indicator icon ([MobileIconGroup]) */
    override val networkTypeIconGroup: Flow<MobileIconGroup> =
    override val networkTypeIconGroup: StateFlow<MobileIconGroup> =
        combine(
                mobileStatusInfo,
                defaultMobileIconMapping,
@@ -69,15 +89,21 @@ class MobileIconInteractorImpl(
                val lookupKey =
                    when (val resolved = info.resolvedNetworkType) {
                        is DefaultNetworkType -> mobileMappingsProxy.toIconKey(resolved.type)
                    is OverrideNetworkType -> mobileMappingsProxy.toIconKeyOverride(resolved.type)
                        is OverrideNetworkType ->
                            mobileMappingsProxy.toIconKeyOverride(resolved.type)
                    }
                mapping[lookupKey] ?: defaultGroup
            }
            .stateIn(scope, SharingStarted.WhileSubscribed(), defaultMobileIconGroup.value)

    override val isEmergencyOnly: Flow<Boolean> = mobileStatusInfo.map { it.isEmergencyOnly }
    override val isEmergencyOnly: StateFlow<Boolean> =
        mobileStatusInfo
            .mapLatest { it.isEmergencyOnly }
            .stateIn(scope, SharingStarted.WhileSubscribed(), false)

    override val level: Flow<Int> =
        mobileStatusInfo.map { mobileModel ->
    override val level: StateFlow<Int> =
        mobileStatusInfo
            .mapLatest { mobileModel ->
                // TODO: incorporate [MobileMappings.Config.alwaysShowCdmaRssi]
                if (mobileModel.isGsm) {
                    mobileModel.primaryLevel
@@ -85,14 +111,16 @@ class MobileIconInteractorImpl(
                    mobileModel.cdmaLevel
                }
            }
            .stateIn(scope, SharingStarted.WhileSubscribed(), 0)

    /**
     * This will become variable based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL]
     * once it's wired up inside of [CarrierConfigTracker]
     */
    override val numberOfLevels: Flow<Int> = flowOf(4)
    override val numberOfLevels: StateFlow<Int> = MutableStateFlow(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)
    override val isDataConnected: StateFlow<Boolean> =
        mobileStatusInfo
            .mapLatest { subscriptionModel -> subscriptionModel.dataConnectionState == Connected }
            .stateIn(scope, SharingStarted.WhileSubscribed(), false)
}
Loading