diff options
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 06e8f467ee0b..41bcab1be2d5 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<MobileSubscriptionModel> /** Observable tracking [TelephonyManager.isDataConnectionAllowed] */ val dataEnabled: Flow<Boolean> + /** + * True if this connection represents the default subscription per + * [SubscriptionManager.getDefaultDataSubscriptionId] + */ + val isDefaultDataSubscription: Flow<Boolean> } @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @OptIn(ExperimentalCoroutinesApi::class) class MobileConnectionRepositoryImpl( + private val context: Context, private val subId: Int, private val telephonyManager: TelephonyManager, + private val globalSettings: GlobalSettings, + defaultDataSubId: Flow<Int>, + globalMobileDataSettingChangedEvent: Flow<Unit>, bgDispatcher: CoroutineDispatcher, logger: ConnectivityPipelineLogger, scope: CoroutineScope, @@ -169,29 +184,66 @@ class MobileConnectionRepositoryImpl( .stateIn(scope, SharingStarted.WhileSubscribed(), state) } + private val telephonyCallbackEvent = subscriptionModelFlow.map {} + + /** Produces whenever the mobile data setting changes for this subId */ + private val localMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow { + val observer = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + trySend(Unit) + } + } + + globalSettings.registerContentObserver( + globalSettings.getUriFor("${Global.MOBILE_DATA}$subId"), + /* notifyForDescendants */ true, + observer + ) + + awaitClose { context.contentResolver.unregisterContentObserver(observer) } + } + /** * There are a few cases where we will need to poll [TelephonyManager] so we can update some * internal state where callbacks aren't provided. Any of those events should be merged into * this flow, which can be used to trigger the polling. */ - private val telephonyPollingEvent: Flow<Unit> = subscriptionModelFlow.map {} + private val telephonyPollingEvent: Flow<Unit> = + merge( + telephonyCallbackEvent, + localMobileDataSettingChangedEvent, + globalMobileDataSettingChangedEvent, + ) override val dataEnabled: Flow<Boolean> = telephonyPollingEvent.map { dataConnectionAllowed() } private fun dataConnectionAllowed(): Boolean = telephonyManager.isDataConnectionAllowed + override val isDefaultDataSubscription: Flow<Boolean> = defaultDataSubId.map { it == subId } + class Factory @Inject constructor( + private val context: Context, private val telephonyManager: TelephonyManager, private val logger: ConnectivityPipelineLogger, + private val globalSettings: GlobalSettings, @Background private val bgDispatcher: CoroutineDispatcher, @Application private val scope: CoroutineScope, ) { - fun build(subId: Int): MobileConnectionRepository { + fun build( + subId: Int, + defaultDataSubId: Flow<Int>, + globalMobileDataSettingChangedEvent: Flow<Unit>, + ): MobileConnectionRepository { return MobileConnectionRepositoryImpl( + context, subId, telephonyManager.createForSubscriptionId(subId), + globalSettings, + defaultDataSubId, + globalMobileDataSettingChangedEvent, bgDispatcher, logger, scope, 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 0e2428ae393a..bd9f85ee9dcb 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<Config> + /** Tracks [SubscriptionManager.getDefaultDataSubscriptionId] */ + val defaultDataSubId: StateFlow<Int> + /** Get or create a repository for the line of service for the given subscription ID */ fun getRepoForSubId(subId: Int): MobileConnectionRepository + + /** Observe changes to the [Settings.Global.MOBILE_DATA] setting */ + val globalMobileDataSettingChangedEvent: Flow<Unit> } @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @@ -76,6 +90,7 @@ constructor( private val telephonyManager: TelephonyManager, private val logger: ConnectivityPipelineLogger, broadcastDispatcher: BroadcastDispatcher, + private val globalSettings: GlobalSettings, private val context: Context, @Background private val bgDispatcher: CoroutineDispatcher, @Application private val scope: CoroutineScope, @@ -121,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<Unit> = + MutableSharedFlow(extraBufferCapacity = 1) + + override val defaultDataSubId: StateFlow<Int> = + broadcastDispatcher + .broadcastFlow( + IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED) + ) { intent, _ -> + intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID) + } + .distinctUntilChanged() + .onEach { defaultDataSubIdChangeEvent.tryEmit(Unit) } .stateIn( scope, - 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<Config> = - combine(defaultDataSubChangedEvent, carrierConfigChangedEvent) { _, _ -> - Config.readConfig(context) - } + merge(defaultDataSubIdChangeEvent, carrierConfigChangedEvent) + .mapLatest { Config.readConfig(context) } .stateIn( scope, SharingStarted.WhileSubscribed(), @@ -168,6 +191,27 @@ constructor( ?: createRepositoryForSubId(subId).also { subIdRepositoryCache[subId] = it } } + /** + * In single-SIM devices, the [MOBILE_DATA] setting is phone-wide. For multi-SIM, the individual + * connection repositories also observe the URI for [MOBILE_DATA] + subId. + */ + override val globalMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow { + val observer = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + trySend(Unit) + } + } + + globalSettings.registerContentObserver( + globalSettings.getUriFor(MOBILE_DATA), + true, + observer + ) + + awaitClose { context.contentResolver.unregisterContentObserver(observer) } + } + private fun isValidSubId(subId: Int): Boolean { subscriptionsFlow.value.forEach { if (it.subscriptionId == subId) { @@ -181,7 +225,11 @@ constructor( @VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache private fun createRepositoryForSubId(subId: Int): MobileConnectionRepository { - return mobileConnectionRepositoryFactory.build(subId) + return mobileConnectionRepositoryFactory.build( + subId, + defaultDataSubId, + globalMobileDataSettingChangedEvent, + ) } private fun dropUnusedReposFromCache(newInfos: List<SubscriptionInfo>) { 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 f99d278c3903..cdd4982f0a79 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<Boolean> + /** Observable for the data enabled state of this connection */ val isDataEnabled: Flow<Boolean> @@ -43,13 +47,11 @@ interface MobileIconInteractor { /** Based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL], either 4 or 5 */ val numberOfLevels: Flow<Int> - - /** True when we want to draw an icon that makes room for the exclamation mark */ - val cutOut: Flow<Boolean> } /** Interactor for a single mobile connection. This connection _should_ have one subscription ID */ class MobileIconInteractorImpl( + defaultSubscriptionHasDataEnabled: Flow<Boolean>, defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>>, defaultMobileIconGroup: Flow<MobileIconGroup>, mobileMappingsProxy: MobileMappingsProxy, @@ -59,6 +61,8 @@ class MobileIconInteractorImpl( override val isDataEnabled: Flow<Boolean> = connectionRepository.dataEnabled + override val isDefaultDataEnabled = defaultSubscriptionHasDataEnabled + /** Observable for the current RAT indicator icon ([MobileIconGroup]) */ override val networkTypeIconGroup: Flow<MobileIconGroup> = combine( @@ -91,8 +95,4 @@ class MobileIconInteractorImpl( * once it's wired up inside of [CarrierConfigTracker] */ override val numberOfLevels: Flow<Int> = flowOf(4) - - /** Whether or not to draw the mobile triangle as "cut out", i.e., with the exclamation mark */ - // TODO: find a better name for this? - override val cutOut: Flow<Boolean> = flowOf(false) } 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 614d583c3c48..31cb21f7720f 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<List<SubscriptionInfo>> + /** True if the active mobile data subscription has data enabled */ + val activeDataConnectionHasDataEnabled: Flow<Boolean> /** The icon mapping from network type to [MobileIconGroup] for the default subscription */ val defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>> /** Fallback [MobileIconGroup] in the case where there is no icon in the mapping */ @@ -79,6 +84,18 @@ constructor( private val activeMobileDataSubscriptionId = mobileConnectionsRepo.activeMobileDataSubscriptionId + private val activeMobileDataConnectionRepo: Flow<MobileConnectionRepository?> = + activeMobileDataSubscriptionId.map { activeId -> + if (activeId == INVALID_SUBSCRIPTION_ID) { + null + } else { + mobileConnectionsRepo.getRepoForSubId(activeId) + } + } + + override val activeDataConnectionHasDataEnabled: Flow<Boolean> = + activeMobileDataConnectionRepo.flatMapLatest { it?.dataEnabled ?: flowOf(false) } + private val unfilteredSubscriptions: Flow<List<SubscriptionInfo>> = mobileConnectionsRepo.subscriptionsFlow @@ -146,6 +163,7 @@ constructor( /** Vends out new [MobileIconInteractor] for a particular subId */ override fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor = MobileIconInteractorImpl( + activeDataConnectionHasDataEnabled, defaultMobileIconMapping, defaultMobileIconGroup, mobileMappingsProxy, 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 81317398f086..b256665358d4 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<Boolean> = + iconInteractor.isDefaultDataEnabled.mapLatest { !it } + /** An int consumable by [SignalDrawable] for display */ - var iconId: Flow<Int> = - combine(iconInteractor.level, iconInteractor.numberOfLevels, iconInteractor.cutOut) { + val iconId: Flow<Int> = + combine(iconInteractor.level, iconInteractor.numberOfLevels, showExclamationMark) { level, numberOfLevels, - cutOut -> - SignalDrawable.getState(level, numberOfLevels, cutOut) + showExclamationMark -> + SignalDrawable.getState(level, numberOfLevels, showExclamationMark) } .distinctUntilChanged() .logOutputChange(logger, "iconId($subscriptionId)") /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */ - var networkTypeIcon: Flow<Icon?> = + val networkTypeIcon: Flow<Icon?> = combine(iconInteractor.networkTypeIconGroup, iconInteractor.isDataEnabled) { networkTypeIconGroup, isDataEnabled -> @@ -72,5 +80,5 @@ constructor( } } - var tint: Flow<Int> = flowOf(Color.CYAN) + val tint: Flow<Int> = flowOf(Color.CYAN) } 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 de1fec85360b..4c7f7eab981d 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 813e750684a0..8488effd22ee 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<List<SubscriptionInfo>>(listOf()) override val subscriptionsFlow: Flow<List<SubscriptionInfo>> = _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<Int, MobileConnectionRepository>() 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<SubscriptionInfo>) { _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 093936444789..ed3eda5beaf3 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<TelephonyCallback> { val callbackCaptor = argumentCaptor<TelephonyCallback>() 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 326e0d28166f..27c7357cb995 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<SubscriptionManager.OnSubscriptionsChangedListener>() 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 5611c448c550..ea3faa0dff1a 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 2bd228603cb0..cf95bf736bab 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<List<SubscriptionInfo>>(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 ff44af4c9204..4d6ed2fc7493 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 877ce0e6b351..799d0a3f1cc7 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 ce0f33f400ab..da0bf5c27485 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 |