[Sb refactor] Implement 2s grace period for data switching

The old mobile pipeline supported this particular use case. Given that
there are 2 mobile subscriptions, A and B, then when data switches from
A to B, we will consider B to be validated (see NetworkCapabilities) for
up to 2 seconds under the conditions:

1. A was validated before the switch
2. A and B are both in the same group (i.e.  they have the same group UUID)

This CL implements the same exact criteria by monitoring for data
switching in the same group and maintaining a validated state for 2
seconds for the default subscription.

Test: MobileIconsInteractorTest
Bug: 238425913
Change-Id: I8e798197beda562634c137377145dea510cbda4c
This commit is contained in:
Evan Laird
2023-01-10 15:46:56 -05:00
parent 5ceb454c8e
commit 7f894afe51
7 changed files with 342 additions and 1 deletions

View File

@@ -37,6 +37,12 @@ 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>
/** 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 }

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

View File

@@ -52,6 +52,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
@@ -63,6 +64,8 @@ import kotlinx.coroutines.flow.Flow
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
@@ -258,6 +261,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

@@ -32,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.
@@ -163,8 +166,45 @@ constructor(
}
}
/**
* 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> =
mobileConnectionsRepo.defaultMobileNetworkConnectivity
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

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,7 @@ class FakeMobileConnectionsRepository(
private val _activeMobileDataSubscriptionId = MutableStateFlow(INVALID_SUBSCRIPTION_ID)
override val activeMobileDataSubscriptionId = _activeMobileDataSubscriptionId
override val activeSubChangedInGroupEvent: MutableSharedFlow<Unit> = MutableSharedFlow()
private val _mobileConnectivity = MutableStateFlow(MobileConnectivityModel())
override val defaultMobileNetworkConnectivity = _mobileConnectivity

View File

@@ -36,6 +36,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
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
@@ -353,6 +354,259 @@ class MobileIconsInteractorTest : SysuiTestCase() {
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 tableLogBuffer =
TableLogBuffer(8, "MobileIconsInteractorTest", FakeSystemClock())