[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
This commit is contained in:
Evan Laird
2022-10-24 16:42:15 -04:00
parent 4046d6b3c6
commit be25e8f1f4
14 changed files with 390 additions and 56 deletions

View File

@@ -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,

View File

@@ -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>) {

View File

@@ -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)
}

View File

@@ -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,

View File

@@ -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)
}

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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())

View File

@@ -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)

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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