From be25e8f1f407df340636259f6d22496638e8b507 Mon Sep 17 00:00:00 2001 From: Evan Laird Date: Mon, 24 Oct 2022 16:42:15 -0400 Subject: [PATCH 1/4] [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 --- .../repository/MobileConnectionRepository.kt | 56 ++++++++++++++- .../repository/MobileConnectionsRepository.kt | 72 +++++++++++++++---- .../domain/interactor/MobileIconInteractor.kt | 14 ++-- .../interactor/MobileIconsInteractor.kt | 18 +++++ .../ui/viewmodel/MobileIconViewModel.kt | 20 ++++-- .../FakeMobileConnectionRepository.kt | 7 ++ .../FakeMobileConnectionsRepository.kt | 19 ++++- .../MobileConnectionRepositoryTest.kt | 61 ++++++++++++++++ .../MobileConnectionsRepositoryTest.kt | 69 ++++++++++++++---- .../interactor/FakeMobileIconInteractor.kt | 14 ++-- .../interactor/FakeMobileIconsInteractor.kt | 6 +- .../interactor/MobileIconInteractorTest.kt | 16 +++++ .../interactor/MobileIconsInteractorTest.kt | 47 ++++++++++++ .../ui/viewmodel/MobileIconViewModelTest.kt | 27 ++++++- 14 files changed, 390 insertions(+), 56 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt index 06e8f467ee0b2..41bcab1be2d59 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt @@ -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 /** Observable tracking [TelephonyManager.isDataConnectionAllowed] */ val dataEnabled: Flow + /** + * True if this connection represents the default subscription per + * [SubscriptionManager.getDefaultDataSubscriptionId] + */ + val isDefaultDataSubscription: Flow } @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, + globalMobileDataSettingChangedEvent: Flow, 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 = 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 = subscriptionModelFlow.map {} + private val telephonyPollingEvent: Flow = + merge( + telephonyCallbackEvent, + localMobileDataSettingChangedEvent, + globalMobileDataSettingChangedEvent, + ) override val dataEnabled: Flow = telephonyPollingEvent.map { dataConnectionAllowed() } private fun dataConnectionAllowed(): Boolean = telephonyManager.isDataConnectionAllowed + override val isDefaultDataSubscription: Flow = 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, + globalMobileDataSettingChangedEvent: Flow, + ): MobileConnectionRepository { return MobileConnectionRepositoryImpl( + context, subId, telephonyManager.createForSubscriptionId(subId), + globalSettings, + defaultDataSubId, + globalMobileDataSettingChangedEvent, bgDispatcher, logger, scope, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt index 0e2428ae393a1..bd9f85ee9dcbb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt @@ -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 + /** Tracks [SubscriptionManager.getDefaultDataSubscriptionId] */ + val defaultDataSubId: StateFlow + /** 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 } @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,17 +136,26 @@ constructor( telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback) awaitClose { telephonyManager.unregisterTelephonyCallback(callback) } } + .stateIn(scope, started = SharingStarted.WhileSubscribed(), INVALID_SUBSCRIPTION_ID) + + private val defaultDataSubIdChangeEvent: MutableSharedFlow = + MutableSharedFlow(extraBufferCapacity = 1) + + override val defaultDataSubId: StateFlow = + 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, - started = SharingStarted.WhileSubscribed(), - SubscriptionManager.INVALID_SUBSCRIPTION_ID + SharingStarted.WhileSubscribed(), + SubscriptionManager.getDefaultDataSubscriptionId() ) - private val defaultDataSubChangedEvent = - broadcastDispatcher.broadcastFlow( - IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED) - ) - private val carrierConfigChangedEvent = broadcastDispatcher.broadcastFlow( IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED) @@ -148,9 +172,8 @@ constructor( * This flow will produce whenever the default data subscription or the carrier config changes. */ override val defaultDataSubRatConfig: StateFlow = - 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 = 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) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt index f99d278c39032..cdd4982f0a791 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt @@ -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 + /** Observable for the data enabled state of this connection */ val isDataEnabled: Flow @@ -43,13 +47,11 @@ interface MobileIconInteractor { /** Based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL], either 4 or 5 */ val numberOfLevels: Flow - - /** True when we want to draw an icon that makes room for the exclamation mark */ - val cutOut: Flow } /** Interactor for a single mobile connection. This connection _should_ have one subscription ID */ class MobileIconInteractorImpl( + defaultSubscriptionHasDataEnabled: Flow, defaultMobileIconMapping: Flow>, defaultMobileIconGroup: Flow, mobileMappingsProxy: MobileMappingsProxy, @@ -59,6 +61,8 @@ class MobileIconInteractorImpl( override val isDataEnabled: Flow = connectionRepository.dataEnabled + override val isDefaultDataEnabled = defaultSubscriptionHasDataEnabled + /** Observable for the current RAT indicator icon ([MobileIconGroup]) */ override val networkTypeIconGroup: Flow = combine( @@ -91,8 +95,4 @@ class MobileIconInteractorImpl( * once it's wired up inside of [CarrierConfigTracker] */ override val numberOfLevels: Flow = 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 = flowOf(false) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt index 614d583c3c486..31cb21f7720f7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt @@ -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> + /** True if the active mobile data subscription has data enabled */ + val activeDataConnectionHasDataEnabled: Flow /** The icon mapping from network type to [MobileIconGroup] for the default subscription */ val defaultMobileIconMapping: Flow> /** 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 = + activeMobileDataSubscriptionId.map { activeId -> + if (activeId == INVALID_SUBSCRIPTION_ID) { + null + } else { + mobileConnectionsRepo.getRepoForSubId(activeId) + } + } + + override val activeDataConnectionHasDataEnabled: Flow = + activeMobileDataConnectionRepo.flatMapLatest { it?.dataEnabled ?: flowOf(false) } + private val unfilteredSubscriptions: Flow> = 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, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt index 81317398f0867..b256665358d44 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt @@ -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 = + iconInteractor.isDefaultDataEnabled.mapLatest { !it } + /** An int consumable by [SignalDrawable] for display */ - var iconId: Flow = - combine(iconInteractor.level, iconInteractor.numberOfLevels, iconInteractor.cutOut) { + val iconId: Flow = + 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 = + val networkTypeIcon: Flow = combine(iconInteractor.networkTypeIconGroup, iconInteractor.isDataEnabled) { networkTypeIconGroup, isDataEnabled -> @@ -72,5 +80,5 @@ constructor( } } - var tint: Flow = flowOf(Color.CYAN) + val tint: Flow = flowOf(Color.CYAN) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt index de1fec85360ba..4c7f7eab981d3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt @@ -27,6 +27,9 @@ class FakeMobileConnectionRepository : MobileConnectionRepository { private val _dataEnabled = MutableStateFlow(true) override val dataEnabled = _dataEnabled + private val _isDefaultDataSubscription = MutableStateFlow(true) + override val isDefaultDataSubscription = _isDefaultDataSubscription + fun setMobileSubscriptionModel(model: MobileSubscriptionModel) { _subscriptionsModelFlow.value = model } @@ -34,4 +37,8 @@ class FakeMobileConnectionRepository : MobileConnectionRepository { fun setDataEnabled(enabled: Boolean) { _dataEnabled.value = enabled } + + fun setIsDefaultDataSubscription(isDefault: Boolean) { + _isDefaultDataSubscription.value = isDefault + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt index 813e750684a04..8488effd22ee3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt @@ -17,7 +17,7 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository import android.telephony.SubscriptionInfo -import android.telephony.SubscriptionManager +import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import com.android.settingslib.mobile.MobileMappings.Config import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -26,18 +26,23 @@ class FakeMobileConnectionsRepository : MobileConnectionsRepository { private val _subscriptionsFlow = MutableStateFlow>(listOf()) override val subscriptionsFlow: Flow> = _subscriptionsFlow - private val _activeMobileDataSubscriptionId = - MutableStateFlow(SubscriptionManager.INVALID_SUBSCRIPTION_ID) + private val _activeMobileDataSubscriptionId = MutableStateFlow(INVALID_SUBSCRIPTION_ID) override val activeMobileDataSubscriptionId = _activeMobileDataSubscriptionId private val _defaultDataSubRatConfig = MutableStateFlow(Config()) override val defaultDataSubRatConfig = _defaultDataSubRatConfig + private val _defaultDataSubId = MutableStateFlow(INVALID_SUBSCRIPTION_ID) + override val defaultDataSubId = _defaultDataSubId + private val subIdRepos = mutableMapOf() override fun getRepoForSubId(subId: Int): MobileConnectionRepository { return subIdRepos[subId] ?: FakeMobileConnectionRepository().also { subIdRepos[subId] = it } } + private val _globalMobileDataSettingChangedEvent = MutableStateFlow(Unit) + override val globalMobileDataSettingChangedEvent = _globalMobileDataSettingChangedEvent + fun setSubscriptions(subs: List) { _subscriptionsFlow.value = subs } @@ -46,6 +51,14 @@ class FakeMobileConnectionsRepository : MobileConnectionsRepository { _defaultDataSubRatConfig.value = config } + fun setDefaultDataSubId(id: Int) { + _defaultDataSubId.value = id + } + + suspend fun triggerGlobalMobileDataSettingChangedEvent() { + _globalMobileDataSettingChangedEvent.emit(Unit) + } + fun setActiveMobileDataSubscriptionId(subId: Int) { _activeMobileDataSubscriptionId.value = subId } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt index 0939364447893..ed3eda5beaf35 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository +import android.os.UserHandle +import android.provider.Settings import android.telephony.CellSignalStrengthCdma import android.telephony.ServiceState import android.telephony.SignalStrength @@ -42,6 +44,7 @@ import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -67,16 +70,23 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { @Mock private lateinit var logger: ConnectivityPipelineLogger private val scope = CoroutineScope(IMMEDIATE) + private val globalSettings = FakeSettings() + private val connectionsRepo = FakeMobileConnectionsRepository() @Before fun setUp() { MockitoAnnotations.initMocks(this) + globalSettings.userId = UserHandle.USER_ALL whenever(telephonyManager.subscriptionId).thenReturn(SUB_1_ID) underTest = MobileConnectionRepositoryImpl( + context, SUB_1_ID, telephonyManager, + globalSettings, + connectionsRepo.defaultDataSubId, + connectionsRepo.globalMobileDataSettingChangedEvent, IMMEDIATE, logger, scope, @@ -315,6 +325,57 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { job.cancel() } + @Test + fun isDefaultDataSubscription_isDefault() = + runBlocking(IMMEDIATE) { + connectionsRepo.setDefaultDataSubId(SUB_1_ID) + + var latest: Boolean? = null + val job = underTest.isDefaultDataSubscription.onEach { latest = it }.launchIn(this) + + assertThat(latest).isTrue() + + job.cancel() + } + + @Test + fun isDefaultDataSubscription_isNotDefault() = + runBlocking(IMMEDIATE) { + // Our subId is SUB_1_ID + connectionsRepo.setDefaultDataSubId(123) + + var latest: Boolean? = null + val job = underTest.isDefaultDataSubscription.onEach { latest = it }.launchIn(this) + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun isDataConnectionAllowed_subIdSettingUpdate_valueUpdated() = + runBlocking(IMMEDIATE) { + val subIdSettingName = "${Settings.Global.MOBILE_DATA}$SUB_1_ID" + + var latest: Boolean? = null + val job = underTest.dataEnabled.onEach { latest = it }.launchIn(this) + + // We don't read the setting directly, we query telephony when changes happen + whenever(telephonyManager.isDataConnectionAllowed).thenReturn(false) + globalSettings.putInt(subIdSettingName, 0) + assertThat(latest).isFalse() + + whenever(telephonyManager.isDataConnectionAllowed).thenReturn(true) + globalSettings.putInt(subIdSettingName, 1) + assertThat(latest).isTrue() + + whenever(telephonyManager.isDataConnectionAllowed).thenReturn(false) + globalSettings.putInt(subIdSettingName, 0) + assertThat(latest).isFalse() + + job.cancel() + } + private fun getTelephonyCallbacks(): List { val callbackCaptor = argumentCaptor() Mockito.verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt index 326e0d28166f5..27c7357cb9958 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt @@ -16,26 +16,27 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository +import android.content.Intent +import android.provider.Settings import android.telephony.SubscriptionInfo import android.telephony.SubscriptionManager import android.telephony.TelephonyCallback import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener import android.telephony.TelephonyManager import androidx.test.filters.SmallTest +import com.android.internal.telephony.PhoneConstants import com.android.systemui.SysuiTestCase -import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.mock -import com.android.systemui.util.mockito.nullable import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.runBlocking @@ -43,7 +44,6 @@ import org.junit.After import org.junit.Assert.assertThrows import org.junit.Before import org.junit.Test -import org.mockito.ArgumentMatchers import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @@ -57,29 +57,21 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { @Mock private lateinit var subscriptionManager: SubscriptionManager @Mock private lateinit var telephonyManager: TelephonyManager @Mock private lateinit var logger: ConnectivityPipelineLogger - @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher private val scope = CoroutineScope(IMMEDIATE) + private val globalSettings = FakeSettings() @Before fun setUp() { MockitoAnnotations.initMocks(this) - whenever( - broadcastDispatcher.broadcastFlow( - any(), - nullable(), - ArgumentMatchers.anyInt(), - nullable(), - ) - ) - .thenReturn(flowOf(Unit)) underTest = MobileConnectionsRepositoryImpl( subscriptionManager, telephonyManager, logger, - broadcastDispatcher, + fakeBroadcastDispatcher, + globalSettings, context, IMMEDIATE, scope, @@ -214,6 +206,53 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { job.cancel() } + @Test + fun testDefaultDataSubId_updatesOnBroadcast() = + runBlocking(IMMEDIATE) { + var latest: Int? = null + val job = underTest.defaultDataSubId.onEach { latest = it }.launchIn(this) + + fakeBroadcastDispatcher.registeredReceivers.forEach { receiver -> + receiver.onReceive( + context, + Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED) + .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_2_ID) + ) + } + + assertThat(latest).isEqualTo(SUB_2_ID) + + fakeBroadcastDispatcher.registeredReceivers.forEach { receiver -> + receiver.onReceive( + context, + Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED) + .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_1_ID) + ) + } + + assertThat(latest).isEqualTo(SUB_1_ID) + + job.cancel() + } + + @Test + fun globalMobileDataSettingsChangedEvent_producesOnSettingChange() = + runBlocking(IMMEDIATE) { + var produced = false + val job = + underTest.globalMobileDataSettingChangedEvent + .onEach { produced = true } + .launchIn(this) + + assertThat(produced).isFalse() + + globalSettings.putInt(Settings.Global.MOBILE_DATA, 0) + + assertThat(produced).isTrue() + + job.cancel() + } + private fun getSubscriptionCallback(): SubscriptionManager.OnSubscriptionsChangedListener { val callbackCaptor = argumentCaptor() verify(subscriptionManager) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt index 5611c448c5502..ea3faa0dff1aa 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt @@ -31,15 +31,15 @@ class FakeMobileIconInteractor : MobileIconInteractor { private val _isDataEnabled = MutableStateFlow(true) override val isDataEnabled = _isDataEnabled + private val _isDefaultDataEnabled = MutableStateFlow(true) + override val isDefaultDataEnabled = _isDefaultDataEnabled + private val _level = MutableStateFlow(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN) override val level = _level private val _numberOfLevels = MutableStateFlow(4) override val numberOfLevels = _numberOfLevels - private val _cutOut = MutableStateFlow(false) - override val cutOut = _cutOut - fun setIconGroup(group: SignalIcon.MobileIconGroup) { _iconGroup.value = group } @@ -52,6 +52,10 @@ class FakeMobileIconInteractor : MobileIconInteractor { _isDataEnabled.value = enabled } + fun setIsDefaultDataEnabled(disabled: Boolean) { + _isDefaultDataEnabled.value = disabled + } + fun setLevel(level: Int) { _level.value = level } @@ -59,8 +63,4 @@ class FakeMobileIconInteractor : MobileIconInteractor { fun setNumberOfLevels(num: Int) { _numberOfLevels.value = num } - - fun setCutOut(cutOut: Boolean) { - _cutOut.value = cutOut - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt index 2bd228603cb01..cf95bf736bab1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt @@ -26,8 +26,7 @@ import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import kotlinx.coroutines.flow.MutableStateFlow -class FakeMobileIconsInteractor(private val mobileMappings: MobileMappingsProxy) : - MobileIconsInteractor { +class FakeMobileIconsInteractor(mobileMappings: MobileMappingsProxy) : MobileIconsInteractor { val THREE_G_KEY = mobileMappings.toIconKey(THREE_G) val LTE_KEY = mobileMappings.toIconKey(LTE) val FOUR_G_KEY = mobileMappings.toIconKey(FOUR_G) @@ -49,6 +48,9 @@ class FakeMobileIconsInteractor(private val mobileMappings: MobileMappingsProxy) private val _filteredSubscriptions = MutableStateFlow>(listOf()) override val filteredSubscriptions = _filteredSubscriptions + private val _activeDataConnectionHasDataEnabled = MutableStateFlow(false) + override val activeDataConnectionHasDataEnabled = _activeDataConnectionHasDataEnabled + private val _defaultMobileIconMapping = MutableStateFlow(TEST_MAPPING) override val defaultMobileIconMapping = _defaultMobileIconMapping diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt index ff44af4c92040..4d6ed2fc7493b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt @@ -53,6 +53,7 @@ class MobileIconInteractorTest : SysuiTestCase() { fun setUp() { underTest = MobileIconInteractorImpl( + mobileIconsInteractor.activeDataConnectionHasDataEnabled, mobileIconsInteractor.defaultMobileIconMapping, mobileIconsInteractor.defaultMobileIconGroup, mobileMappingsProxy, @@ -196,6 +197,21 @@ class MobileIconInteractorTest : SysuiTestCase() { job.cancel() } + @Test + fun test_isDefaultDataEnabled_matchesParent() = + runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest.isDefaultDataEnabled.onEach { latest = it }.launchIn(this) + + mobileIconsInteractor.activeDataConnectionHasDataEnabled.value = true + assertThat(latest).isTrue() + + mobileIconsInteractor.activeDataConnectionHasDataEnabled.value = false + assertThat(latest).isFalse() + + job.cancel() + } + companion object { private val IMMEDIATE = Dispatchers.Main.immediate diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt index 877ce0e6b3513..799d0a3f1cc72 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor import android.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository @@ -32,6 +33,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.yield import org.junit.After import org.junit.Before import org.junit.Test @@ -168,6 +170,51 @@ class MobileIconsInteractorTest : SysuiTestCase() { job.cancel() } + @Test + fun activeDataConnection_turnedOn() = + runBlocking(IMMEDIATE) { + CONNECTION_1.setDataEnabled(true) + var latest: Boolean? = null + val job = + underTest.activeDataConnectionHasDataEnabled.onEach { latest = it }.launchIn(this) + + assertThat(latest).isTrue() + + job.cancel() + } + + @Test + fun activeDataConnection_turnedOff() = + runBlocking(IMMEDIATE) { + CONNECTION_1.setDataEnabled(true) + var latest: Boolean? = null + val job = + underTest.activeDataConnectionHasDataEnabled.onEach { latest = it }.launchIn(this) + + CONNECTION_1.setDataEnabled(false) + yield() + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun activeDataConnection_invalidSubId() = + runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = + underTest.activeDataConnectionHasDataEnabled.onEach { latest = it }.launchIn(this) + + connectionsRepository.setActiveMobileDataSubscriptionId(INVALID_SUBSCRIPTION_ID) + yield() + + // An invalid active subId should tell us that data is off + assertThat(latest).isFalse() + + job.cancel() + } + companion object { private val IMMEDIATE = Dispatchers.Main.immediate diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt index ce0f33f400ab6..da0bf5c274856 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt @@ -46,7 +46,7 @@ class MobileIconViewModelTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) interactor.apply { setLevel(1) - setCutOut(false) + setIsDefaultDataEnabled(true) setIconGroup(THREE_G) setIsEmergencyOnly(false) setNumberOfLevels(4) @@ -59,8 +59,23 @@ class MobileIconViewModelTest : SysuiTestCase() { runBlocking(IMMEDIATE) { var latest: Int? = null val job = underTest.iconId.onEach { latest = it }.launchIn(this) + val expected = defaultSignal() - assertThat(latest).isEqualTo(SignalDrawable.getState(1, 4, false)) + assertThat(latest).isEqualTo(expected) + + job.cancel() + } + + @Test + fun iconId_cutout_whenDefaultDataDisabled() = + runBlocking(IMMEDIATE) { + interactor.setIsDefaultDataEnabled(false) + + var latest: Int? = null + val job = underTest.iconId.onEach { latest = it }.launchIn(this) + val expected = defaultSignal(level = 1, connected = false) + + assertThat(latest).isEqualTo(expected) job.cancel() } @@ -119,6 +134,14 @@ class MobileIconViewModelTest : SysuiTestCase() { job.cancel() } + /** Convenience constructor for these tests */ + private fun defaultSignal( + level: Int = 1, + connected: Boolean = true, + ): Int { + return SignalDrawable.getState(level, /* numLevels */ 4, !connected) + } + companion object { private val IMMEDIATE = Dispatchers.Main.immediate private const val SUB_1_ID = 1 From 93b945ce54f1e61d75e3cf9cef4a4083f03c5e24 Mon Sep 17 00:00:00 2001 From: Evan Laird Date: Wed, 26 Oct 2022 16:04:08 -0400 Subject: [PATCH 2/4] [SB refactor] Upgrade most data sources to `StateFlow` Most of the data model for mobile is more correctly represented as a StateFlow, since the state from callbacks needs to persist even when there is no corrent collector on the mobile icon. This change also exposes a current issue: toggling mobile data on/off multiple times can introduce an inconsistency in the UI. This is approximately the same as the old pipeline, which also experiences the same problem. Test: manual Test: atest packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/* Bug: 240492102 Change-Id: Idd5fa0205380b4ba15f7d723b32b08e114456f7f --- .../repository/MobileConnectionRepository.kt | 27 ++++-- .../repository/MobileConnectionsRepository.kt | 2 +- .../data/repository/UserSetupRepository.kt | 3 +- .../domain/interactor/MobileIconInteractor.kt | 88 +++++++++++-------- .../interactor/MobileIconsInteractor.kt | 39 ++++---- .../FakeMobileConnectionRepository.kt | 3 +- .../repository/FakeUserSetupRepository.kt | 3 +- .../MobileConnectionRepositoryTest.kt | 20 +++-- .../interactor/MobileIconInteractorTest.kt | 4 + 9 files changed, 112 insertions(+), 77 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt index 41bcab1be2d59..581842bc2f579 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepository.kt @@ -47,10 +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 /** @@ -71,12 +73,12 @@ interface MobileConnectionRepository { */ val subscriptionModelFlow: Flow /** Observable tracking [TelephonyManager.isDataConnectionAllowed] */ - val dataEnabled: Flow + val dataEnabled: StateFlow /** * True if this connection represents the default subscription per * [SubscriptionManager.getDefaultDataSubscriptionId] */ - val isDefaultDataSubscription: Flow + val isDefaultDataSubscription: StateFlow } @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @@ -86,7 +88,7 @@ class MobileConnectionRepositoryImpl( private val subId: Int, private val telephonyManager: TelephonyManager, private val globalSettings: GlobalSettings, - defaultDataSubId: Flow, + defaultDataSubId: StateFlow, globalMobileDataSettingChangedEvent: Flow, bgDispatcher: CoroutineDispatcher, logger: ConnectivityPipelineLogger, @@ -101,6 +103,8 @@ class MobileConnectionRepositoryImpl( } } + private val telephonyCallbackEvent = MutableSharedFlow(extraBufferCapacity = 1) + override val subscriptionModelFlow: StateFlow = run { var state = MobileSubscriptionModel() conflatedCallbackFlow { @@ -180,12 +184,11 @@ class MobileConnectionRepositoryImpl( telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback) awaitClose { telephonyManager.unregisterTelephonyCallback(callback) } } + .onEach { telephonyCallbackEvent.tryEmit(Unit) } .logOutputChange(logger, "MobileSubscriptionModel") .stateIn(scope, SharingStarted.WhileSubscribed(), state) } - private val telephonyCallbackEvent = subscriptionModelFlow.map {} - /** Produces whenever the mobile data setting changes for this subId */ private val localMobileDataSettingChangedEvent: Flow = conflatedCallbackFlow { val observer = @@ -216,11 +219,17 @@ class MobileConnectionRepositoryImpl( globalMobileDataSettingChangedEvent, ) - override val dataEnabled: Flow = telephonyPollingEvent.map { dataConnectionAllowed() } + override val dataEnabled: StateFlow = + telephonyPollingEvent + .mapLatest { dataConnectionAllowed() } + .stateIn(scope, SharingStarted.WhileSubscribed(), dataConnectionAllowed()) private fun dataConnectionAllowed(): Boolean = telephonyManager.isDataConnectionAllowed - override val isDefaultDataSubscription: Flow = defaultDataSubId.map { it == subId } + override val isDefaultDataSubscription: StateFlow = + defaultDataSubId + .mapLatest { it == subId } + .stateIn(scope, SharingStarted.WhileSubscribed(), defaultDataSubId.value == subId) class Factory @Inject @@ -234,7 +243,7 @@ class MobileConnectionRepositoryImpl( ) { fun build( subId: Int, - defaultDataSubId: Flow, + defaultDataSubId: StateFlow, globalMobileDataSettingChangedEvent: Flow, ): MobileConnectionRepository { return MobileConnectionRepositoryImpl( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt index bd9f85ee9dcbb..89edc8a873b96 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt @@ -65,7 +65,7 @@ interface MobileConnectionsRepository { val subscriptionsFlow: Flow> /** Observable for the subscriptionId of the current mobile data connection */ - val activeMobileDataSubscriptionId: Flow + val activeMobileDataSubscriptionId: StateFlow /** Observable for [MobileMappings.Config] tracking the defaults */ val defaultDataSubRatConfig: StateFlow diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt index 77de849691db3..91886bb121d5f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/UserSetupRepository.kt @@ -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 + val isUserSetupFlow: StateFlow } @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt index cdd4982f0a791..dd8a91fe58ed4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt @@ -18,81 +18,97 @@ 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.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.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +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 { // TODO(b/256839546): clarify naming of default vs active /** True if we want to consider the data connection enabled */ - val isDefaultDataEnabled: Flow + val isDefaultDataEnabled: StateFlow /** Observable for the data enabled state of this connection */ - val isDataEnabled: Flow + val isDataEnabled: StateFlow /** Observable for RAT type (network type) indicator */ - val networkTypeIconGroup: Flow + val networkTypeIconGroup: StateFlow /** True if this line of service is emergency-only */ - val isEmergencyOnly: Flow + val isEmergencyOnly: StateFlow /** Int describing the connection strength. 0-4 OR 1-5. See [numberOfLevels] */ - val level: Flow + val level: StateFlow /** Based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL], either 4 or 5 */ - val numberOfLevels: Flow + val numberOfLevels: StateFlow } /** Interactor for a single mobile connection. This connection _should_ have one subscription ID */ +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) class MobileIconInteractorImpl( - defaultSubscriptionHasDataEnabled: Flow, - defaultMobileIconMapping: Flow>, - defaultMobileIconGroup: Flow, + @Application scope: CoroutineScope, + defaultSubscriptionHasDataEnabled: StateFlow, + defaultMobileIconMapping: StateFlow>, + defaultMobileIconGroup: StateFlow, mobileMappingsProxy: MobileMappingsProxy, connectionRepository: MobileConnectionRepository, ) : MobileIconInteractor { private val mobileStatusInfo = connectionRepository.subscriptionModelFlow - override val isDataEnabled: Flow = connectionRepository.dataEnabled + override val isDataEnabled: StateFlow = connectionRepository.dataEnabled override val isDefaultDataEnabled = defaultSubscriptionHasDataEnabled /** Observable for the current RAT indicator icon ([MobileIconGroup]) */ - override val networkTypeIconGroup: Flow = + override val networkTypeIconGroup: StateFlow = combine( - mobileStatusInfo, - defaultMobileIconMapping, - defaultMobileIconGroup, - ) { info, mapping, defaultGroup -> - val lookupKey = - when (val resolved = info.resolvedNetworkType) { - is DefaultNetworkType -> mobileMappingsProxy.toIconKey(resolved.type) - is OverrideNetworkType -> mobileMappingsProxy.toIconKeyOverride(resolved.type) - } - mapping[lookupKey] ?: defaultGroup - } - - override val isEmergencyOnly: Flow = mobileStatusInfo.map { it.isEmergencyOnly } - - override val level: Flow = - mobileStatusInfo.map { mobileModel -> - // TODO: incorporate [MobileMappings.Config.alwaysShowCdmaRssi] - if (mobileModel.isGsm) { - mobileModel.primaryLevel - } else { - mobileModel.cdmaLevel + mobileStatusInfo, + defaultMobileIconMapping, + defaultMobileIconGroup, + ) { info, mapping, defaultGroup -> + val lookupKey = + when (val resolved = info.resolvedNetworkType) { + is DefaultNetworkType -> mobileMappingsProxy.toIconKey(resolved.type) + is OverrideNetworkType -> + mobileMappingsProxy.toIconKeyOverride(resolved.type) + } + mapping[lookupKey] ?: defaultGroup } - } + .stateIn(scope, SharingStarted.WhileSubscribed(), defaultMobileIconGroup.value) + + override val isEmergencyOnly: StateFlow = + mobileStatusInfo + .mapLatest { it.isEmergencyOnly } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + override val level: StateFlow = + mobileStatusInfo + .mapLatest { mobileModel -> + // TODO: incorporate [MobileMappings.Config.alwaysShowCdmaRssi] + if (mobileModel.isGsm) { + mobileModel.primaryLevel + } else { + 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 = flowOf(4) + override val numberOfLevels: StateFlow = MutableStateFlow(4) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt index 31cb21f7720f7..9d30ec05aad31 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt @@ -38,7 +38,7 @@ 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.mapLatest import kotlinx.coroutines.flow.stateIn /** @@ -55,13 +55,13 @@ interface MobileIconsInteractor { /** List of subscriptions, potentially filtered for CBRS */ val filteredSubscriptions: Flow> /** True if the active mobile data subscription has data enabled */ - val activeDataConnectionHasDataEnabled: Flow + val activeDataConnectionHasDataEnabled: StateFlow /** The icon mapping from network type to [MobileIconGroup] for the default subscription */ - val defaultMobileIconMapping: Flow> + val defaultMobileIconMapping: StateFlow> /** Fallback [MobileIconGroup] in the case where there is no icon in the mapping */ - val defaultMobileIconGroup: Flow + val defaultMobileIconGroup: StateFlow /** True once the user has been set up */ - val isUserSetup: Flow + val isUserSetup: StateFlow /** * Vends out a [MobileIconInteractor] tracking the [MobileConnectionRepository] for the given * subId. Will throw if the ID is invalid @@ -84,17 +84,21 @@ constructor( private val activeMobileDataSubscriptionId = mobileConnectionsRepo.activeMobileDataSubscriptionId - private val activeMobileDataConnectionRepo: Flow = - activeMobileDataSubscriptionId.map { activeId -> - if (activeId == INVALID_SUBSCRIPTION_ID) { - null - } else { - mobileConnectionsRepo.getRepoForSubId(activeId) + private val activeMobileDataConnectionRepo: StateFlow = + activeMobileDataSubscriptionId + .mapLatest { activeId -> + if (activeId == INVALID_SUBSCRIPTION_ID) { + null + } else { + mobileConnectionsRepo.getRepoForSubId(activeId) + } } - } + .stateIn(scope, SharingStarted.WhileSubscribed(), null) - override val activeDataConnectionHasDataEnabled: Flow = - activeMobileDataConnectionRepo.flatMapLatest { it?.dataEnabled ?: flowOf(false) } + override val activeDataConnectionHasDataEnabled: StateFlow = + activeMobileDataConnectionRepo + .flatMapLatest { it?.dataEnabled ?: flowOf(false) } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) private val unfilteredSubscriptions: Flow> = mobileConnectionsRepo.subscriptionsFlow @@ -149,20 +153,21 @@ constructor( */ override val defaultMobileIconMapping: StateFlow> = mobileConnectionsRepo.defaultDataSubRatConfig - .map { mobileMappingsProxy.mapIconSets(it) } + .mapLatest { mobileMappingsProxy.mapIconSets(it) } .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue = mapOf()) /** If there is no mapping in [defaultMobileIconMapping], then use this default icon group */ override val defaultMobileIconGroup: StateFlow = mobileConnectionsRepo.defaultDataSubRatConfig - .map { mobileMappingsProxy.getDefaultIcons(it) } + .mapLatest { mobileMappingsProxy.getDefaultIcons(it) } .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue = TelephonyIcons.G) - override val isUserSetup: Flow = userSetupRepo.isUserSetupFlow + override val isUserSetup: StateFlow = userSetupRepo.isUserSetupFlow /** Vends out new [MobileIconInteractor] for a particular subId */ override fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor = MobileIconInteractorImpl( + scope, activeDataConnectionHasDataEnabled, defaultMobileIconMapping, defaultMobileIconGroup, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt index 4c7f7eab981d3..288f54c7d03c0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt @@ -17,12 +17,11 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow class FakeMobileConnectionRepository : MobileConnectionRepository { private val _subscriptionsModelFlow = MutableStateFlow(MobileSubscriptionModel()) - override val subscriptionModelFlow: Flow = _subscriptionsModelFlow + override val subscriptionModelFlow = _subscriptionsModelFlow private val _dataEnabled = MutableStateFlow(true) override val dataEnabled = _dataEnabled diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt index 6c495c5c705af..141b50c017e11 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeUserSetupRepository.kt @@ -16,13 +16,12 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow /** Defaults to `true` */ class FakeUserSetupRepository : UserSetupRepository { private val _isUserSetup: MutableStateFlow = MutableStateFlow(true) - override val isUserSetupFlow: Flow = _isUserSetup + override val isUserSetupFlow = _isUserSetup fun setUserSetup(setup: Boolean) { _isUserSetup.value = setup diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt index ed3eda5beaf35..5ce51bb62c78e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryTest.kt @@ -300,14 +300,20 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { } @Test - fun dataEnabled_isEnabled() = + fun dataEnabled_initial_false() = runBlocking(IMMEDIATE) { whenever(telephonyManager.isDataConnectionAllowed).thenReturn(true) - var latest: Boolean? = null - val job = underTest.dataEnabled.onEach { latest = it }.launchIn(this) + assertThat(underTest.dataEnabled.value).isFalse() + } - assertThat(latest).isTrue() + @Test + fun dataEnabled_isEnabled_true() = + runBlocking(IMMEDIATE) { + whenever(telephonyManager.isDataConnectionAllowed).thenReturn(true) + val job = underTest.dataEnabled.launchIn(this) + + assertThat(underTest.dataEnabled.value).isTrue() job.cancel() } @@ -316,11 +322,9 @@ class MobileConnectionRepositoryTest : SysuiTestCase() { fun dataEnabled_isDisabled() = runBlocking(IMMEDIATE) { whenever(telephonyManager.isDataConnectionAllowed).thenReturn(false) + val job = underTest.dataEnabled.launchIn(this) - var latest: Boolean? = null - val job = underTest.dataEnabled.onEach { latest = it }.launchIn(this) - - assertThat(latest).isFalse() + assertThat(underTest.dataEnabled.value).isFalse() job.cancel() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt index 4d6ed2fc7493b..8dd7ef88a0fb6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt @@ -34,6 +34,7 @@ import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsPro import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -49,10 +50,13 @@ class MobileIconInteractorTest : SysuiTestCase() { private val mobileIconsInteractor = FakeMobileIconsInteractor(mobileMappingsProxy) private val connectionRepository = FakeMobileConnectionRepository() + private val scope = CoroutineScope(IMMEDIATE) + @Before fun setUp() { underTest = MobileIconInteractorImpl( + scope, mobileIconsInteractor.activeDataConnectionHasDataEnabled, mobileIconsInteractor.defaultMobileIconMapping, mobileIconsInteractor.defaultMobileIconGroup, From 09c154b291770f339624856ca50f821e39db8f48 Mon Sep 17 00:00:00 2001 From: Evan Laird Date: Wed, 26 Oct 2022 17:59:25 -0400 Subject: [PATCH 3/4] [SB refactor] Add connectivity tracking to the mobile pipeline This CL tracks 2 bits from ConnectivityManager for mobile networks: isConnected and isValidated. These bits are used to know whether or not cellular networks are the default transport, and whether or not connectivitymanager has validated that transport. Test: manual Test: atest packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/* Bug: 240492102 Change-Id: I13890cdc59c198c1b5290ae92e7ec4ba491999e3 --- .../data/model/MobileConnectivityModel.kt | 27 ++++++ .../repository/MobileConnectionsRepository.kt | 42 ++++++++ .../domain/interactor/MobileIconInteractor.kt | 5 +- .../interactor/MobileIconsInteractor.kt | 18 ++++ .../ui/viewmodel/MobileIconViewModel.kt | 10 +- .../FakeMobileConnectionsRepository.kt | 8 ++ .../MobileConnectionsRepositoryTest.kt | 97 +++++++++++++++++++ .../interactor/FakeMobileIconInteractor.kt | 7 ++ .../interactor/FakeMobileIconsInteractor.kt | 2 + .../interactor/MobileIconInteractorTest.kt | 15 +++ .../interactor/MobileIconsInteractorTest.kt | 42 ++++++++ .../ui/viewmodel/MobileIconViewModelTest.kt | 15 +++ 12 files changed, 283 insertions(+), 5 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectivityModel.kt diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectivityModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectivityModel.kt new file mode 100644 index 0000000000000..e61890523ebba --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectivityModel.kt @@ -0,0 +1,27 @@ +/* + * 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, +) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt index 89edc8a873b96..c3c1f1403c606 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt @@ -16,9 +16,16 @@ 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 @@ -37,6 +44,7 @@ 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 @@ -73,6 +81,9 @@ interface MobileConnectionsRepository { /** Tracks [SubscriptionManager.getDefaultDataSubscriptionId] */ val defaultDataSubId: StateFlow + /** The current connectivity status for the default mobile network connection */ + val defaultMobileNetworkConnectivity: StateFlow + /** Get or create a repository for the line of service for the given subscription ID */ fun getRepoForSubId(subId: Int): MobileConnectionRepository @@ -86,6 +97,7 @@ interface MobileConnectionsRepository { class MobileConnectionsRepositoryImpl @Inject constructor( + private val connectivityManager: ConnectivityManager, private val subscriptionManager: SubscriptionManager, private val telephonyManager: TelephonyManager, private val logger: ConnectivityPipelineLogger, @@ -212,6 +224,36 @@ constructor( awaitClose { context.contentResolver.unregisterContentObserver(observer) } } + @SuppressLint("MissingPermission") + override val defaultMobileNetworkConnectivity: StateFlow = + 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) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt index dd8a91fe58ed4..31dfb7ea7527c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt @@ -26,7 +26,6 @@ import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import com.android.systemui.util.CarrierConfigTracker import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -35,6 +34,9 @@ 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 + // TODO(b/256839546): clarify naming of default vs active /** True if we want to consider the data connection enabled */ val isDefaultDataEnabled: StateFlow @@ -63,6 +65,7 @@ class MobileIconInteractorImpl( defaultSubscriptionHasDataEnabled: StateFlow, defaultMobileIconMapping: StateFlow>, defaultMobileIconGroup: StateFlow, + override val isDefaultConnectionFailed: StateFlow, mobileMappingsProxy: MobileMappingsProxy, connectionRepository: MobileConnectionRepository, ) : MobileIconInteractor { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt index 9d30ec05aad31..a4175c3a6ab19 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt @@ -60,6 +60,8 @@ interface MobileIconsInteractor { val defaultMobileIconMapping: StateFlow> /** Fallback [MobileIconGroup] in the case where there is no icon in the mapping */ val defaultMobileIconGroup: StateFlow + /** True only if the default network is mobile, and validation also failed */ + val isDefaultConnectionFailed: StateFlow /** True once the user has been set up */ val isUserSetup: StateFlow /** @@ -162,6 +164,21 @@ constructor( .mapLatest { mobileMappingsProxy.getDefaultIcons(it) } .stateIn(scope, SharingStarted.WhileSubscribed(), initialValue = TelephonyIcons.G) + /** + * We want to show an error state when cellular has actually failed to validate, but not if some + * other transport type is active, because then we expect there not to be validation. + */ + override val isDefaultConnectionFailed: StateFlow = + mobileConnectionsRepo.defaultMobileNetworkConnectivity + .mapLatest { connectivityModel -> + if (!connectivityModel.isConnected) { + false + } else { + !connectivityModel.isValidated + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + override val isUserSetup: StateFlow = userSetupRepo.isUserSetupFlow /** Vends out new [MobileIconInteractor] for a particular subId */ @@ -171,6 +188,7 @@ constructor( activeDataConnectionHasDataEnabled, defaultMobileIconMapping, defaultMobileIconGroup, + isDefaultConnectionFailed, mobileMappingsProxy, mobileConnectionsRepo.getRepoForSubId(subId), ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt index b256665358d44..157b0252779c4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt @@ -66,10 +66,12 @@ constructor( /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */ val networkTypeIcon: Flow = - combine(iconInteractor.networkTypeIconGroup, iconInteractor.isDataEnabled) { - networkTypeIconGroup, - isDataEnabled -> - if (!isDataEnabled) { + combine( + iconInteractor.networkTypeIconGroup, + iconInteractor.isDataEnabled, + iconInteractor.isDefaultConnectionFailed + ) { networkTypeIconGroup, isDataEnabled, isFailedConnection -> + if (!isDataEnabled || isFailedConnection) { null } else { val desc = diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt index 8488effd22ee3..533d5d9d5b4a3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository import android.telephony.SubscriptionInfo import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import com.android.settingslib.mobile.MobileMappings.Config +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -35,6 +36,9 @@ class FakeMobileConnectionsRepository : MobileConnectionsRepository { private val _defaultDataSubId = MutableStateFlow(INVALID_SUBSCRIPTION_ID) override val defaultDataSubId = _defaultDataSubId + private val _mobileConnectivity = MutableStateFlow(MobileConnectivityModel()) + override val defaultMobileNetworkConnectivity = _mobileConnectivity + private val subIdRepos = mutableMapOf() override fun getRepoForSubId(subId: Int): MobileConnectionRepository { return subIdRepos[subId] ?: FakeMobileConnectionRepository().also { subIdRepos[subId] = it } @@ -55,6 +59,10 @@ class FakeMobileConnectionsRepository : MobileConnectionsRepository { _defaultDataSubId.value = id } + fun setMobileConnectivity(model: MobileConnectivityModel) { + _mobileConnectivity.value = model + } + suspend fun triggerGlobalMobileDataSettingChangedEvent() { _globalMobileDataSettingChangedEvent.emit(Unit) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt index 27c7357cb9958..a953a3d802e61 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryTest.kt @@ -17,6 +17,11 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository import android.content.Intent +import android.net.ConnectivityManager +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.telephony.SubscriptionInfo import android.telephony.SubscriptionManager @@ -26,6 +31,7 @@ import android.telephony.TelephonyManager import androidx.test.filters.SmallTest import com.android.internal.telephony.PhoneConstants import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor @@ -54,6 +60,7 @@ import org.mockito.MockitoAnnotations class MobileConnectionsRepositoryTest : SysuiTestCase() { private lateinit var underTest: MobileConnectionsRepositoryImpl + @Mock private lateinit var connectivityManager: ConnectivityManager @Mock private lateinit var subscriptionManager: SubscriptionManager @Mock private lateinit var telephonyManager: TelephonyManager @Mock private lateinit var logger: ConnectivityPipelineLogger @@ -67,6 +74,7 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { underTest = MobileConnectionsRepositoryImpl( + connectivityManager, subscriptionManager, telephonyManager, logger, @@ -235,6 +243,29 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { job.cancel() } + @Test + fun mobileConnectivity_default() { + assertThat(underTest.defaultMobileNetworkConnectivity.value) + .isEqualTo(MobileConnectivityModel(isConnected = false, isValidated = false)) + } + + @Test + fun mobileConnectivity_isConnected_isValidated() = + runBlocking(IMMEDIATE) { + val caps = createCapabilities(connected = true, validated = true) + + var latest: MobileConnectivityModel? = null + val job = + underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this) + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) + + assertThat(latest) + .isEqualTo(MobileConnectivityModel(isConnected = true, isValidated = true)) + + job.cancel() + } + @Test fun globalMobileDataSettingsChangedEvent_producesOnSettingChange() = runBlocking(IMMEDIATE) { @@ -253,6 +284,69 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { job.cancel() } + @Test + fun mobileConnectivity_isConnected_isNotValidated() = + runBlocking(IMMEDIATE) { + val caps = createCapabilities(connected = true, validated = false) + + var latest: MobileConnectivityModel? = null + val job = + underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this) + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) + + assertThat(latest) + .isEqualTo(MobileConnectivityModel(isConnected = true, isValidated = false)) + + job.cancel() + } + + @Test + fun mobileConnectivity_isNotConnected_isNotValidated() = + runBlocking(IMMEDIATE) { + val caps = createCapabilities(connected = false, validated = false) + + var latest: MobileConnectivityModel? = null + val job = + underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this) + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) + + assertThat(latest) + .isEqualTo(MobileConnectivityModel(isConnected = false, isValidated = false)) + + job.cancel() + } + + /** In practice, I don't think this state can ever happen (!connected, validated) */ + @Test + fun mobileConnectivity_isNotConnected_isValidated() = + runBlocking(IMMEDIATE) { + val caps = createCapabilities(connected = false, validated = true) + + var latest: MobileConnectivityModel? = null + val job = + underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this) + + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) + + assertThat(latest).isEqualTo(MobileConnectivityModel(false, true)) + + job.cancel() + } + + private fun createCapabilities(connected: Boolean, validated: Boolean): NetworkCapabilities = + mock().also { + whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(connected) + whenever(it.hasCapability(NET_CAPABILITY_VALIDATED)).thenReturn(validated) + } + + private fun getDefaultNetworkCallback(): ConnectivityManager.NetworkCallback { + val callbackCaptor = argumentCaptor() + verify(connectivityManager).registerDefaultNetworkCallback(callbackCaptor.capture()) + return callbackCaptor.value!! + } + private fun getSubscriptionCallback(): SubscriptionManager.OnSubscriptionsChangedListener { val callbackCaptor = argumentCaptor() verify(subscriptionManager) @@ -281,5 +375,8 @@ class MobileConnectionsRepositoryTest : SysuiTestCase() { private const val SUB_2_ID = 2 private val SUB_2 = mock().also { whenever(it.subscriptionId).thenReturn(SUB_2_ID) } + + private const val NET_ID = 123 + private val NETWORK = mock().apply { whenever(getNetId()).thenReturn(NET_ID) } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt index ea3faa0dff1aa..b05a284b864cd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt @@ -28,6 +28,9 @@ class FakeMobileIconInteractor : MobileIconInteractor { private val _isEmergencyOnly = MutableStateFlow(false) override val isEmergencyOnly = _isEmergencyOnly + private val _isFailedConnection = MutableStateFlow(false) + override val isDefaultConnectionFailed = _isFailedConnection + private val _isDataEnabled = MutableStateFlow(true) override val isDataEnabled = _isDataEnabled @@ -56,6 +59,10 @@ class FakeMobileIconInteractor : MobileIconInteractor { _isDefaultDataEnabled.value = disabled } + fun setIsFailedConnection(failed: Boolean) { + _isFailedConnection.value = failed + } + fun setLevel(level: Int) { _level.value = level } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt index cf95bf736bab1..061c3b54650e6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt @@ -45,6 +45,8 @@ class FakeMobileIconsInteractor(mobileMappings: MobileMappingsProxy) : MobileIco FIVE_G_OVERRIDE_KEY to TelephonyIcons.NR_5G, ) + override val isDefaultConnectionFailed = MutableStateFlow(false) + private val _filteredSubscriptions = MutableStateFlow>(listOf()) override val filteredSubscriptions = _filteredSubscriptions diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt index 8dd7ef88a0fb6..ddc0bc78db9f6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt @@ -60,6 +60,7 @@ class MobileIconInteractorTest : SysuiTestCase() { mobileIconsInteractor.activeDataConnectionHasDataEnabled, mobileIconsInteractor.defaultMobileIconMapping, mobileIconsInteractor.defaultMobileIconGroup, + mobileIconsInteractor.isDefaultConnectionFailed, mobileMappingsProxy, connectionRepository, ) @@ -216,6 +217,20 @@ class MobileIconInteractorTest : SysuiTestCase() { job.cancel() } + @Test + fun test_isDefaultConnectionFailed_matchedParent() = + runBlocking(IMMEDIATE) { + val job = underTest.isDefaultConnectionFailed.launchIn(this) + + mobileIconsInteractor.isDefaultConnectionFailed.value = false + assertThat(underTest.isDefaultConnectionFailed.value).isFalse() + + mobileIconsInteractor.isDefaultConnectionFailed.value = true + assertThat(underTest.isDefaultConnectionFailed.value).isTrue() + + job.cancel() + } + companion object { private val IMMEDIATE = Dispatchers.Main.immediate diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt index 799d0a3f1cc72..b56dcd7525579 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt @@ -20,6 +20,7 @@ import android.telephony.SubscriptionInfo import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeUserSetupRepository @@ -215,6 +216,47 @@ class MobileIconsInteractorTest : SysuiTestCase() { job.cancel() } + @Test + fun failedConnection_connected_validated_notFailed() = + runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this) + connectionsRepository.setMobileConnectivity(MobileConnectivityModel(true, true)) + yield() + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun failedConnection_notConnected_notValidated_notFailed() = + runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this) + + connectionsRepository.setMobileConnectivity(MobileConnectivityModel(false, false)) + yield() + + assertThat(latest).isFalse() + + job.cancel() + } + + @Test + fun failedConnection_connected_notValidated_failed() = + runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this) + + connectionsRepository.setMobileConnectivity(MobileConnectivityModel(true, false)) + yield() + + assertThat(latest).isTrue() + + job.cancel() + } + companion object { private val IMMEDIATE = Dispatchers.Main.immediate diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt index da0bf5c274856..4eecbb9835ed8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt @@ -47,6 +47,7 @@ class MobileIconViewModelTest : SysuiTestCase() { interactor.apply { setLevel(1) setIsDefaultDataEnabled(true) + setIsFailedConnection(false) setIconGroup(THREE_G) setIsEmergencyOnly(false) setNumberOfLevels(4) @@ -111,6 +112,20 @@ class MobileIconViewModelTest : SysuiTestCase() { job.cancel() } + @Test + fun networkType_nullWhenFailedConnection() = + runBlocking(IMMEDIATE) { + interactor.setIconGroup(THREE_G) + interactor.setIsDataEnabled(true) + interactor.setIsFailedConnection(true) + var latest: Icon? = null + val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) + + assertThat(latest).isNull() + + job.cancel() + } + @Test fun networkType_null_changeToDisabled() = runBlocking(IMMEDIATE) { From 172b95c22c40a9f3de9943c771c60b2ba5c05d9f Mon Sep 17 00:00:00 2001 From: Evan Laird Date: Thu, 27 Oct 2022 13:03:44 -0400 Subject: [PATCH 4/4] [SB refactor] Wire up DataConnectionState to the view model DataConnectionState is used to help decide whether or not to show the RAT icon. This CL wires it up to the interactor + view model Test: atest packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/* Bug: 240492102 Change-Id: I37ab77674319757b954c469da6b73a4a2876dadd --- .../domain/interactor/MobileIconInteractor.kt | 9 ++++++ .../ui/viewmodel/MobileIconViewModel.kt | 7 ++-- .../interactor/FakeMobileIconInteractor.kt | 2 ++ .../interactor/MobileIconInteractorTest.kt | 32 +++++++++++++++++++ .../ui/viewmodel/MobileIconViewModelTest.kt | 25 +++++++++++++++ 5 files changed, 72 insertions(+), 3 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt index 31dfb7ea7527c..0da84f0bec9cd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt @@ -19,6 +19,7 @@ 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 @@ -37,6 +38,9 @@ interface MobileIconInteractor { /** Only true if mobile is the default transport but is not validated, otherwise false */ val isDefaultConnectionFailed: StateFlow + /** True when telephony tells us that the data state is CONNECTED */ + val isDataConnected: StateFlow + // TODO(b/256839546): clarify naming of default vs active /** True if we want to consider the data connection enabled */ val isDefaultDataEnabled: StateFlow @@ -114,4 +118,9 @@ class MobileIconInteractorImpl( * once it's wired up inside of [CarrierConfigTracker] */ override val numberOfLevels: StateFlow = MutableStateFlow(4) + + override val isDataConnected: StateFlow = + mobileStatusInfo + .mapLatest { subscriptionModel -> subscriptionModel.dataConnectionState == Connected } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt index 157b0252779c4..7869021c05016 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt @@ -68,10 +68,11 @@ constructor( val networkTypeIcon: Flow = combine( iconInteractor.networkTypeIconGroup, + iconInteractor.isDataConnected, iconInteractor.isDataEnabled, - iconInteractor.isDefaultConnectionFailed - ) { networkTypeIconGroup, isDataEnabled, isFailedConnection -> - if (!isDataEnabled || isFailedConnection) { + iconInteractor.isDefaultConnectionFailed, + ) { networkTypeIconGroup, dataConnected, dataEnabled, failedConnection -> + if (!dataConnected || !dataEnabled || failedConnection) { null } else { val desc = diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt index b05a284b864cd..3ae7d3ca1c196 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt @@ -31,6 +31,8 @@ class FakeMobileIconInteractor : MobileIconInteractor { private val _isFailedConnection = MutableStateFlow(false) override val isDefaultConnectionFailed = _isFailedConnection + override val isDataConnected = MutableStateFlow(true) + private val _isDataEnabled = MutableStateFlow(true) override val isDataEnabled = _isDataEnabled diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt index ddc0bc78db9f6..7fc1c0f6272cd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt @@ -23,6 +23,7 @@ import androidx.test.filters.SmallTest import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState import com.android.systemui.statusbar.pipeline.mobile.data.model.DefaultNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileSubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.model.OverrideNetworkType @@ -231,6 +232,37 @@ class MobileIconInteractorTest : SysuiTestCase() { job.cancel() } + @Test + fun dataState_connected() = + runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest.isDataConnected.onEach { latest = it }.launchIn(this) + + connectionRepository.setMobileSubscriptionModel( + MobileSubscriptionModel(dataConnectionState = DataConnectionState.Connected) + ) + yield() + + assertThat(latest).isTrue() + + job.cancel() + } + + @Test + fun dataState_notConnected() = + runBlocking(IMMEDIATE) { + var latest: Boolean? = null + val job = underTest.isDataConnected.onEach { latest = it }.launchIn(this) + + connectionRepository.setMobileSubscriptionModel( + MobileSubscriptionModel(dataConnectionState = DataConnectionState.Disconnected) + ) + + assertThat(latest).isFalse() + + job.cancel() + } + companion object { private val IMMEDIATE = Dispatchers.Main.immediate diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt index 4eecbb9835ed8..d4c2c3f6cc2bc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt @@ -51,6 +51,7 @@ class MobileIconViewModelTest : SysuiTestCase() { setIconGroup(THREE_G) setIsEmergencyOnly(false) setNumberOfLevels(4) + isDataConnected.value = true } underTest = MobileIconViewModel(SUB_1_ID, interactor, logger) } @@ -126,6 +127,30 @@ class MobileIconViewModelTest : SysuiTestCase() { job.cancel() } + @Test + fun networkType_nullWhenDataDisconnects() = + runBlocking(IMMEDIATE) { + val initial = + Icon.Resource( + THREE_G.dataType, + ContentDescription.Resource(THREE_G.dataContentDescription) + ) + + interactor.setIconGroup(THREE_G) + var latest: Icon? = null + val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) + + interactor.setIconGroup(THREE_G) + assertThat(latest).isEqualTo(initial) + + interactor.isDataConnected.value = false + yield() + + assertThat(latest).isNull() + + job.cancel() + } + @Test fun networkType_null_changeToDisabled() = runBlocking(IMMEDIATE) {