Merge changes I1c9ade7e,I06365d19,I0945a29d,I8e798197,Ib3a4f14b, ... into tm-qpr-dev

* changes:
  [Sb refactor] [demo] Use better default for demo mode
  [Sb refactor] Support NOT_DEFAULT_DATA network type
  Partial revert of "[SB Refactor] Remove unused `isDefaultDataSubscription` flow."
  [Sb refactor] Implement 2s grace period for data switching
  [Sb refactor] Add the default network's connectivity to the view model
  [Sb refactor] Upgrade MobileIconsInteractorTest to use testScope
This commit is contained in:
Evan Laird
2023-01-17 21:51:57 +00:00
committed by Android (Google) Code Review
15 changed files with 615 additions and 39 deletions

View File

@@ -18,6 +18,7 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository
import android.provider.Settings
import android.telephony.CarrierConfigManager
import android.telephony.SubscriptionManager
import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.settingslib.mobile.MobileMappings
import com.android.settingslib.mobile.MobileMappings.Config
@@ -37,6 +38,15 @@ interface MobileConnectionsRepository {
/** Observable for the subscriptionId of the current mobile data connection */
val activeMobileDataSubscriptionId: StateFlow<Int>
/**
* Observable event for when the active data sim switches but the group stays the same. E.g.,
* CBRS switching would trigger this
*/
val activeSubChangedInGroupEvent: Flow<Unit>
/** Tracks [SubscriptionManager.getDefaultDataSubscriptionId] */
val defaultDataSubId: StateFlow<Int>
/** The current connectivity status for the default mobile network connection */
val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel>

View File

@@ -124,6 +124,9 @@ constructor(
realRepository.activeMobileDataSubscriptionId.value
)
override val activeSubChangedInGroupEvent: Flow<Unit> =
activeRepo.flatMapLatest { it.activeSubChangedInGroupEvent }
override val defaultDataSubRatConfig: StateFlow<MobileMappings.Config> =
activeRepo
.flatMapLatest { it.defaultDataSubRatConfig }
@@ -139,6 +142,11 @@ constructor(
override val defaultMobileIconGroup: Flow<SignalIcon.MobileIconGroup> =
activeRepo.flatMapLatest { it.defaultMobileIconGroup }
override val defaultDataSubId: StateFlow<Int> =
activeRepo
.flatMapLatest { it.defaultDataSubId }
.stateIn(scope, SharingStarted.WhileSubscribed(), realRepository.defaultDataSubId.value)
override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> =
activeRepo
.flatMapLatest { it.defaultMobileNetworkConnectivity }

View File

@@ -48,6 +48,7 @@ import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -120,6 +121,9 @@ constructor(
subscriptions.value.firstOrNull()?.subscriptionId ?: INVALID_SUBSCRIPTION_ID
)
// TODO(b/261029387): consider adding a demo command for this
override val activeSubChangedInGroupEvent: Flow<Unit> = flowOf()
/** Demo mode doesn't currently support modifications to the mobile mappings */
override val defaultDataSubRatConfig =
MutableStateFlow(MobileMappings.Config.readConfig(context))
@@ -148,8 +152,12 @@ constructor(
private fun <K, V> Map<K, V>.reverse() = entries.associateBy({ it.value }) { it.key }
// TODO(b/261029387): add a command for this value
override val defaultDataSubId = MutableStateFlow(INVALID_SUBSCRIPTION_ID)
// TODO(b/261029387): not yet supported
override val defaultMobileNetworkConnectivity = MutableStateFlow(MobileConnectivityModel())
override val defaultMobileNetworkConnectivity =
MutableStateFlow(MobileConnectivityModel(isConnected = true, isValidated = true))
override fun getRepoForSubId(subId: Int): DemoMobileConnectionRepository {
val current = connectionRepoCache[subId]?.repo
@@ -229,6 +237,9 @@ constructor(
val connection = getRepoForSubId(subId)
connectionRepoCache[subId]?.lastMobileState = state
// TODO(b/261029387): until we have a command, use the most recent subId
defaultDataSubId.value = subId
// This is always true here, because we split out disabled states at the data-source level
connection.dataEnabled.value = true
connection.networkName.value = NetworkNameModel.Derived(state.name)

View File

@@ -35,6 +35,7 @@ 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.SignalIcon.MobileIconGroup
import com.android.settingslib.mobile.MobileMappings.Config
import com.android.systemui.R
@@ -52,6 +53,7 @@ import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
import com.android.systemui.util.kotlin.pairwiseBy
import com.android.systemui.util.settings.GlobalSettings
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
@@ -60,9 +62,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.distinctUntilChanged
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.merge
@@ -159,10 +164,24 @@ constructor(
.logInputChange(logger, "onActiveDataSubscriptionIdChanged")
.stateIn(scope, started = SharingStarted.WhileSubscribed(), INVALID_SUBSCRIPTION_ID)
private val defaultDataSubIdChangedEvent =
private val defaultDataSubIdChangeEvent: MutableSharedFlow<Unit> =
MutableSharedFlow(extraBufferCapacity = 1)
override val defaultDataSubId: StateFlow<Int> =
broadcastDispatcher
.broadcastFlow(IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED))
.broadcastFlow(
IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
) { intent, _ ->
intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID)
}
.distinctUntilChanged()
.logInputChange(logger, "ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED")
.onEach { defaultDataSubIdChangeEvent.tryEmit(Unit) }
.stateIn(
scope,
SharingStarted.WhileSubscribed(),
SubscriptionManager.getDefaultDataSubscriptionId()
)
private val carrierConfigChangedEvent =
broadcastDispatcher
@@ -170,7 +189,7 @@ constructor(
.logInputChange(logger, "ACTION_CARRIER_CONFIG_CHANGED")
override val defaultDataSubRatConfig: StateFlow<Config> =
merge(defaultDataSubIdChangedEvent, carrierConfigChangedEvent)
merge(defaultDataSubIdChangeEvent, carrierConfigChangedEvent)
.mapLatest { Config.readConfig(context) }
.distinctUntilChanged()
.logInputChange(logger, "defaultDataSubRatConfig")
@@ -258,6 +277,35 @@ constructor(
.logInputChange(logger, "defaultMobileNetworkConnectivity")
.stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectivityModel())
/**
* Flow that tracks the active mobile data subscriptions. Emits `true` whenever the active data
* subscription Id changes but the subscription group remains the same. In these cases, we want
* to retain the previous subscription's validation status for up to 2s to avoid flickering the
* icon.
*
* TODO(b/265164432): we should probably expose all change events, not just same group
*/
@SuppressLint("MissingPermission")
override val activeSubChangedInGroupEvent =
flow {
activeMobileDataSubscriptionId.pairwiseBy { prevVal: Int, newVal: Int ->
if (!defaultMobileNetworkConnectivity.value.isValidated) {
return@pairwiseBy
}
val prevSub = subscriptionManager.getActiveSubscriptionInfo(prevVal)
val nextSub = subscriptionManager.getActiveSubscriptionInfo(newVal)
if (prevSub == null || nextSub == null) {
return@pairwiseBy
}
if (prevSub.groupUuid != null && prevSub.groupUuid == nextSub.groupUuid) {
emit(Unit)
}
}
}
.flowOn(bgDispatcher)
private fun isValidSubId(subId: Int): Boolean {
subscriptions.value.forEach {
if (it.subscriptionId == subId) {

View File

@@ -18,9 +18,11 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor
import android.telephony.CarrierConfigManager
import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.settingslib.mobile.TelephonyIcons.NOT_DEFAULT_DATA
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Connected
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
@@ -41,12 +43,29 @@ interface MobileIconInteractor {
/** The current mobile data activity */
val activity: Flow<DataActivityModel>
/**
* This bit is meant to be `true` if and only if the default network capabilities (see
* [android.net.ConnectivityManager.registerDefaultNetworkCallback]) result in a network that
* has the [android.net.NetworkCapabilities.TRANSPORT_CELLULAR] represented.
*
* Note that this differs from [isDataConnected], which is tracked by telephony and has to do
* with the state of using this mobile connection for data as opposed to just voice. It is
* possible for a mobile subscription to be connected but not be in a connected data state, and
* thus we wouldn't want to show the network type icon.
*/
val isConnected: Flow<Boolean>
/**
* True when telephony tells us that the data state is CONNECTED. See
* [android.telephony.TelephonyCallback.DataConnectionStateListener] for more details. We
* consider this connection to be serving data, and thus want to show a network type icon, when
* data is connected. Other data connection states would typically cause us not to show the icon
*/
val isDataConnected: StateFlow<Boolean>
/** Only true if mobile is the default transport but is not validated, otherwise false */
val isDefaultConnectionFailed: StateFlow<Boolean>
/** True when telephony tells us that the data state is CONNECTED */
val isDataConnected: StateFlow<Boolean>
/** True if we consider this connection to be in service, i.e. can make calls */
val isInService: StateFlow<Boolean>
@@ -100,8 +119,10 @@ class MobileIconInteractorImpl(
defaultSubscriptionHasDataEnabled: StateFlow<Boolean>,
override val alwaysShowDataRatIcon: StateFlow<Boolean>,
override val alwaysUseCdmaLevel: StateFlow<Boolean>,
defaultMobileConnectivity: StateFlow<MobileConnectivityModel>,
defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>>,
defaultMobileIconGroup: StateFlow<MobileIconGroup>,
defaultDataSubId: StateFlow<Int>,
override val isDefaultConnectionFailed: StateFlow<Boolean>,
connectionRepository: MobileConnectionRepository,
) : MobileIconInteractor {
@@ -111,8 +132,19 @@ class MobileIconInteractorImpl(
override val activity = connectionInfo.mapLatest { it.dataActivityDirection }
override val isConnected: Flow<Boolean> = defaultMobileConnectivity.mapLatest { it.isConnected }
override val isDataEnabled: StateFlow<Boolean> = connectionRepository.dataEnabled
private val isDefault =
defaultDataSubId
.mapLatest { connectionRepository.subId == it }
.stateIn(
scope,
SharingStarted.WhileSubscribed(),
connectionRepository.subId == defaultDataSubId.value
)
override val isDefaultDataEnabled = defaultSubscriptionHasDataEnabled
override val networkName =
@@ -137,7 +169,12 @@ class MobileIconInteractorImpl(
connectionInfo,
defaultMobileIconMapping,
defaultMobileIconGroup,
) { info, mapping, defaultGroup ->
isDefault,
) { info, mapping, defaultGroup, isDefault ->
if (!isDefault) {
return@combine NOT_DEFAULT_DATA
}
when (info.resolvedNetworkType) {
is ResolvedNetworkType.CarrierMergedNetworkType ->
info.resolvedNetworkType.iconGroupOverride

View File

@@ -23,6 +23,7 @@ import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.settingslib.mobile.TelephonyIcons
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
@@ -31,14 +32,17 @@ import com.android.systemui.util.CarrierConfigTracker
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
/**
* Business layer logic for the set of mobile subscription icons.
@@ -62,6 +66,17 @@ interface MobileIconsInteractor {
/** True if the CDMA level should be preferred over the primary level. */
val alwaysUseCdmaLevel: StateFlow<Boolean>
/** Tracks the subscriptionId set as the default for data connections */
val defaultDataSubId: StateFlow<Int>
/**
* The connectivity of the default mobile network. Note that this can differ from what is
* reported from [MobileConnectionsRepository] in some cases. E.g., when the active subscription
* changes but the groupUuid remains the same, we keep the old validation information for 2
* seconds to avoid icon flickering.
*/
val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel>
/** The icon mapping from network type to [MobileIconGroup] for the default subscription */
val defaultMobileIconMapping: StateFlow<Map<String, MobileIconGroup>>
/** Fallback [MobileIconGroup] in the case where there is no icon in the mapping */
@@ -154,6 +169,48 @@ constructor(
}
}
override val defaultDataSubId = mobileConnectionsRepo.defaultDataSubId
/**
* Copied from the old pipeline. We maintain a 2s period of time where we will keep the
* validated bit from the old active network (A) while data is changing to the new one (B).
*
* This condition only applies if
* 1. A and B are in the same subscription group (e.c. for CBRS data switching) and
* 2. A was validated before the switch
*
* The goal of this is to minimize the flickering in the UI of the cellular indicator
*/
private val forcingCellularValidation =
mobileConnectionsRepo.activeSubChangedInGroupEvent
.filter { mobileConnectionsRepo.defaultMobileNetworkConnectivity.value.isValidated }
.transformLatest {
emit(true)
delay(2000)
emit(false)
}
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> =
combine(
mobileConnectionsRepo.defaultMobileNetworkConnectivity,
forcingCellularValidation,
) { networkConnectivity, forceValidation ->
return@combine if (forceValidation) {
MobileConnectivityModel(
isValidated = true,
isConnected = networkConnectivity.isConnected
)
} else {
networkConnectivity
}
}
.stateIn(
scope,
SharingStarted.WhileSubscribed(),
mobileConnectionsRepo.defaultMobileNetworkConnectivity.value
)
/**
* Mapping from network type to [MobileIconGroup] using the config generated for the default
* subscription Id. This mapping is the same for every subscription.
@@ -207,8 +264,10 @@ constructor(
activeDataConnectionHasDataEnabled,
alwaysShowDataRatIcon,
alwaysUseCdmaLevel,
defaultMobileNetworkConnectivity,
defaultMobileIconMapping,
defaultMobileIconGroup,
defaultDataSubId,
isDefaultConnectionFailed,
mobileConnectionsRepo.getRepoForSubId(subId),
)

View File

@@ -102,24 +102,29 @@ constructor(
.stateIn(scope, SharingStarted.WhileSubscribed(), initial)
}
private val showNetworkTypeIcon: Flow<Boolean> =
combine(
iconInteractor.isDataConnected,
iconInteractor.isDataEnabled,
iconInteractor.isDefaultConnectionFailed,
iconInteractor.alwaysShowDataRatIcon,
iconInteractor.isConnected,
) { dataConnected, dataEnabled, failedConnection, alwaysShow, connected ->
alwaysShow || (dataConnected && dataEnabled && !failedConnection && connected)
}
override val networkTypeIcon: Flow<Icon?> =
combine(
iconInteractor.networkTypeIconGroup,
iconInteractor.isDataConnected,
iconInteractor.isDataEnabled,
iconInteractor.isDefaultConnectionFailed,
iconInteractor.alwaysShowDataRatIcon,
) { networkTypeIconGroup, dataConnected, dataEnabled, failedConnection, alwaysShow ->
showNetworkTypeIcon,
) { networkTypeIconGroup, shouldShow ->
val desc =
if (networkTypeIconGroup.dataContentDescription != 0)
ContentDescription.Resource(networkTypeIconGroup.dataContentDescription)
else null
val icon = Icon.Resource(networkTypeIconGroup.dataType, desc)
return@combine when {
alwaysShow -> icon
!dataConnected -> null
!dataEnabled -> null
failedConnection -> null
!shouldShow -> null
else -> icon
}
}

View File

@@ -26,6 +26,7 @@ import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
// TODO(b/261632894): remove this in favor of the real impl or DemoMobileConnectionsRepository
@@ -56,6 +57,10 @@ class FakeMobileConnectionsRepository(
private val _activeMobileDataSubscriptionId = MutableStateFlow(INVALID_SUBSCRIPTION_ID)
override val activeMobileDataSubscriptionId = _activeMobileDataSubscriptionId
override val activeSubChangedInGroupEvent: MutableSharedFlow<Unit> = MutableSharedFlow()
private val _defaultDataSubId = MutableStateFlow(INVALID_SUBSCRIPTION_ID)
override val defaultDataSubId = _defaultDataSubId
private val _mobileConnectivity = MutableStateFlow(MobileConnectivityModel())
override val defaultMobileNetworkConnectivity = _mobileConnectivity
@@ -81,6 +86,10 @@ class FakeMobileConnectionsRepository(
_subscriptions.value = subs
}
fun setDefaultDataSubId(id: Int) {
_defaultDataSubId.value = id
}
fun setMobileConnectivity(model: MobileConnectivityModel) {
_mobileConnectivity.value = model
}

View File

@@ -89,6 +89,14 @@ class DemoMobileConnectionsRepositoryTest : SysuiTestCase() {
underTest.startProcessingCommands()
}
@Test
fun `connectivity - defaults to connected and validated`() =
testScope.runTest {
val connectivity = underTest.defaultMobileNetworkConnectivity.value
assertThat(connectivity.isConnected).isTrue()
assertThat(connectivity.isValidated).isTrue()
}
@Test
fun `network event - create new subscription`() =
testScope.runTest {

View File

@@ -478,6 +478,35 @@ 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 mobileConnectivity_default() {
assertThat(underTest.defaultMobileNetworkConnectivity.value)

View File

@@ -40,6 +40,8 @@ class FakeMobileIconInteractor(
)
)
override val isConnected = MutableStateFlow(true)
private val _iconGroup = MutableStateFlow<SignalIcon.MobileIconGroup>(TelephonyIcons.THREE_G)
override val networkTypeIconGroup = _iconGroup

View File

@@ -23,6 +23,7 @@ import android.telephony.TelephonyManager.NETWORK_TYPE_UMTS
import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.settingslib.mobile.TelephonyIcons
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
import kotlinx.coroutines.flow.MutableStateFlow
@@ -59,6 +60,9 @@ class FakeMobileIconsInteractor(
override val alwaysShowDataRatIcon = MutableStateFlow(false)
override val alwaysUseCdmaLevel = MutableStateFlow(false)
override val defaultDataSubId = MutableStateFlow(DEFAULT_DATA_SUB_ID)
override val defaultMobileNetworkConnectivity = MutableStateFlow(MobileConnectivityModel())
private val _defaultMobileIconMapping = MutableStateFlow(TEST_MAPPING)
override val defaultMobileIconMapping = _defaultMobileIconMapping
@@ -77,6 +81,8 @@ class FakeMobileIconsInteractor(
companion object {
val DEFAULT_ICON = TelephonyIcons.G
const val DEFAULT_DATA_SUB_ID = 1
// Use [MobileMappings] to define some simple definitions
const val THREE_G = NETWORK_TYPE_GSM
const val LTE = NETWORK_TYPE_LTE

View File

@@ -61,8 +61,10 @@ class MobileIconInteractorTest : SysuiTestCase() {
mobileIconsInteractor.activeDataConnectionHasDataEnabled,
mobileIconsInteractor.alwaysShowDataRatIcon,
mobileIconsInteractor.alwaysUseCdmaLevel,
mobileIconsInteractor.defaultMobileNetworkConnectivity,
mobileIconsInteractor.defaultMobileIconMapping,
mobileIconsInteractor.defaultMobileIconGroup,
mobileIconsInteractor.defaultDataSubId,
mobileIconsInteractor.isDefaultConnectionFailed,
connectionRepository,
)
@@ -288,6 +290,30 @@ class MobileIconInteractorTest : SysuiTestCase() {
job.cancel()
}
@Test
fun `icon group - checks default data`() =
runBlocking(IMMEDIATE) {
mobileIconsInteractor.defaultDataSubId.value = SUB_1_ID
connectionRepository.setConnectionInfo(
MobileConnectionModel(
resolvedNetworkType = DefaultNetworkType(mobileMappingsProxy.toIconKey(THREE_G))
),
)
var latest: MobileIconGroup? = null
val job = underTest.networkTypeIconGroup.onEach { latest = it }.launchIn(this)
assertThat(latest).isEqualTo(TelephonyIcons.THREE_G)
// Default data sub id changes to something else
mobileIconsInteractor.defaultDataSubId.value = 123
yield()
assertThat(latest).isEqualTo(TelephonyIcons.NOT_DEFAULT_DATA)
job.cancel()
}
@Test
fun alwaysShowDataRatIcon_matchesParent() =
runBlocking(IMMEDIATE) {

View File

@@ -31,11 +31,13 @@ import com.android.systemui.util.CarrierConfigTracker
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.yield
import org.junit.After
import org.junit.Before
@@ -43,13 +45,16 @@ import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
class MobileIconsInteractorTest : SysuiTestCase() {
private lateinit var underTest: MobileIconsInteractor
private lateinit var connectionsRepository: FakeMobileConnectionsRepository
private val userSetupRepository = FakeUserSetupRepository()
private val mobileMappingsProxy = FakeMobileMappingsProxy()
private val scope = CoroutineScope(IMMEDIATE)
private val testDispatcher = UnconfinedTestDispatcher()
private val testScope = TestScope(testDispatcher)
@Mock private lateinit var carrierConfigTracker: CarrierConfigTracker
@@ -73,7 +78,7 @@ class MobileIconsInteractorTest : SysuiTestCase() {
connectionsRepository,
carrierConfigTracker,
userSetupRepository,
scope
testScope.backgroundScope,
)
}
@@ -81,7 +86,7 @@ class MobileIconsInteractorTest : SysuiTestCase() {
@Test
fun filteredSubscriptions_default() =
runBlocking(IMMEDIATE) {
testScope.runTest {
var latest: List<SubscriptionModel>? = null
val job = underTest.filteredSubscriptions.onEach { latest = it }.launchIn(this)
@@ -92,7 +97,7 @@ class MobileIconsInteractorTest : SysuiTestCase() {
@Test
fun filteredSubscriptions_nonOpportunistic_updatesWithMultipleSubs() =
runBlocking(IMMEDIATE) {
testScope.runTest {
connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_2))
var latest: List<SubscriptionModel>? = null
@@ -105,7 +110,7 @@ class MobileIconsInteractorTest : SysuiTestCase() {
@Test
fun filteredSubscriptions_bothOpportunistic_configFalse_showsActive_3() =
runBlocking(IMMEDIATE) {
testScope.runTest {
connectionsRepository.setSubscriptions(listOf(SUB_3_OPP, SUB_4_OPP))
connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
@@ -122,7 +127,7 @@ class MobileIconsInteractorTest : SysuiTestCase() {
@Test
fun filteredSubscriptions_bothOpportunistic_configFalse_showsActive_4() =
runBlocking(IMMEDIATE) {
testScope.runTest {
connectionsRepository.setSubscriptions(listOf(SUB_3_OPP, SUB_4_OPP))
connectionsRepository.setActiveMobileDataSubscriptionId(SUB_4_ID)
whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
@@ -139,7 +144,7 @@ class MobileIconsInteractorTest : SysuiTestCase() {
@Test
fun filteredSubscriptions_oneOpportunistic_configTrue_showsPrimary_active_1() =
runBlocking(IMMEDIATE) {
testScope.runTest {
connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_3_OPP))
connectionsRepository.setActiveMobileDataSubscriptionId(SUB_1_ID)
whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
@@ -157,7 +162,7 @@ class MobileIconsInteractorTest : SysuiTestCase() {
@Test
fun filteredSubscriptions_oneOpportunistic_configTrue_showsPrimary_nonActive_1() =
runBlocking(IMMEDIATE) {
testScope.runTest {
connectionsRepository.setSubscriptions(listOf(SUB_1, SUB_3_OPP))
connectionsRepository.setActiveMobileDataSubscriptionId(SUB_3_ID)
whenever(carrierConfigTracker.alwaysShowPrimarySignalBarInOpportunisticNetworkDefault)
@@ -175,7 +180,7 @@ class MobileIconsInteractorTest : SysuiTestCase() {
@Test
fun activeDataConnection_turnedOn() =
runBlocking(IMMEDIATE) {
testScope.runTest {
CONNECTION_1.setDataEnabled(true)
var latest: Boolean? = null
val job =
@@ -188,7 +193,7 @@ class MobileIconsInteractorTest : SysuiTestCase() {
@Test
fun activeDataConnection_turnedOff() =
runBlocking(IMMEDIATE) {
testScope.runTest {
CONNECTION_1.setDataEnabled(true)
var latest: Boolean? = null
val job =
@@ -204,7 +209,7 @@ class MobileIconsInteractorTest : SysuiTestCase() {
@Test
fun activeDataConnection_invalidSubId() =
runBlocking(IMMEDIATE) {
testScope.runTest {
var latest: Boolean? = null
val job =
underTest.activeDataConnectionHasDataEnabled.onEach { latest = it }.launchIn(this)
@@ -220,7 +225,7 @@ class MobileIconsInteractorTest : SysuiTestCase() {
@Test
fun failedConnection_connected_validated_notFailed() =
runBlocking(IMMEDIATE) {
testScope.runTest {
var latest: Boolean? = null
val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this)
connectionsRepository.setMobileConnectivity(MobileConnectivityModel(true, true))
@@ -233,7 +238,7 @@ class MobileIconsInteractorTest : SysuiTestCase() {
@Test
fun failedConnection_notConnected_notValidated_notFailed() =
runBlocking(IMMEDIATE) {
testScope.runTest {
var latest: Boolean? = null
val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this)
@@ -247,7 +252,7 @@ class MobileIconsInteractorTest : SysuiTestCase() {
@Test
fun failedConnection_connected_notValidated_failed() =
runBlocking(IMMEDIATE) {
testScope.runTest {
var latest: Boolean? = null
val job = underTest.isDefaultConnectionFailed.onEach { latest = it }.launchIn(this)
@@ -261,7 +266,7 @@ class MobileIconsInteractorTest : SysuiTestCase() {
@Test
fun alwaysShowDataRatIcon_configHasTrue() =
runBlocking(IMMEDIATE) {
testScope.runTest {
var latest: Boolean? = null
val job = underTest.alwaysShowDataRatIcon.onEach { latest = it }.launchIn(this)
@@ -277,7 +282,7 @@ class MobileIconsInteractorTest : SysuiTestCase() {
@Test
fun alwaysShowDataRatIcon_configHasFalse() =
runBlocking(IMMEDIATE) {
testScope.runTest {
var latest: Boolean? = null
val job = underTest.alwaysShowDataRatIcon.onEach { latest = it }.launchIn(this)
@@ -293,7 +298,7 @@ class MobileIconsInteractorTest : SysuiTestCase() {
@Test
fun alwaysUseCdmaLevel_configHasTrue() =
runBlocking(IMMEDIATE) {
testScope.runTest {
var latest: Boolean? = null
val job = underTest.alwaysUseCdmaLevel.onEach { latest = it }.launchIn(this)
@@ -309,7 +314,7 @@ class MobileIconsInteractorTest : SysuiTestCase() {
@Test
fun alwaysUseCdmaLevel_configHasFalse() =
runBlocking(IMMEDIATE) {
testScope.runTest {
var latest: Boolean? = null
val job = underTest.alwaysUseCdmaLevel.onEach { latest = it }.launchIn(this)
@@ -323,8 +328,286 @@ class MobileIconsInteractorTest : SysuiTestCase() {
job.cancel()
}
@Test
fun `default mobile connectivity - uses repo value`() =
testScope.runTest {
var latest: MobileConnectivityModel? = null
val job =
underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this)
var expected = MobileConnectivityModel(isConnected = true, isValidated = true)
connectionsRepository.setMobileConnectivity(expected)
assertThat(latest).isEqualTo(expected)
expected = MobileConnectivityModel(isConnected = false, isValidated = true)
connectionsRepository.setMobileConnectivity(expected)
assertThat(latest).isEqualTo(expected)
expected = MobileConnectivityModel(isConnected = true, isValidated = false)
connectionsRepository.setMobileConnectivity(expected)
assertThat(latest).isEqualTo(expected)
expected = MobileConnectivityModel(isConnected = false, isValidated = false)
connectionsRepository.setMobileConnectivity(expected)
assertThat(latest).isEqualTo(expected)
job.cancel()
}
@Test
fun `data switch - in same group - validated matches previous value`() =
testScope.runTest {
var latest: MobileConnectivityModel? = null
val job =
underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this)
connectionsRepository.setMobileConnectivity(
MobileConnectivityModel(
isConnected = true,
isValidated = true,
)
)
// Trigger a data change in the same subscription group
connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
connectionsRepository.setMobileConnectivity(
MobileConnectivityModel(
isConnected = false,
isValidated = false,
)
)
assertThat(latest)
.isEqualTo(
MobileConnectivityModel(
isConnected = false,
isValidated = true,
)
)
job.cancel()
}
@Test
fun `data switch - in same group - validated matches previous value - expires after 2s`() =
testScope.runTest {
var latest: MobileConnectivityModel? = null
val job =
underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this)
connectionsRepository.setMobileConnectivity(
MobileConnectivityModel(
isConnected = true,
isValidated = true,
)
)
// Trigger a data change in the same subscription group
connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
connectionsRepository.setMobileConnectivity(
MobileConnectivityModel(
isConnected = false,
isValidated = false,
)
)
// After 1s, the force validation bit is still present
advanceTimeBy(1000)
assertThat(latest)
.isEqualTo(
MobileConnectivityModel(
isConnected = false,
isValidated = true,
)
)
// After 2s, the force validation expires
advanceTimeBy(1001)
assertThat(latest)
.isEqualTo(
MobileConnectivityModel(
isConnected = false,
isValidated = false,
)
)
job.cancel()
}
@Test
fun `data switch - in same group - not validated - uses new value immediately`() =
testScope.runTest {
var latest: MobileConnectivityModel? = null
val job =
underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this)
connectionsRepository.setMobileConnectivity(
MobileConnectivityModel(
isConnected = true,
isValidated = false,
)
)
connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
connectionsRepository.setMobileConnectivity(
MobileConnectivityModel(
isConnected = false,
isValidated = false,
)
)
assertThat(latest)
.isEqualTo(
MobileConnectivityModel(
isConnected = false,
isValidated = false,
)
)
job.cancel()
}
@Test
fun `data switch - lose validation - then switch happens - clears forced bit`() =
testScope.runTest {
var latest: MobileConnectivityModel? = null
val job =
underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this)
// GIVEN the network starts validated
connectionsRepository.setMobileConnectivity(
MobileConnectivityModel(
isConnected = true,
isValidated = true,
)
)
// WHEN a data change happens in the same group
connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
// WHEN the validation bit is lost
connectionsRepository.setMobileConnectivity(
MobileConnectivityModel(
isConnected = false,
isValidated = false,
)
)
// WHEN another data change happens in the same group
connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
// THEN the forced validation bit is still removed after 2s
assertThat(latest)
.isEqualTo(
MobileConnectivityModel(
isConnected = false,
isValidated = true,
)
)
advanceTimeBy(1000)
assertThat(latest)
.isEqualTo(
MobileConnectivityModel(
isConnected = false,
isValidated = true,
)
)
advanceTimeBy(1001)
assertThat(latest)
.isEqualTo(
MobileConnectivityModel(
isConnected = false,
isValidated = false,
)
)
job.cancel()
}
@Test
fun `data switch - while already forcing validation - resets clock`() =
testScope.runTest {
var latest: MobileConnectivityModel? = null
val job =
underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this)
connectionsRepository.setMobileConnectivity(
MobileConnectivityModel(
isConnected = true,
isValidated = true,
)
)
connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
advanceTimeBy(1000)
// WHEN another change in same group event happens
connectionsRepository.activeSubChangedInGroupEvent.emit(Unit)
connectionsRepository.setMobileConnectivity(
MobileConnectivityModel(
isConnected = false,
isValidated = false,
)
)
// THEN the forced validation remains for exactly 2 more seconds from now
// 1.500s from second event
advanceTimeBy(1500)
assertThat(latest)
.isEqualTo(
MobileConnectivityModel(
isConnected = false,
isValidated = true,
)
)
// 2.001s from the second event
advanceTimeBy(501)
assertThat(latest)
.isEqualTo(
MobileConnectivityModel(
isConnected = false,
isValidated = false,
)
)
job.cancel()
}
@Test
fun `data switch - not in same group - uses new values`() =
testScope.runTest {
var latest: MobileConnectivityModel? = null
val job =
underTest.defaultMobileNetworkConnectivity.onEach { latest = it }.launchIn(this)
connectionsRepository.setMobileConnectivity(
MobileConnectivityModel(
isConnected = true,
isValidated = true,
)
)
connectionsRepository.setMobileConnectivity(
MobileConnectivityModel(
isConnected = false,
isValidated = false,
)
)
assertThat(latest)
.isEqualTo(
MobileConnectivityModel(
isConnected = false,
isValidated = false,
)
)
job.cancel()
}
companion object {
private val IMMEDIATE = Dispatchers.Main.immediate
private val tableLogBuffer =
TableLogBuffer(8, "MobileIconsInteractorTest", FakeSystemClock())

View File

@@ -273,6 +273,41 @@ class MobileIconViewModelTest : SysuiTestCase() {
job.cancel()
}
@Test
fun `network type - alwaysShow - shown when not connected`() =
testScope.runTest {
interactor.setIconGroup(THREE_G)
interactor.isConnected.value = false
interactor.alwaysShowDataRatIcon.value = true
var latest: Icon? = null
val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
val expected =
Icon.Resource(
THREE_G.dataType,
ContentDescription.Resource(THREE_G.dataContentDescription)
)
assertThat(latest).isEqualTo(expected)
job.cancel()
}
@Test
fun `network type - not shown when not connected`() =
testScope.runTest {
interactor.setIconGroup(THREE_G)
interactor.isDataConnected.value = true
interactor.isConnected.value = false
var latest: Icon? = null
val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this)
assertThat(latest).isNull()
job.cancel()
}
@Test
fun roaming() =
testScope.runTest {