[kairos] Kairos in Mobile Pipeline data layer
Flag: com.android.systemui.status_bar_mobile_icon_kairos
Bug: 383172066
Test: atest
Change-Id: I6f5e921609e4b482bffaf2d63c202d680a94bcef
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index b53cb27..5b48566 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -727,6 +727,7 @@
"TraceurCommon",
"Traceur-res",
"aconfig_settings_flags_lib",
+ "kairos",
],
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherKairosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherKairosTest.kt
index f82de7a..80f4b2c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherKairosTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherKairosTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2025 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,235 +19,136 @@
import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager
import android.telephony.SubscriptionManager.PROFILE_CLASS_UNSET
-import android.telephony.TelephonyManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.demoModeController
import com.android.systemui.demomode.DemoMode
-import com.android.systemui.demomode.DemoModeController
-import com.android.systemui.dump.DumpManager
-import com.android.systemui.log.table.TableLogBuffer
-import com.android.systemui.log.table.tableLogBufferFactory
-import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
-import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosTestScope
+import com.android.systemui.kairos.runKairosTest
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoMobileConnectionsRepository
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoModeMobileConnectionDataSource
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoMobileConnectionsRepositoryKairos
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.validMobileEvent
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileConnectionsRepositoryImpl
-import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
-import com.android.systemui.statusbar.pipeline.mobile.util.FakeSubscriptionManagerProxy
-import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
-import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
-import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
-import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource
import com.android.systemui.testKosmos
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.kotlinArgumentCaptor
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.runBlocking
-import org.junit.After
-import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.Mock
import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.stub
/**
* The switcher acts as a dispatcher to either the `prod` or `demo` versions of the repository
* interface it's switching on. These tests just need to verify that the entire interface properly
* switches over when the value of `demoMode` changes
*/
-@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalKairosApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class MobileRepositorySwitcherKairosTest : SysuiTestCase() {
- private val kosmos = testKosmos()
-
- private lateinit var underTest: MobileRepositorySwitcherKairos
- private lateinit var realRepo: MobileConnectionsRepositoryImpl
- private lateinit var demoRepo: DemoMobileConnectionsRepository
- private lateinit var mobileDataSource: DemoModeMobileConnectionDataSource
- private lateinit var wifiDataSource: DemoModeWifiDataSource
- private lateinit var wifiRepository: FakeWifiRepository
- private lateinit var connectivityRepository: ConnectivityRepository
-
- @Mock private lateinit var subscriptionManager: SubscriptionManager
- @Mock private lateinit var telephonyManager: TelephonyManager
- @Mock private lateinit var logger: MobileInputLogger
- @Mock private lateinit var summaryLogger: TableLogBuffer
- @Mock private lateinit var demoModeController: DemoModeController
- @Mock private lateinit var dumpManager: DumpManager
-
- private val fakeNetworkEventsFlow = MutableStateFlow<FakeNetworkEventModel?>(null)
- private val mobileMappings = FakeMobileMappingsProxy()
- private val subscriptionManagerProxy = FakeSubscriptionManagerProxy()
-
- private val scope = CoroutineScope(IMMEDIATE)
-
- @Before
- fun setUp() {
- MockitoAnnotations.initMocks(this)
-
- // Never start in demo mode
- whenever(demoModeController.isInDemoMode).thenReturn(false)
-
- mobileDataSource =
- mock<DemoModeMobileConnectionDataSource>().also {
- whenever(it.mobileEvents).thenReturn(fakeNetworkEventsFlow)
+ private val kosmos =
+ testKosmos().apply {
+ useUnconfinedTestDispatcher()
+ demoModeController.stub {
+ // Never start in demo mode
+ on { isInDemoMode } doReturn false
}
- wifiDataSource =
- mock<DemoModeWifiDataSource>().also {
- whenever(it.wifiEvents).thenReturn(MutableStateFlow(null))
- }
- wifiRepository = FakeWifiRepository()
+ wifiDataSource.stub { on { wifiEvents } doReturn MutableStateFlow(null) }
+ }
- connectivityRepository = FakeConnectivityRepository()
+ private val Kosmos.underTest
+ get() = mobileRepositorySwitcherKairos
- realRepo =
- MobileConnectionsRepositoryImpl(
- connectivityRepository,
- subscriptionManager,
- subscriptionManagerProxy,
- telephonyManager,
- logger,
- summaryLogger,
- mobileMappings,
- fakeBroadcastDispatcher,
- context,
- /* bgDispatcher = */ IMMEDIATE,
- scope,
- /* mainDispatcher = */ IMMEDIATE,
- FakeAirplaneModeRepository(),
- wifiRepository,
- mock(),
- mock(),
- mock(),
- )
+ private val Kosmos.realRepo
+ get() = mobileConnectionsRepositoryKairosImpl
- demoRepo =
- DemoMobileConnectionsRepository(
- mobileDataSource = mobileDataSource,
- wifiDataSource = wifiDataSource,
- scope = scope,
- context = context,
- logFactory = kosmos.tableLogBufferFactory,
- )
+ private fun runTest(block: suspend KairosTestScope.() -> Unit) =
+ kosmos.run { runKairosTest { block() } }
- underTest =
- MobileRepositorySwitcherKairos(
- scope = scope,
- realRepository = realRepo,
- demoMobileConnectionsRepository = demoRepo,
- demoModeController = demoModeController,
- )
- }
+ @Test
+ fun activeRepoMatchesDemoModeSetting() = runTest {
+ demoModeController.stub { on { isInDemoMode } doReturn false }
- @After
- fun tearDown() {
- scope.cancel()
+ val latest by underTest.activeRepo.collectLastValue()
+
+ assertThat(latest).isEqualTo(realRepo)
+
+ startDemoMode()
+
+ assertThat(latest).isInstanceOf(DemoMobileConnectionsRepositoryKairos::class.java)
+
+ finishDemoMode()
+
+ assertThat(latest).isEqualTo(realRepo)
}
@Test
- fun activeRepoMatchesDemoModeSetting() =
- runBlocking(IMMEDIATE) {
- whenever(demoModeController.isInDemoMode).thenReturn(false)
+ fun subscriptionListUpdatesWhenDemoModeChanges() = runTest {
+ demoModeController.stub { on { isInDemoMode } doReturn false }
- var latest: MobileConnectionsRepository? = null
- val job = underTest.activeRepo.onEach { latest = it }.launchIn(this)
-
- assertThat(latest).isEqualTo(realRepo)
-
- startDemoMode()
-
- assertThat(latest).isEqualTo(demoRepo)
-
- finishDemoMode()
-
- assertThat(latest).isEqualTo(realRepo)
-
- job.cancel()
+ subscriptionManager.stub {
+ on { completeActiveSubscriptionInfoList } doReturn listOf(SUB_1, SUB_2)
}
- @Test
- fun subscriptionListUpdatesWhenDemoModeChanges() =
- runBlocking(IMMEDIATE) {
- whenever(demoModeController.isInDemoMode).thenReturn(false)
+ val latest by underTest.subscriptions.collectLastValue()
- whenever(subscriptionManager.completeActiveSubscriptionInfoList)
- .thenReturn(listOf(SUB_1, SUB_2))
+ // The real subscriptions has 2 subs
+ getSubscriptionCallback().onSubscriptionsChanged()
- var latest: List<SubscriptionModel>? = null
- val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
+ assertThat(latest).isEqualTo(listOf(MODEL_1, MODEL_2))
- // The real subscriptions has 2 subs
- whenever(subscriptionManager.completeActiveSubscriptionInfoList)
- .thenReturn(listOf(SUB_1, SUB_2))
- getSubscriptionCallback().onSubscriptionsChanged()
+ // Demo mode turns on, and we should see only the demo subscriptions
+ startDemoMode()
+ demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(validMobileEvent(subId = 3))
- assertThat(latest).isEqualTo(listOf(MODEL_1, MODEL_2))
+ // Demo mobile connections repository makes arbitrarily-formed subscription info
+ // objects, so just validate the data we care about
+ assertThat(latest).hasSize(1)
+ assertThat(latest!!.first().subscriptionId).isEqualTo(3)
- // Demo mode turns on, and we should see only the demo subscriptions
- startDemoMode()
- fakeNetworkEventsFlow.value = validMobileEvent(subId = 3)
+ finishDemoMode()
- // Demo mobile connections repository makes arbitrarily-formed subscription info
- // objects, so just validate the data we care about
- assertThat(latest).hasSize(1)
- assertThat(latest!![0].subscriptionId).isEqualTo(3)
+ assertThat(latest).isEqualTo(listOf(MODEL_1, MODEL_2))
+ }
- finishDemoMode()
-
- assertThat(latest).isEqualTo(listOf(MODEL_1, MODEL_2))
-
- job.cancel()
- }
-
- private fun startDemoMode() {
- whenever(demoModeController.isInDemoMode).thenReturn(true)
+ private fun KairosTestScope.startDemoMode() {
+ demoModeController.stub { on { isInDemoMode } doReturn true }
getDemoModeCallback().onDemoModeStarted()
}
- private fun finishDemoMode() {
- whenever(demoModeController.isInDemoMode).thenReturn(false)
+ private fun KairosTestScope.finishDemoMode() {
+ demoModeController.stub { on { isInDemoMode } doReturn false }
getDemoModeCallback().onDemoModeFinished()
}
- private fun getSubscriptionCallback(): SubscriptionManager.OnSubscriptionsChangedListener {
- val callbackCaptor =
- kotlinArgumentCaptor<SubscriptionManager.OnSubscriptionsChangedListener>()
- verify(subscriptionManager)
- .addOnSubscriptionsChangedListener(any(), callbackCaptor.capture())
- return callbackCaptor.value
- }
+ private fun KairosTestScope.getSubscriptionCallback():
+ SubscriptionManager.OnSubscriptionsChangedListener =
+ argumentCaptor<SubscriptionManager.OnSubscriptionsChangedListener>()
+ .apply {
+ verify(subscriptionManager).addOnSubscriptionsChangedListener(any(), capture())
+ }
+ .lastValue
- private fun getDemoModeCallback(): DemoMode {
- val captor = kotlinArgumentCaptor<DemoMode>()
- verify(demoModeController).addCallback(captor.capture())
- return captor.value
- }
+ private fun KairosTestScope.getDemoModeCallback(): DemoMode =
+ argumentCaptor<DemoMode>()
+ .apply { verify(demoModeController).addCallback(capture()) }
+ .lastValue
companion object {
- private val IMMEDIATE = Dispatchers.Main.immediate
-
private const val SUB_1_ID = 1
private const val SUB_1_NAME = "Carrier $SUB_1_ID"
- private val SUB_1 =
- mock<SubscriptionInfo>().also {
- whenever(it.subscriptionId).thenReturn(SUB_1_ID)
- whenever(it.carrierName).thenReturn(SUB_1_NAME)
- whenever(it.profileClass).thenReturn(PROFILE_CLASS_UNSET)
- }
+ private val SUB_1: SubscriptionInfo = mock {
+ on { subscriptionId } doReturn SUB_1_ID
+ on { carrierName } doReturn SUB_1_NAME
+ on { profileClass } doReturn PROFILE_CLASS_UNSET
+ }
private val MODEL_1 =
SubscriptionModel(
subscriptionId = SUB_1_ID,
@@ -257,12 +158,11 @@
private const val SUB_2_ID = 2
private const val SUB_2_NAME = "Carrier $SUB_2_ID"
- private val SUB_2 =
- mock<SubscriptionInfo>().also {
- whenever(it.subscriptionId).thenReturn(SUB_2_ID)
- whenever(it.carrierName).thenReturn(SUB_2_NAME)
- whenever(it.profileClass).thenReturn(PROFILE_CLASS_UNSET)
- }
+ private val SUB_2: SubscriptionInfo = mock {
+ on { subscriptionId } doReturn SUB_2_ID
+ on { carrierName } doReturn SUB_2_NAME
+ on { profileClass } doReturn PROFILE_CLASS_UNSET
+ }
private val MODEL_2 =
SubscriptionModel(
subscriptionId = SUB_2_ID,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionKairosParameterizedTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionKairosParameterizedTest.kt
index d2cd227..99cc93d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionKairosParameterizedTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionKairosParameterizedTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,156 +23,116 @@
import com.android.settingslib.SignalIcon
import com.android.settingslib.mobile.TelephonyIcons
import com.android.systemui.SysuiTestCase
-import com.android.systemui.log.table.tableLogBufferFactory
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosTestScope
+import com.android.systemui.kairos.kairos
+import com.android.systemui.kairos.map
+import com.android.systemui.kairos.runKairosTest
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demoMobileConnectionsRepositoryKairos
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demoModeMobileConnectionDataSourceKairos
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.fake
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.wifiDataSource
import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
-import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource
import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel
import com.android.systemui.testKosmos
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.runTest
-import org.junit.After
-import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.stub
import platform.test.runner.parameterized.ParameterizedAndroidJunit4
import platform.test.runner.parameterized.Parameters
/**
* Parameterized test for all of the common values of [FakeNetworkEventModel]. This test simply
- * verifies that passing the given model to [DemoMobileConnectionsRepository] results in the correct
- * flows emitting from the given connection.
+ * verifies that passing the given model to [DemoMobileConnectionsRepositoryKairos] results in the
+ * correct flows emitting from the given connection.
*/
-@OptIn(ExperimentalCoroutinesApi::class)
+@OptIn(ExperimentalKairosApi::class)
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
internal class DemoMobileConnectionKairosParameterizedTest(private val testCase: TestCase) :
SysuiTestCase() {
- private val kosmos = testKosmos()
- private val testDispatcher = UnconfinedTestDispatcher()
- private val testScope = TestScope(testDispatcher)
+ private val Kosmos.fakeWifiEventFlow by Fixture { MutableStateFlow<FakeWifiEventModel?>(null) }
- private val fakeNetworkEventFlow = MutableStateFlow<FakeNetworkEventModel?>(null)
- private val fakeWifiEventFlow = MutableStateFlow<FakeWifiEventModel?>(null)
+ private val kosmos =
+ testKosmos().apply {
+ useUnconfinedTestDispatcher()
+ wifiDataSource.stub { on { wifiEvents } doReturn fakeWifiEventFlow }
+ }
- private lateinit var connectionsRepo: DemoMobileConnectionsRepositoryKairos
- private lateinit var underTest: DemoMobileConnectionRepository
- private lateinit var mockDataSource: DemoModeMobileConnectionDataSource
- private lateinit var mockWifiDataSource: DemoModeWifiDataSource
-
- @Before
- fun setUp() {
- // The data source only provides one API, so we can mock it with a flow here for convenience
- mockDataSource =
- mock<DemoModeMobileConnectionDataSource>().also {
- whenever(it.mobileEvents).thenReturn(fakeNetworkEventFlow)
- }
- mockWifiDataSource =
- mock<DemoModeWifiDataSource>().also {
- whenever(it.wifiEvents).thenReturn(fakeWifiEventFlow)
- }
-
- connectionsRepo =
- DemoMobileConnectionsRepositoryKairos(
- mobileDataSource = mockDataSource,
- wifiDataSource = mockWifiDataSource,
- scope = testScope.backgroundScope,
- context = context,
- logFactory = kosmos.tableLogBufferFactory,
- )
-
- connectionsRepo.startProcessingCommands()
- }
-
- @After
- fun tearDown() {
- testScope.cancel()
- }
+ private fun runTest(block: suspend KairosTestScope.() -> Unit) =
+ kosmos.run { runKairosTest { block() } }
@Test
- fun demoNetworkData() =
- testScope.runTest {
- val networkModel =
- FakeNetworkEventModel.Mobile(
- level = testCase.level,
- dataType = testCase.dataType,
- subId = testCase.subId,
- carrierId = testCase.carrierId,
- inflateStrength = testCase.inflateStrength,
- activity = testCase.activity,
- carrierNetworkChange = testCase.carrierNetworkChange,
- roaming = testCase.roaming,
- name = "demo name",
- slice = testCase.slice,
- )
-
- fakeNetworkEventFlow.value = networkModel
- underTest = connectionsRepo.getRepoForSubId(subId)
-
- assertConnection(underTest, networkModel)
- }
-
- private fun TestScope.startCollection(conn: DemoMobileConnectionRepository): Job {
- val job = launch {
- launch { conn.cdmaLevel.collect {} }
- launch { conn.primaryLevel.collect {} }
- launch { conn.dataActivityDirection.collect {} }
- launch { conn.carrierNetworkChangeActive.collect {} }
- launch { conn.isRoaming.collect {} }
- launch { conn.networkName.collect {} }
- launch { conn.carrierName.collect {} }
- launch { conn.isEmergencyOnly.collect {} }
- launch { conn.dataConnectionState.collect {} }
- launch { conn.hasPrioritizedNetworkCapabilities.collect {} }
- }
- return job
+ fun demoNetworkData() = runTest {
+ val underTest by
+ demoMobileConnectionsRepositoryKairos.mobileConnectionsBySubId
+ .map { it[subId] }
+ .collectLastValue()
+ val networkModel =
+ FakeNetworkEventModel.Mobile(
+ level = testCase.level,
+ dataType = testCase.dataType,
+ subId = testCase.subId,
+ carrierId = testCase.carrierId,
+ inflateStrength = testCase.inflateStrength,
+ activity = testCase.activity,
+ carrierNetworkChange = testCase.carrierNetworkChange,
+ roaming = testCase.roaming,
+ name = "demo name",
+ slice = testCase.slice,
+ )
+ demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(networkModel)
+ assertConnection(underTest!!, networkModel)
}
- private fun TestScope.assertConnection(
- conn: DemoMobileConnectionRepository,
+ private suspend fun KairosTestScope.assertConnection(
+ conn: DemoMobileConnectionRepositoryKairos,
model: FakeNetworkEventModel,
) {
- val job = startCollection(underTest)
when (model) {
is FakeNetworkEventModel.Mobile -> {
- assertThat(conn.subId).isEqualTo(model.subId)
- assertThat(conn.cdmaLevel.value).isEqualTo(model.level)
- assertThat(conn.primaryLevel.value).isEqualTo(model.level)
- assertThat(conn.dataActivityDirection.value)
- .isEqualTo((model.activity ?: DATA_ACTIVITY_NONE).toMobileDataActivityModel())
- assertThat(conn.carrierNetworkChangeActive.value)
- .isEqualTo(model.carrierNetworkChange)
- assertThat(conn.isRoaming.value).isEqualTo(model.roaming)
- assertThat(conn.networkName.value)
- .isEqualTo(NetworkNameModel.IntentDerived(model.name))
- assertThat(conn.carrierName.value)
- .isEqualTo(NetworkNameModel.SubscriptionDerived("${model.name} ${model.subId}"))
- assertThat(conn.hasPrioritizedNetworkCapabilities.value).isEqualTo(model.slice)
- assertThat(conn.isNonTerrestrial.value).isEqualTo(model.ntn)
+ kairos.transact {
+ assertThat(conn.subId).isEqualTo(model.subId)
+ assertThat(conn.cdmaLevel.sample()).isEqualTo(model.level)
+ assertThat(conn.primaryLevel.sample()).isEqualTo(model.level)
+ assertThat(conn.dataActivityDirection.sample())
+ .isEqualTo(
+ (model.activity ?: DATA_ACTIVITY_NONE).toMobileDataActivityModel()
+ )
+ assertThat(conn.carrierNetworkChangeActive.sample())
+ .isEqualTo(model.carrierNetworkChange)
+ assertThat(conn.isRoaming.sample()).isEqualTo(model.roaming)
+ assertThat(conn.networkName.sample())
+ .isEqualTo(NetworkNameModel.IntentDerived(model.name))
+ assertThat(conn.carrierName.sample())
+ .isEqualTo(
+ NetworkNameModel.SubscriptionDerived("${model.name} ${model.subId}")
+ )
+ assertThat(conn.hasPrioritizedNetworkCapabilities.sample())
+ .isEqualTo(model.slice)
+ assertThat(conn.isNonTerrestrial.sample()).isEqualTo(model.ntn)
- // TODO(b/261029387): check these once we start handling them
- assertThat(conn.isEmergencyOnly.value).isFalse()
- assertThat(conn.isGsm.value).isFalse()
- assertThat(conn.dataConnectionState.value).isEqualTo(DataConnectionState.Connected)
+ // TODO(b/261029387): check these once we start handling them
+ assertThat(conn.isEmergencyOnly.sample()).isFalse()
+ assertThat(conn.isGsm.sample()).isFalse()
+ assertThat(conn.dataConnectionState.sample())
+ .isEqualTo(DataConnectionState.Connected)
+ }
}
// MobileDisabled isn't combinatorial in nature, and is tested in
// DemoMobileConnectionsRepositoryTest.kt
else -> {}
}
-
- job.cancel()
}
/** Matches [FakeNetworkEventModel] */
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryKairosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryKairosTest.kt
index 95cdee0..503d561 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryKairosTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryKairosTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,572 +21,445 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.log.table.tableLogBufferFactory
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosTestScope
+import com.android.systemui.kairos.kairos
+import com.android.systemui.kairos.map
+import com.android.systemui.kairos.runKairosTest
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
-import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demoMobileConnectionsRepositoryKairos
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demoModeMobileConnectionDataSourceKairos
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.fake
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.wifiDataSource
import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
-import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource
import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel
import com.android.systemui.testKosmos
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import junit.framework.Assert
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.stub
-@OptIn(ExperimentalCoroutinesApi::class)
+@OptIn(ExperimentalKairosApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class DemoMobileConnectionsRepositoryKairosTest : SysuiTestCase() {
- private val kosmos = testKosmos()
- private val testDispatcher = UnconfinedTestDispatcher()
- private val testScope = TestScope(testDispatcher)
+ private val Kosmos.fakeWifiEventFlow by
+ Kosmos.Fixture { MutableStateFlow<FakeWifiEventModel?>(null) }
- private val fakeNetworkEventFlow = MutableStateFlow<FakeNetworkEventModel?>(null)
- private val fakeWifiEventFlow = MutableStateFlow<FakeWifiEventModel?>(null)
+ private val Kosmos.underTest
+ get() = demoMobileConnectionsRepositoryKairos
- private lateinit var underTest: DemoMobileConnectionsRepositoryKairos
- private lateinit var mobileDataSource: DemoModeMobileConnectionDataSource
- private lateinit var wifiDataSource: DemoModeWifiDataSource
+ private val kosmos =
+ testKosmos().apply {
+ useUnconfinedTestDispatcher()
+ wifiDataSource.stub { on { wifiEvents } doReturn fakeWifiEventFlow }
+ }
- @Before
- fun setUp() {
- // The data source only provides one API, so we can mock it with a flow here for convenience
- mobileDataSource =
- mock<DemoModeMobileConnectionDataSource>().also {
- whenever(it.mobileEvents).thenReturn(fakeNetworkEventFlow)
- }
- wifiDataSource =
- mock<DemoModeWifiDataSource>().also {
- whenever(it.wifiEvents).thenReturn(fakeWifiEventFlow)
- }
+ private fun runTest(block: suspend KairosTestScope.() -> Unit) =
+ kosmos.run { runKairosTest { block() } }
- underTest =
- DemoMobileConnectionsRepositoryKairos(
- mobileDataSource = mobileDataSource,
- wifiDataSource = wifiDataSource,
- scope = testScope.backgroundScope,
- context = context,
- logFactory = kosmos.tableLogBufferFactory,
- )
-
- underTest.startProcessingCommands()
+ @Test
+ fun isDefault_defaultsToTrue() = runTest {
+ underTest
+ val isDefault = kairos.transact { underTest.mobileIsDefault.sample() }
+ assertThat(isDefault).isTrue()
}
@Test
- fun isDefault_defaultsToTrue() =
- testScope.runTest {
- val isDefault = underTest.mobileIsDefault.value
- assertThat(isDefault).isTrue()
- }
-
- @Test
- fun validated_defaultsToTrue() =
- testScope.runTest {
- val isValidated = underTest.defaultConnectionIsValidated.value
- assertThat(isValidated).isTrue()
- }
-
- @Test
- fun networkEvent_createNewSubscription() =
- testScope.runTest {
- var latest: List<SubscriptionModel>? = null
- val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
-
- assertThat(latest).isEmpty()
-
- fakeNetworkEventFlow.value = validMobileEvent(subId = 1)
-
- assertThat(latest).hasSize(1)
- assertThat(latest!![0].subscriptionId).isEqualTo(1)
-
- job.cancel()
- }
-
- @Test
- fun wifiCarrierMergedEvent_createNewSubscription() =
- testScope.runTest {
- var latest: List<SubscriptionModel>? = null
- val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
-
- assertThat(latest).isEmpty()
-
- fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5)
-
- assertThat(latest).hasSize(1)
- assertThat(latest!![0].subscriptionId).isEqualTo(5)
-
- job.cancel()
- }
-
- @Test
- fun networkEvent_reusesSubscriptionWhenSameId() =
- testScope.runTest {
- var latest: List<SubscriptionModel>? = null
- val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
-
- assertThat(latest).isEmpty()
-
- fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 1)
-
- assertThat(latest).hasSize(1)
- assertThat(latest!![0].subscriptionId).isEqualTo(1)
-
- // Second network event comes in with the same subId, does not create a new subscription
- fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 2)
-
- assertThat(latest).hasSize(1)
- assertThat(latest!![0].subscriptionId).isEqualTo(1)
-
- job.cancel()
- }
-
- @Test
- fun wifiCarrierMergedEvent_reusesSubscriptionWhenSameId() =
- testScope.runTest {
- var latest: List<SubscriptionModel>? = null
- val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
-
- assertThat(latest).isEmpty()
-
- fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5, level = 1)
-
- assertThat(latest).hasSize(1)
- assertThat(latest!![0].subscriptionId).isEqualTo(5)
-
- // Second network event comes in with the same subId, does not create a new subscription
- fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5, level = 2)
-
- assertThat(latest).hasSize(1)
- assertThat(latest!![0].subscriptionId).isEqualTo(5)
-
- job.cancel()
- }
-
- @Test
- fun multipleSubscriptions() =
- testScope.runTest {
- var latest: List<SubscriptionModel>? = null
- val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
-
- fakeNetworkEventFlow.value = validMobileEvent(subId = 1)
- fakeNetworkEventFlow.value = validMobileEvent(subId = 2)
-
- assertThat(latest).hasSize(2)
-
- job.cancel()
- }
-
- @Test
- fun mobileSubscriptionAndCarrierMergedSubscription() =
- testScope.runTest {
- var latest: List<SubscriptionModel>? = null
- val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
-
- fakeNetworkEventFlow.value = validMobileEvent(subId = 1)
- fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5)
-
- assertThat(latest).hasSize(2)
-
- job.cancel()
- }
-
- @Test
- fun multipleMobileSubscriptionsAndCarrierMergedSubscription() =
- testScope.runTest {
- var latest: List<SubscriptionModel>? = null
- val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
-
- fakeNetworkEventFlow.value = validMobileEvent(subId = 1)
- fakeNetworkEventFlow.value = validMobileEvent(subId = 2)
- fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 3)
-
- assertThat(latest).hasSize(3)
-
- job.cancel()
- }
-
- @Test
- fun mobileDisabledEvent_disablesConnection_subIdSpecified_singleConn() =
- testScope.runTest {
- var latest: List<SubscriptionModel>? = null
- val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
-
- fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 1)
-
- fakeNetworkEventFlow.value = MobileDisabled(subId = 1)
-
- assertThat(latest).hasSize(0)
-
- job.cancel()
- }
-
- @Test
- fun mobileDisabledEvent_disablesConnection_subIdNotSpecified_singleConn() =
- testScope.runTest {
- var latest: List<SubscriptionModel>? = null
- val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
-
- fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 1)
-
- fakeNetworkEventFlow.value = MobileDisabled(subId = null)
-
- assertThat(latest).hasSize(0)
-
- job.cancel()
- }
-
- @Test
- fun mobileDisabledEvent_disablesConnection_subIdSpecified_multipleConn() =
- testScope.runTest {
- var latest: List<SubscriptionModel>? = null
- val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
-
- fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 1)
- fakeNetworkEventFlow.value = validMobileEvent(subId = 2, level = 1)
-
- fakeNetworkEventFlow.value = MobileDisabled(subId = 2)
-
- assertThat(latest).hasSize(1)
-
- job.cancel()
- }
-
- @Test
- fun mobileDisabledEvent_subIdNotSpecified_multipleConn_ignoresCommand() =
- testScope.runTest {
- var latest: List<SubscriptionModel>? = null
- val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
-
- fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 1)
- fakeNetworkEventFlow.value = validMobileEvent(subId = 2, level = 1)
-
- fakeNetworkEventFlow.value = MobileDisabled(subId = null)
-
- assertThat(latest).hasSize(2)
-
- job.cancel()
- }
-
- @Test
- fun wifiNetworkUpdatesToDisabled_carrierMergedConnectionRemoved() =
- testScope.runTest {
- var latest: List<SubscriptionModel>? = null
- val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
-
- fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 1)
-
- assertThat(latest).hasSize(1)
-
- fakeWifiEventFlow.value = FakeWifiEventModel.WifiDisabled
-
- assertThat(latest).isEmpty()
-
- job.cancel()
- }
-
- @Test
- fun wifiNetworkUpdatesToActive_carrierMergedConnectionRemoved() =
- testScope.runTest {
- var latest: List<SubscriptionModel>? = null
- val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
-
- fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 1)
-
- assertThat(latest).hasSize(1)
-
- fakeWifiEventFlow.value =
- FakeWifiEventModel.Wifi(level = 1, activity = 0, ssid = null, validated = true)
-
- assertThat(latest).isEmpty()
-
- job.cancel()
- }
-
- @Test
- fun mobileSubUpdatesToCarrierMerged_onlyOneConnection() =
- testScope.runTest {
- var latestSubsList: List<SubscriptionModel>? = null
- var connections: List<DemoMobileConnectionRepository>? = null
- val job =
- underTest.subscriptions
- .onEach { latestSubsList = it }
- .onEach { infos ->
- connections =
- infos.map { info -> underTest.getRepoForSubId(info.subscriptionId) }
- }
- .launchIn(this)
-
- fakeNetworkEventFlow.value = validMobileEvent(subId = 3, level = 2)
- assertThat(latestSubsList).hasSize(1)
-
- val carrierMergedEvent = validCarrierMergedEvent(subId = 3, level = 1)
- fakeWifiEventFlow.value = carrierMergedEvent
- assertThat(latestSubsList).hasSize(1)
- val connection = connections!!.find { it.subId == 3 }!!
- assertCarrierMergedConnection(connection, carrierMergedEvent)
-
- job.cancel()
- }
-
- @Test
- fun mobileSubUpdatesToCarrierMergedThenBack_hasOldMobileData() =
- testScope.runTest {
- var latestSubsList: List<SubscriptionModel>? = null
- var connections: List<DemoMobileConnectionRepository>? = null
- val job =
- underTest.subscriptions
- .onEach { latestSubsList = it }
- .onEach { infos ->
- connections =
- infos.map { info -> underTest.getRepoForSubId(info.subscriptionId) }
- }
- .launchIn(this)
-
- val mobileEvent = validMobileEvent(subId = 3, level = 2)
- fakeNetworkEventFlow.value = mobileEvent
- assertThat(latestSubsList).hasSize(1)
-
- val carrierMergedEvent = validCarrierMergedEvent(subId = 3, level = 1)
- fakeWifiEventFlow.value = carrierMergedEvent
- assertThat(latestSubsList).hasSize(1)
- var connection = connections!!.find { it.subId == 3 }!!
- assertCarrierMergedConnection(connection, carrierMergedEvent)
-
- // WHEN the carrier merged is removed
- fakeWifiEventFlow.value =
- FakeWifiEventModel.Wifi(level = 4, activity = 0, ssid = null, validated = true)
-
- // THEN the subId=3 connection goes back to the mobile information
- connection = connections!!.find { it.subId == 3 }!!
- assertConnection(connection, mobileEvent)
-
- job.cancel()
- }
-
- /** Regression test for b/261706421 */
- @Test
- fun multipleConnections_removeAll_doesNotThrow() =
- testScope.runTest {
- var latest: List<SubscriptionModel>? = null
- val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
-
- // Two subscriptions are added
- fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 1)
- fakeNetworkEventFlow.value = validMobileEvent(subId = 2, level = 1)
-
- // Then both are removed by turning off demo mode
- underTest.stopProcessingCommands()
-
- assertThat(latest).isEmpty()
-
- job.cancel()
- }
-
- @Test
- fun demoConnection_singleSubscription() =
- testScope.runTest {
- var currentEvent: FakeNetworkEventModel = validMobileEvent(subId = 1)
- var connections: List<DemoMobileConnectionRepository>? = null
- val job =
- underTest.subscriptions
- .onEach { infos ->
- connections =
- infos.map { info -> underTest.getRepoForSubId(info.subscriptionId) }
- }
- .launchIn(this)
-
- fakeNetworkEventFlow.value = currentEvent
-
- assertThat(connections).hasSize(1)
- val connection1 = connections!![0]
-
- assertConnection(connection1, currentEvent)
-
- // Exercise the whole api
-
- currentEvent = validMobileEvent(subId = 1, level = 2)
- fakeNetworkEventFlow.value = currentEvent
- assertConnection(connection1, currentEvent)
-
- job.cancel()
- }
-
- @Test
- fun demoConnection_twoConnections_updateSecond_noAffectOnFirst() =
- testScope.runTest {
- var currentEvent1 = validMobileEvent(subId = 1)
- var connection1: DemoMobileConnectionRepository? = null
- var currentEvent2 = validMobileEvent(subId = 2)
- var connection2: DemoMobileConnectionRepository? = null
- var connections: List<DemoMobileConnectionRepository>? = null
- val job =
- underTest.subscriptions
- .onEach { infos ->
- connections =
- infos.map { info -> underTest.getRepoForSubId(info.subscriptionId) }
- }
- .launchIn(this)
-
- fakeNetworkEventFlow.value = currentEvent1
- fakeNetworkEventFlow.value = currentEvent2
- assertThat(connections).hasSize(2)
- connections!!.forEach {
- if (it.subId == 1) {
- connection1 = it
- } else if (it.subId == 2) {
- connection2 = it
- } else {
- Assert.fail("Unexpected subscription")
- }
- }
-
- assertConnection(connection1!!, currentEvent1)
- assertConnection(connection2!!, currentEvent2)
-
- // WHEN the event changes for connection 2, it updates, and connection 1 stays the same
- currentEvent2 = validMobileEvent(subId = 2, activity = DATA_ACTIVITY_INOUT)
- fakeNetworkEventFlow.value = currentEvent2
- assertConnection(connection1!!, currentEvent1)
- assertConnection(connection2!!, currentEvent2)
-
- // and vice versa
- currentEvent1 = validMobileEvent(subId = 1, inflateStrength = true)
- fakeNetworkEventFlow.value = currentEvent1
- assertConnection(connection1!!, currentEvent1)
- assertConnection(connection2!!, currentEvent2)
-
- job.cancel()
- }
-
- @Test
- fun demoConnection_twoConnections_updateCarrierMerged_noAffectOnFirst() =
- testScope.runTest {
- var currentEvent1 = validMobileEvent(subId = 1)
- var connection1: DemoMobileConnectionRepository? = null
- var currentEvent2 = validCarrierMergedEvent(subId = 2)
- var connection2: DemoMobileConnectionRepository? = null
- var connections: List<DemoMobileConnectionRepository>? = null
- val job =
- underTest.subscriptions
- .onEach { infos ->
- connections =
- infos.map { info -> underTest.getRepoForSubId(info.subscriptionId) }
- }
- .launchIn(this)
-
- fakeNetworkEventFlow.value = currentEvent1
- fakeWifiEventFlow.value = currentEvent2
- assertThat(connections).hasSize(2)
- connections!!.forEach {
- when (it.subId) {
- 1 -> connection1 = it
- 2 -> connection2 = it
- else -> Assert.fail("Unexpected subscription")
- }
- }
-
- assertConnection(connection1!!, currentEvent1)
- assertCarrierMergedConnection(connection2!!, currentEvent2)
-
- // WHEN the event changes for connection 2, it updates, and connection 1 stays the same
- currentEvent2 = validCarrierMergedEvent(subId = 2, level = 4)
- fakeWifiEventFlow.value = currentEvent2
- assertConnection(connection1!!, currentEvent1)
- assertCarrierMergedConnection(connection2!!, currentEvent2)
-
- // and vice versa
- currentEvent1 = validMobileEvent(subId = 1, inflateStrength = true)
- fakeNetworkEventFlow.value = currentEvent1
- assertConnection(connection1!!, currentEvent1)
- assertCarrierMergedConnection(connection2!!, currentEvent2)
-
- job.cancel()
- }
-
- @Test
- fun demoIsNotInEcmState() = testScope.runTest { assertThat(underTest.isInEcmMode()).isFalse() }
-
- private fun TestScope.startCollection(conn: DemoMobileConnectionRepository): Job {
- val job = launch {
- launch { conn.cdmaLevel.collect {} }
- launch { conn.primaryLevel.collect {} }
- launch { conn.dataActivityDirection.collect {} }
- launch { conn.carrierNetworkChangeActive.collect {} }
- launch { conn.isRoaming.collect {} }
- launch { conn.networkName.collect {} }
- launch { conn.carrierName.collect {} }
- launch { conn.isEmergencyOnly.collect {} }
- launch { conn.dataConnectionState.collect {} }
- launch { conn.hasPrioritizedNetworkCapabilities.collect {} }
- }
- return job
+ fun validated_defaultsToTrue() = runTest {
+ underTest
+ val isValidated = kairos.transact { underTest.defaultConnectionIsValidated.sample() }
+ assertThat(isValidated).isTrue()
}
- private fun TestScope.assertConnection(
- conn: DemoMobileConnectionRepository,
+ @Test
+ fun networkEvent_createNewSubscription() = runTest {
+ val latest by underTest.subscriptions.collectLastValue()
+
+ assertThat(latest).isEmpty()
+
+ demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(validMobileEvent(subId = 1))
+
+ assertThat(latest).hasSize(1)
+ assertThat(latest!!.first().subscriptionId).isEqualTo(1)
+ }
+
+ @Test
+ fun wifiCarrierMergedEvent_createNewSubscription() = runTest {
+ val latest by underTest.subscriptions.collectLastValue()
+
+ assertThat(latest).isEmpty()
+
+ fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5)
+
+ assertThat(latest).hasSize(1)
+ assertThat(latest!!.first().subscriptionId).isEqualTo(5)
+ }
+
+ @Test
+ fun networkEvent_reusesSubscriptionWhenSameId() = runTest {
+ val latest by underTest.subscriptions.collectLastValue()
+
+ assertThat(latest).isEmpty()
+
+ demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(
+ validMobileEvent(subId = 1, level = 1)
+ )
+
+ assertThat(latest).hasSize(1)
+ assertThat(latest!!.first().subscriptionId).isEqualTo(1)
+
+ // Second network event comes in with the same subId, does not create a new subscription
+ demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(
+ validMobileEvent(subId = 1, level = 2)
+ )
+
+ assertThat(latest).hasSize(1)
+ assertThat(latest!!.first().subscriptionId).isEqualTo(1)
+ }
+
+ @Test
+ fun wifiCarrierMergedEvent_reusesSubscriptionWhenSameId() = runTest {
+ val latest by underTest.subscriptions.collectLastValue()
+
+ assertThat(latest).isEmpty()
+
+ fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5, level = 1)
+
+ assertThat(latest).hasSize(1)
+ assertThat(latest!!.first().subscriptionId).isEqualTo(5)
+
+ // Second network event comes in with the same subId, does not create a new subscription
+ fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5, level = 2)
+
+ assertThat(latest).hasSize(1)
+ assertThat(latest!!.first().subscriptionId).isEqualTo(5)
+ }
+
+ @Test
+ fun multipleSubscriptions() = runTest {
+ val latest by underTest.subscriptions.collectLastValue()
+
+ demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(validMobileEvent(subId = 1))
+ demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(validMobileEvent(subId = 2))
+
+ assertThat(latest).hasSize(2)
+ }
+
+ @Test
+ fun mobileSubscriptionAndCarrierMergedSubscription() = runTest {
+ val latest by underTest.subscriptions.collectLastValue()
+
+ demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(validMobileEvent(subId = 1))
+ fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5)
+
+ assertThat(latest).hasSize(2)
+ }
+
+ @Test
+ fun multipleMobileSubscriptionsAndCarrierMergedSubscription() = runTest {
+ val latest by underTest.subscriptions.collectLastValue()
+
+ demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(validMobileEvent(subId = 1))
+ demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(validMobileEvent(subId = 2))
+ fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 3)
+
+ assertThat(latest).hasSize(3)
+ }
+
+ @Test
+ fun mobileDisabledEvent_disablesConnection_subIdSpecified_singleConn() = runTest {
+ val latest by underTest.subscriptions.collectLastValue()
+
+ demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(
+ validMobileEvent(subId = 1, level = 1)
+ )
+
+ demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(MobileDisabled(subId = 1))
+
+ assertThat(latest).hasSize(0)
+ }
+
+ @Test
+ fun mobileDisabledEvent_disablesConnection_subIdNotSpecified_singleConn() = runTest {
+ val latest by underTest.subscriptions.collectLastValue()
+
+ demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(
+ validMobileEvent(subId = 1, level = 1)
+ )
+
+ demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(
+ MobileDisabled(subId = null)
+ )
+
+ assertThat(latest).hasSize(0)
+ }
+
+ @Test
+ fun mobileDisabledEvent_disablesConnection_subIdSpecified_multipleConn() = runTest {
+ val latest by underTest.subscriptions.collectLastValue()
+
+ demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(
+ validMobileEvent(subId = 1, level = 1)
+ )
+
+ assertThat(latest).hasSize(1)
+
+ demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(
+ validMobileEvent(subId = 2, level = 1)
+ )
+
+ assertThat(latest).hasSize(2)
+
+ demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(MobileDisabled(subId = 2))
+
+ assertThat(latest).hasSize(1)
+ }
+
+ @Test
+ fun mobileDisabledEvent_subIdNotSpecified_multipleConn_ignoresCommand() = runTest {
+ val latest by underTest.subscriptions.collectLastValue()
+
+ demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(
+ validMobileEvent(subId = 1, level = 1)
+ )
+ demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(
+ validMobileEvent(subId = 2, level = 1)
+ )
+ demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(
+ MobileDisabled(subId = null)
+ )
+
+ assertThat(latest).hasSize(2)
+ }
+
+ @Test
+ fun wifiNetworkUpdatesToDisabled_carrierMergedConnectionRemoved() = runTest {
+ val latest by underTest.subscriptions.collectLastValue()
+
+ fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 1)
+
+ assertThat(latest).hasSize(1)
+
+ fakeWifiEventFlow.value = FakeWifiEventModel.WifiDisabled
+
+ assertThat(latest).isEmpty()
+ }
+
+ @Test
+ fun wifiNetworkUpdatesToActive_carrierMergedConnectionRemoved() = runTest {
+ val latest by underTest.subscriptions.collectLastValue()
+
+ fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 1)
+
+ assertThat(latest).hasSize(1)
+
+ fakeWifiEventFlow.value =
+ FakeWifiEventModel.Wifi(level = 1, activity = 0, ssid = null, validated = true)
+
+ assertThat(latest).isEmpty()
+ }
+
+ @Test
+ fun mobileSubUpdatesToCarrierMerged_onlyOneConnection() = runTest {
+ val latestSubsList by underTest.subscriptions.collectLastValue()
+ val connections by underTest.mobileConnectionsBySubId.map { it.values }.collectLastValue()
+
+ demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(
+ validMobileEvent(subId = 3, level = 2)
+ )
+ assertThat(latestSubsList).hasSize(1)
+
+ val carrierMergedEvent = validCarrierMergedEvent(subId = 3, level = 1)
+ fakeWifiEventFlow.value = carrierMergedEvent
+ assertThat(latestSubsList).hasSize(1)
+ val connection = connections!!.find { it.subId == 3 }!!
+ assertCarrierMergedConnection(connection, carrierMergedEvent)
+ }
+
+ @Test
+ fun mobileSubUpdatesToCarrierMergedThenBack_hasOldMobileData() = runTest {
+ val latestSubsList by underTest.subscriptions.collectLastValue()
+ val connections by underTest.mobileConnectionsBySubId.map { it.values }.collectLastValue()
+
+ val mobileEvent = validMobileEvent(subId = 3, level = 2)
+ demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(mobileEvent)
+ assertThat(latestSubsList).hasSize(1)
+
+ val carrierMergedEvent = validCarrierMergedEvent(subId = 3, level = 1)
+ fakeWifiEventFlow.value = carrierMergedEvent
+ assertThat(latestSubsList).hasSize(1)
+ var connection = connections!!.find { it.subId == 3 }!!
+ assertCarrierMergedConnection(connection, carrierMergedEvent)
+
+ // WHEN the carrier merged is removed
+ fakeWifiEventFlow.value =
+ FakeWifiEventModel.Wifi(level = 4, activity = 0, ssid = null, validated = true)
+
+ assertThat(latestSubsList).hasSize(1)
+ assertThat(connections).hasSize(1)
+
+ // THEN the subId=3 connection goes back to the mobile information
+ connection = connections!!.find { it.subId == 3 }!!
+ assertConnection(connection, mobileEvent)
+ }
+
+ @Test
+ fun demoConnection_singleSubscription() = runTest {
+ var currentEvent: FakeNetworkEventModel = validMobileEvent(subId = 1)
+ val connections by underTest.mobileConnectionsBySubId.map { it.values }.collectLastValue()
+
+ demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(currentEvent)
+
+ assertThat(connections).hasSize(1)
+ val connection1 = connections!!.first()
+
+ assertConnection(connection1, currentEvent)
+
+ // Exercise the whole api
+
+ currentEvent = validMobileEvent(subId = 1, level = 2)
+ demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(currentEvent)
+ assertConnection(connection1, currentEvent)
+ }
+
+ @Test
+ fun demoConnection_twoConnections_updateSecond_noAffectOnFirst() = runTest {
+ var currentEvent1 = validMobileEvent(subId = 1)
+ var connection1: DemoMobileConnectionRepositoryKairos? = null
+ var currentEvent2 = validMobileEvent(subId = 2)
+ var connection2: DemoMobileConnectionRepositoryKairos? = null
+ val connections by underTest.mobileConnectionsBySubId.map { it.values }.collectLastValue()
+
+ demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(currentEvent1)
+ demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(currentEvent2)
+ assertThat(connections).hasSize(2)
+ connections!!.forEach {
+ when (it.subId) {
+ 1 -> connection1 = it
+ 2 -> connection2 = it
+ else -> Assert.fail("Unexpected subscription")
+ }
+ }
+
+ assertConnection(connection1!!, currentEvent1)
+ assertConnection(connection2!!, currentEvent2)
+
+ // WHEN the event changes for connection 2, it updates, and connection 1 stays the same
+ currentEvent2 = validMobileEvent(subId = 2, activity = DATA_ACTIVITY_INOUT)
+ demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(currentEvent2)
+ assertConnection(connection1!!, currentEvent1)
+ assertConnection(connection2!!, currentEvent2)
+
+ // and vice versa
+ currentEvent1 = validMobileEvent(subId = 1, inflateStrength = true)
+ demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(currentEvent1)
+ assertConnection(connection1!!, currentEvent1)
+ assertConnection(connection2!!, currentEvent2)
+ }
+
+ @Test
+ fun demoConnection_twoConnections_updateCarrierMerged_noAffectOnFirst() = runTest {
+ var currentEvent1 = validMobileEvent(subId = 1)
+ var connection1: DemoMobileConnectionRepositoryKairos? = null
+ var currentEvent2 = validCarrierMergedEvent(subId = 2)
+ var connection2: DemoMobileConnectionRepositoryKairos? = null
+ val connections by underTest.mobileConnectionsBySubId.map { it.values }.collectLastValue()
+
+ demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(currentEvent1)
+ fakeWifiEventFlow.value = currentEvent2
+ assertThat(connections).hasSize(2)
+ connections!!.forEach {
+ when (it.subId) {
+ 1 -> connection1 = it
+ 2 -> connection2 = it
+ else -> Assert.fail("Unexpected subscription")
+ }
+ }
+
+ assertConnection(connection1!!, currentEvent1)
+ assertCarrierMergedConnection(connection2!!, currentEvent2)
+
+ // WHEN the event changes for connection 2, it updates, and connection 1 stays the same
+ currentEvent2 = validCarrierMergedEvent(subId = 2, level = 4)
+ fakeWifiEventFlow.value = currentEvent2
+ assertConnection(connection1!!, currentEvent1)
+ assertCarrierMergedConnection(connection2!!, currentEvent2)
+
+ // and vice versa
+ currentEvent1 = validMobileEvent(subId = 1, inflateStrength = true)
+ demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(currentEvent1)
+ assertConnection(connection1!!, currentEvent1)
+ assertCarrierMergedConnection(connection2!!, currentEvent2)
+ }
+
+ @Test
+ fun demoIsNotInEcmState() = runTest {
+ underTest
+ assertThat(kairos.transact { underTest.isInEcmMode.sample() }).isFalse()
+ }
+
+ private suspend fun KairosTestScope.assertConnection(
+ conn: DemoMobileConnectionRepositoryKairos,
model: FakeNetworkEventModel,
) {
- val job = startCollection(conn)
- // Assert the fields using the `MutableStateFlow` so that we don't have to start up
- // a collector for every field for every test
when (model) {
is FakeNetworkEventModel.Mobile -> {
- assertThat(conn.subId).isEqualTo(model.subId)
- assertThat(conn.cdmaLevel.value).isEqualTo(model.level)
- assertThat(conn.primaryLevel.value).isEqualTo(model.level)
- assertThat(conn.dataActivityDirection.value)
- .isEqualTo((model.activity ?: DATA_ACTIVITY_NONE).toMobileDataActivityModel())
- assertThat(conn.carrierNetworkChangeActive.value)
- .isEqualTo(model.carrierNetworkChange)
- assertThat(conn.isRoaming.value).isEqualTo(model.roaming)
- assertThat(conn.networkName.value)
- .isEqualTo(NetworkNameModel.IntentDerived(model.name))
- assertThat(conn.carrierName.value)
- .isEqualTo(NetworkNameModel.SubscriptionDerived("${model.name} ${model.subId}"))
- assertThat(conn.hasPrioritizedNetworkCapabilities.value).isEqualTo(model.slice)
- assertThat(conn.isNonTerrestrial.value).isEqualTo(model.ntn)
+ kairos.transact {
+ assertThat(conn.subId).isEqualTo(model.subId)
+ assertThat(conn.cdmaLevel.sample()).isEqualTo(model.level)
+ assertThat(conn.primaryLevel.sample()).isEqualTo(model.level)
+ assertThat(conn.dataActivityDirection.sample())
+ .isEqualTo(
+ (model.activity ?: DATA_ACTIVITY_NONE).toMobileDataActivityModel()
+ )
+ assertThat(conn.carrierNetworkChangeActive.sample())
+ .isEqualTo(model.carrierNetworkChange)
+ assertThat(conn.isRoaming.sample()).isEqualTo(model.roaming)
+ assertThat(conn.networkName.sample())
+ .isEqualTo(NetworkNameModel.IntentDerived(model.name))
+ assertThat(conn.carrierName.sample())
+ .isEqualTo(
+ NetworkNameModel.SubscriptionDerived("${model.name} ${model.subId}")
+ )
+ assertThat(conn.hasPrioritizedNetworkCapabilities.sample())
+ .isEqualTo(model.slice)
+ assertThat(conn.isNonTerrestrial.sample()).isEqualTo(model.ntn)
- // TODO(b/261029387) check these once we start handling them
- assertThat(conn.isEmergencyOnly.value).isFalse()
- assertThat(conn.isGsm.value).isFalse()
- assertThat(conn.dataConnectionState.value).isEqualTo(DataConnectionState.Connected)
+ // TODO(b/261029387) check these once we start handling them
+ assertThat(conn.isEmergencyOnly.sample()).isFalse()
+ assertThat(conn.isGsm.sample()).isFalse()
+ assertThat(conn.dataConnectionState.sample())
+ .isEqualTo(DataConnectionState.Connected)
+ }
}
else -> {}
}
-
- job.cancel()
}
- private fun TestScope.assertCarrierMergedConnection(
- conn: DemoMobileConnectionRepository,
+ private suspend fun KairosTestScope.assertCarrierMergedConnection(
+ conn: DemoMobileConnectionRepositoryKairos,
model: FakeWifiEventModel.CarrierMerged,
) {
- val job = startCollection(conn)
- assertThat(conn.subId).isEqualTo(model.subscriptionId)
- assertThat(conn.cdmaLevel.value).isEqualTo(model.level)
- assertThat(conn.primaryLevel.value).isEqualTo(model.level)
- assertThat(conn.carrierNetworkChangeActive.value).isEqualTo(false)
- assertThat(conn.isRoaming.value).isEqualTo(false)
- assertThat(conn.isEmergencyOnly.value).isFalse()
- assertThat(conn.isGsm.value).isFalse()
- assertThat(conn.dataConnectionState.value).isEqualTo(DataConnectionState.Connected)
- assertThat(conn.hasPrioritizedNetworkCapabilities.value).isFalse()
- job.cancel()
+ kairos.transact {
+ assertThat(conn.subId).isEqualTo(model.subscriptionId)
+ assertThat(conn.cdmaLevel.sample()).isEqualTo(model.level)
+ assertThat(conn.primaryLevel.sample()).isEqualTo(model.level)
+ assertThat(conn.carrierNetworkChangeActive.sample()).isEqualTo(false)
+ assertThat(conn.isRoaming.sample()).isEqualTo(false)
+ assertThat(conn.isEmergencyOnly.sample()).isFalse()
+ assertThat(conn.isGsm.sample()).isFalse()
+ assertThat(conn.dataConnectionState.sample()).isEqualTo(DataConnectionState.Connected)
+ assertThat(conn.hasPrioritizedNetworkCapabilities.sample()).isFalse()
+ }
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryKairosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryKairosTest.kt
index 93d56d5..1838d13 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryKairosTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryKairosTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,281 +20,223 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.kairos.ActivatedKairosFixture
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosTestScope
+import com.android.systemui.kairos.runKairosTest
+import com.android.systemui.kairos.stateOf
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.log.table.logcatTableLogBuffer
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
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.shared.data.model.DataActivityModel
-import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.fakeWifiRepository
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.wifiRepository
import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
-import com.android.systemui.util.mockito.whenever
+import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.stub
+@OptIn(ExperimentalKairosApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class CarrierMergedConnectionRepositoryKairosTest : SysuiTestCase() {
- private lateinit var underTest: CarrierMergedConnectionRepositoryKairos
+ private val Kosmos.underTest by ActivatedKairosFixture {
+ CarrierMergedConnectionRepositoryKairos(
+ subId = SUB_ID,
+ tableLogBuffer = logcatTableLogBuffer(this),
+ telephonyManager = telephonyManager,
+ wifiRepository = wifiRepository,
+ isInEcmMode = stateOf(false),
+ )
+ }
- private lateinit var wifiRepository: FakeWifiRepository
- @Mock private lateinit var logger: TableLogBuffer
- @Mock private lateinit var telephonyManager: TelephonyManager
+ private val Kosmos.telephonyManager: TelephonyManager by Fixture {
+ mock {
+ on { subscriptionId } doReturn SUB_ID
+ on { simOperatorName } doReturn ""
+ }
+ }
- private val testDispatcher = UnconfinedTestDispatcher()
- private val testScope = TestScope(testDispatcher)
+ private fun runTest(block: suspend KairosTestScope.() -> Unit) =
+ testKosmos().run {
+ useUnconfinedTestDispatcher()
+ runKairosTest { block() }
+ }
- @Before
- fun setUp() {
- MockitoAnnotations.initMocks(this)
- whenever(telephonyManager.subscriptionId).thenReturn(SUB_ID)
- whenever(telephonyManager.simOperatorName).thenReturn("")
+ @Test
+ fun inactiveWifi_isDefault() = runTest {
+ val latestConnState by underTest.dataConnectionState.collectLastValue()
+ val latestNetType by underTest.resolvedNetworkType.collectLastValue()
- wifiRepository = FakeWifiRepository()
+ fakeWifiRepository.setWifiNetwork(WifiNetworkModel.Inactive())
- underTest =
- CarrierMergedConnectionRepositoryKairos(
- SUB_ID,
- logger,
- telephonyManager,
- testScope.backgroundScope.coroutineContext,
- testScope.backgroundScope,
- wifiRepository,
- )
+ assertThat(latestConnState).isEqualTo(DataConnectionState.Disconnected)
+ assertThat(latestNetType).isNotEqualTo(ResolvedNetworkType.CarrierMergedNetworkType)
}
@Test
- fun inactiveWifi_isDefault() =
- testScope.runTest {
- var latestConnState: DataConnectionState? = null
- var latestNetType: ResolvedNetworkType? = null
+ fun activeWifi_isDefault() = runTest {
+ val latestConnState by underTest.dataConnectionState.collectLastValue()
+ val latestNetType by underTest.resolvedNetworkType.collectLastValue()
- val dataJob =
- underTest.dataConnectionState.onEach { latestConnState = it }.launchIn(this)
- val netJob = underTest.resolvedNetworkType.onEach { latestNetType = it }.launchIn(this)
+ fakeWifiRepository.setWifiNetwork(WifiNetworkModel.Active.of(level = 1))
- wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive())
-
- assertThat(latestConnState).isEqualTo(DataConnectionState.Disconnected)
- assertThat(latestNetType).isNotEqualTo(ResolvedNetworkType.CarrierMergedNetworkType)
-
- dataJob.cancel()
- netJob.cancel()
- }
+ assertThat(latestConnState).isEqualTo(DataConnectionState.Disconnected)
+ assertThat(latestNetType).isNotEqualTo(ResolvedNetworkType.CarrierMergedNetworkType)
+ }
@Test
- fun activeWifi_isDefault() =
- testScope.runTest {
- var latestConnState: DataConnectionState? = null
- var latestNetType: ResolvedNetworkType? = null
+ fun carrierMergedWifi_isValidAndFieldsComeFromWifiNetwork() = runTest {
+ val latest by underTest.primaryLevel.collectLastValue()
- val dataJob =
- underTest.dataConnectionState.onEach { latestConnState = it }.launchIn(this)
- val netJob = underTest.resolvedNetworkType.onEach { latestNetType = it }.launchIn(this)
+ fakeWifiRepository.setIsWifiEnabled(true)
+ fakeWifiRepository.setIsWifiDefault(true)
- wifiRepository.setWifiNetwork(WifiNetworkModel.Active.of(level = 1))
+ fakeWifiRepository.setWifiNetwork(
+ WifiNetworkModel.CarrierMerged.of(subscriptionId = SUB_ID, level = 3)
+ )
- assertThat(latestConnState).isEqualTo(DataConnectionState.Disconnected)
- assertThat(latestNetType).isNotEqualTo(ResolvedNetworkType.CarrierMergedNetworkType)
-
- dataJob.cancel()
- netJob.cancel()
- }
+ assertThat(latest).isEqualTo(3)
+ }
@Test
- fun carrierMergedWifi_isValidAndFieldsComeFromWifiNetwork() =
- testScope.runTest {
- var latest: Int? = null
- val job = underTest.primaryLevel.onEach { latest = it }.launchIn(this)
+ fun activity_comesFromWifiActivity() = runTest {
+ val latest by underTest.dataActivityDirection.collectLastValue()
- wifiRepository.setIsWifiEnabled(true)
- wifiRepository.setIsWifiDefault(true)
+ fakeWifiRepository.setIsWifiEnabled(true)
+ fakeWifiRepository.setIsWifiDefault(true)
+ fakeWifiRepository.setWifiNetwork(
+ WifiNetworkModel.CarrierMerged.of(subscriptionId = SUB_ID, level = 3)
+ )
+ fakeWifiRepository.setWifiActivity(
+ DataActivityModel(hasActivityIn = true, hasActivityOut = false)
+ )
- wifiRepository.setWifiNetwork(
- WifiNetworkModel.CarrierMerged.of(subscriptionId = SUB_ID, level = 3)
- )
+ assertThat(latest!!.hasActivityIn).isTrue()
+ assertThat(latest!!.hasActivityOut).isFalse()
- assertThat(latest).isEqualTo(3)
+ fakeWifiRepository.setWifiActivity(
+ DataActivityModel(hasActivityIn = false, hasActivityOut = true)
+ )
- job.cancel()
- }
+ assertThat(latest!!.hasActivityIn).isFalse()
+ assertThat(latest!!.hasActivityOut).isTrue()
+ }
@Test
- fun activity_comesFromWifiActivity() =
- testScope.runTest {
- var latest: DataActivityModel? = null
- val job = underTest.dataActivityDirection.onEach { latest = it }.launchIn(this)
+ fun carrierMergedWifi_wrongSubId_isDefault() = runTest {
+ val latestLevel by underTest.primaryLevel.collectLastValue()
+ val latestType by underTest.resolvedNetworkType.collectLastValue()
- wifiRepository.setIsWifiEnabled(true)
- wifiRepository.setIsWifiDefault(true)
- wifiRepository.setWifiNetwork(
- WifiNetworkModel.CarrierMerged.of(subscriptionId = SUB_ID, level = 3)
- )
- wifiRepository.setWifiActivity(
- DataActivityModel(hasActivityIn = true, hasActivityOut = false)
- )
+ fakeWifiRepository.setWifiNetwork(
+ WifiNetworkModel.CarrierMerged.of(subscriptionId = SUB_ID + 10, level = 3)
+ )
- assertThat(latest!!.hasActivityIn).isTrue()
- assertThat(latest!!.hasActivityOut).isFalse()
-
- wifiRepository.setWifiActivity(
- DataActivityModel(hasActivityIn = false, hasActivityOut = true)
- )
-
- assertThat(latest!!.hasActivityIn).isFalse()
- assertThat(latest!!.hasActivityOut).isTrue()
-
- job.cancel()
- }
-
- @Test
- fun carrierMergedWifi_wrongSubId_isDefault() =
- testScope.runTest {
- var latestLevel: Int? = null
- var latestType: ResolvedNetworkType? = null
- val levelJob = underTest.primaryLevel.onEach { latestLevel = it }.launchIn(this)
- val typeJob = underTest.resolvedNetworkType.onEach { latestType = it }.launchIn(this)
-
- wifiRepository.setWifiNetwork(
- WifiNetworkModel.CarrierMerged.of(subscriptionId = SUB_ID + 10, level = 3)
- )
-
- assertThat(latestLevel).isNotEqualTo(3)
- assertThat(latestType).isNotEqualTo(ResolvedNetworkType.CarrierMergedNetworkType)
-
- levelJob.cancel()
- typeJob.cancel()
- }
+ assertThat(latestLevel).isNotEqualTo(3)
+ assertThat(latestType).isNotEqualTo(ResolvedNetworkType.CarrierMergedNetworkType)
+ }
// This scenario likely isn't possible, but write a test for it anyway
@Test
- fun carrierMergedButNotEnabled_isDefault() =
- testScope.runTest {
- var latest: Int? = null
- val job = underTest.primaryLevel.onEach { latest = it }.launchIn(this)
+ fun carrierMergedButNotEnabled_isDefault() = runTest {
+ val latest by underTest.primaryLevel.collectLastValue()
- wifiRepository.setWifiNetwork(
- WifiNetworkModel.CarrierMerged.of(subscriptionId = SUB_ID, level = 3)
- )
- wifiRepository.setIsWifiEnabled(false)
+ fakeWifiRepository.setWifiNetwork(
+ WifiNetworkModel.CarrierMerged.of(subscriptionId = SUB_ID, level = 3)
+ )
+ fakeWifiRepository.setIsWifiEnabled(false)
- assertThat(latest).isNotEqualTo(3)
-
- job.cancel()
- }
+ assertThat(latest).isNotEqualTo(3)
+ }
// This scenario likely isn't possible, but write a test for it anyway
@Test
- fun carrierMergedButWifiNotDefault_isDefault() =
- testScope.runTest {
- var latest: Int? = null
- val job = underTest.primaryLevel.onEach { latest = it }.launchIn(this)
+ fun carrierMergedButWifiNotDefault_isDefault() = runTest {
+ val latest by underTest.primaryLevel.collectLastValue()
- wifiRepository.setWifiNetwork(
- WifiNetworkModel.CarrierMerged.of(subscriptionId = SUB_ID, level = 3)
+ fakeWifiRepository.setWifiNetwork(
+ WifiNetworkModel.CarrierMerged.of(subscriptionId = SUB_ID, level = 3)
+ )
+ fakeWifiRepository.setIsWifiDefault(false)
+
+ assertThat(latest).isNotEqualTo(3)
+ }
+
+ @Test
+ fun numberOfLevels_comesFromCarrierMerged() = runTest {
+ val latest by underTest.numberOfLevels.collectLastValue()
+
+ fakeWifiRepository.setWifiNetwork(
+ WifiNetworkModel.CarrierMerged.of(
+ subscriptionId = SUB_ID,
+ level = 1,
+ numberOfLevels = 6,
)
- wifiRepository.setIsWifiDefault(false)
+ )
- assertThat(latest).isNotEqualTo(3)
-
- job.cancel()
- }
+ assertThat(latest).isEqualTo(6)
+ }
@Test
- fun numberOfLevels_comesFromCarrierMerged() =
- testScope.runTest {
- var latest: Int? = null
- val job = underTest.numberOfLevels.onEach { latest = it }.launchIn(this)
+ fun dataEnabled_matchesWifiEnabled() = runTest {
+ val latest by underTest.dataEnabled.collectLastValue()
- wifiRepository.setWifiNetwork(
- WifiNetworkModel.CarrierMerged.of(
- subscriptionId = SUB_ID,
- level = 1,
- numberOfLevels = 6,
- )
- )
+ fakeWifiRepository.setIsWifiEnabled(true)
+ assertThat(latest).isTrue()
- assertThat(latest).isEqualTo(6)
-
- job.cancel()
- }
+ fakeWifiRepository.setIsWifiEnabled(false)
+ assertThat(latest).isFalse()
+ }
@Test
- fun dataEnabled_matchesWifiEnabled() =
- testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.dataEnabled.onEach { latest = it }.launchIn(this)
-
- wifiRepository.setIsWifiEnabled(true)
- assertThat(latest).isTrue()
-
- wifiRepository.setIsWifiEnabled(false)
- assertThat(latest).isFalse()
-
- job.cancel()
- }
+ fun cdmaRoaming_alwaysFalse() = runTest {
+ val latest by underTest.cdmaRoaming.collectLastValue()
+ assertThat(latest).isFalse()
+ }
@Test
- fun cdmaRoaming_alwaysFalse() =
- testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.cdmaRoaming.onEach { latest = it }.launchIn(this)
+ fun networkName_usesSimOperatorNameAsInitial() = runTest {
+ telephonyManager.stub { on { simOperatorName } doReturn "Test SIM name" }
- assertThat(latest).isFalse()
+ val latest by underTest.networkName.collectLastValue()
- job.cancel()
- }
+ assertThat(latest).isEqualTo(NetworkNameModel.SimDerived("Test SIM name"))
+ }
@Test
- fun networkName_usesSimOperatorNameAsInitial() =
- testScope.runTest {
- whenever(telephonyManager.simOperatorName).thenReturn("Test SIM name")
+ fun networkName_updatesOnNetworkUpdate() = runTest {
+ fakeWifiRepository.setIsWifiEnabled(true)
+ fakeWifiRepository.setIsWifiDefault(true)
- var latest: NetworkNameModel? = null
- val job = underTest.networkName.onEach { latest = it }.launchIn(this)
+ telephonyManager.stub { on { simOperatorName } doReturn "Test SIM name" }
- assertThat(latest).isEqualTo(NetworkNameModel.SimDerived("Test SIM name"))
+ val latest by underTest.networkName.collectLastValue()
- job.cancel()
- }
+ assertThat(latest).isEqualTo(NetworkNameModel.SimDerived("Test SIM name"))
+
+ telephonyManager.stub { on { simOperatorName } doReturn "New SIM name" }
+ fakeWifiRepository.setWifiNetwork(
+ WifiNetworkModel.CarrierMerged.of(subscriptionId = SUB_ID, level = 3)
+ )
+
+ assertThat(latest).isEqualTo(NetworkNameModel.SimDerived("New SIM name"))
+ }
@Test
- fun networkName_updatesOnNetworkUpdate() =
- testScope.runTest {
- whenever(telephonyManager.simOperatorName).thenReturn("Test SIM name")
+ fun isAllowedDuringAirplaneMode_alwaysTrue() = runTest {
+ val latest by underTest.isAllowedDuringAirplaneMode.collectLastValue()
- var latest: NetworkNameModel? = null
- val job = underTest.networkName.onEach { latest = it }.launchIn(this)
-
- assertThat(latest).isEqualTo(NetworkNameModel.SimDerived("Test SIM name"))
-
- whenever(telephonyManager.simOperatorName).thenReturn("New SIM name")
- wifiRepository.setWifiNetwork(
- WifiNetworkModel.CarrierMerged.of(subscriptionId = SUB_ID, level = 3)
- )
-
- assertThat(latest).isEqualTo(NetworkNameModel.SimDerived("New SIM name"))
-
- job.cancel()
- }
-
- @Test
- fun isAllowedDuringAirplaneMode_alwaysTrue() =
- testScope.runTest {
- val latest by collectLastValue(underTest.isAllowedDuringAirplaneMode)
-
- assertThat(latest).isTrue()
- }
+ assertThat(latest).isTrue()
+ }
private companion object {
const val SUB_ID = 123
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryKairosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryKairosTest.kt
index 56d76fc..858bb09 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryKairosTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryKairosTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2025 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,7 +16,6 @@
package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
-import android.net.ConnectivityManager
import android.os.PersistableBundle
import android.telephony.ServiceState
import android.telephony.SignalStrength
@@ -26,616 +25,512 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.flags.FakeFeatureFlagsClassic
+import com.android.systemui.activated
import com.android.systemui.flags.Flags.ROAMING_INDICATOR_VIA_DISPLAY_INFO
+import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.flags.featureFlagsClassic
+import com.android.systemui.kairos.ActivatedKairosFixture
+import com.android.systemui.kairos.BuildSpec
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosTestScope
+import com.android.systemui.kairos.MutableState
+import com.android.systemui.kairos.buildSpec
+import com.android.systemui.kairos.kairos
+import com.android.systemui.kairos.runKairosTest
+import com.android.systemui.kairos.stateOf
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.log.table.logcatTableLogBuffer
-import com.android.systemui.log.table.tableLogBufferFactory
import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfig
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepositoryKairos
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepositoryKairos
import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_EMERGENCY
import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_OPERATOR
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_PRIMARY_LEVEL
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepositoryKairos.Companion.COL_PRIMARY_LEVEL
import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileTelephonyHelpers.getTelephonyCallbackForType
-import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.fakeWifiRepository
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.wifiRepository
import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
import com.android.systemui.testKosmos
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import java.io.PrintWriter
import java.io.StringWriter
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.Mockito.never
import org.mockito.Mockito.verify
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
/**
* This repo acts as a dispatcher to either the `typical` or `carrier merged` versions of the
* repository interface it's switching on. These tests just need to verify that the entire interface
* properly switches over when the value of `isCarrierMerged` changes.
*/
-@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalKairosApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class FullMobileConnectionRepositoryKairosTest : SysuiTestCase() {
- private val kosmos = testKosmos()
+ private val Kosmos.fakeMobileRepo by Fixture {
+ FakeMobileConnectionRepositoryKairos(SUB_ID, kairos, mobileLogger)
+ }
- private lateinit var underTest: FullMobileConnectionRepositoryKairos
+ private val Kosmos.fakeCarrierMergedRepo by Fixture {
+ FakeMobileConnectionRepositoryKairos(SUB_ID, kairos, mobileLogger).apply {
+ // Mimicks the real carrier merged repository
+ isAllowedDuringAirplaneMode.setValue(true)
+ }
+ }
- private val flags =
- FakeFeatureFlagsClassic().also { it.set(ROAMING_INDICATOR_VIA_DISPLAY_INFO, true) }
+ private var Kosmos.mobileRepo: MobileConnectionRepositoryKairos by Fixture { fakeMobileRepo }
+ private var Kosmos.carrierMergedRepoSpec:
+ BuildSpec<MobileConnectionRepositoryKairos> by Fixture {
+ buildSpec { fakeCarrierMergedRepo }
+ }
- private val testDispatcher = UnconfinedTestDispatcher()
- private val testScope = TestScope(testDispatcher)
- private val tableLogBuffer = logcatTableLogBuffer(kosmos, "TestName")
- private val mobileFactory = mock<MobileConnectionRepositoryImpl.Factory>()
- private val carrierMergedFactory = mock<CarrierMergedConnectionRepository.Factory>()
- private val connectivityManager = mock<ConnectivityManager>()
+ private val Kosmos.mobileLogger by Fixture { logcatTableLogBuffer(this, "TestName") }
- private val subscriptionModel =
- MutableStateFlow(
+ private val Kosmos.underTest by ActivatedKairosFixture {
+ FullMobileConnectionRepositoryKairos(
+ SUB_ID,
+ mobileLogger,
+ mobileRepo,
+ carrierMergedRepoSpec,
+ isCarrierMerged,
+ )
+ }
+
+ private val Kosmos.subscriptionModel by Fixture {
+ MutableState(
+ kairos,
SubscriptionModel(
subscriptionId = SUB_ID,
carrierName = DEFAULT_NAME,
profileClass = PROFILE_CLASS_UNSET,
- )
+ ),
)
+ }
+
+ private val Kosmos.isCarrierMerged by Fixture { MutableState(kairos, false) }
// Use a real config, with no overrides
private val systemUiCarrierConfig = SystemUiCarrierConfig(SUB_ID, PersistableBundle())
- private lateinit var mobileRepo: FakeMobileConnectionRepository
- private lateinit var carrierMergedRepo: FakeMobileConnectionRepository
+ private val kosmos =
+ testKosmos().apply {
+ useUnconfinedTestDispatcher()
+ fakeFeatureFlagsClassic.set(ROAMING_INDICATOR_VIA_DISPLAY_INFO, true)
+ }
- @Before
- fun setUp() {
- mobileRepo = FakeMobileConnectionRepository(SUB_ID, tableLogBuffer)
- carrierMergedRepo =
- FakeMobileConnectionRepository(SUB_ID, tableLogBuffer).apply {
- // Mimicks the real carrier merged repository
- this.isAllowedDuringAirplaneMode.value = true
- }
+ private fun runTest(block: suspend KairosTestScope.() -> Unit) =
+ kosmos.run { runKairosTest { block() } }
- whenever(mobileFactory.build(eq(SUB_ID), any(), any(), eq(DEFAULT_NAME_MODEL), eq(SEP)))
- .thenReturn(mobileRepo)
- whenever(carrierMergedFactory.build(eq(SUB_ID), any())).thenReturn(carrierMergedRepo)
+ @Test
+ fun startingIsCarrierMerged_usesCarrierMergedInitially() = runTest {
+ val carrierMergedOperatorName = "Carrier Merged Operator"
+ val nonCarrierMergedName = "Non-carrier-merged"
+
+ fakeCarrierMergedRepo.operatorAlphaShort.setValue(carrierMergedOperatorName)
+ fakeMobileRepo.operatorAlphaShort.setValue(nonCarrierMergedName)
+
+ isCarrierMerged.setValue(true)
+
+ val activeRepo by underTest.activeRepo.collectLastValue()
+ val operatorAlphaShort by underTest.operatorAlphaShort.collectLastValue()
+
+ assertThat(activeRepo).isEqualTo(fakeCarrierMergedRepo)
+ assertThat(operatorAlphaShort).isEqualTo(carrierMergedOperatorName)
}
@Test
- fun startingIsCarrierMerged_usesCarrierMergedInitially() =
- testScope.runTest {
- val carrierMergedOperatorName = "Carrier Merged Operator"
- val nonCarrierMergedName = "Non-carrier-merged"
+ fun startingNotCarrierMerged_usesTypicalInitially() = runTest {
+ val carrierMergedOperatorName = "Carrier Merged Operator"
+ val nonCarrierMergedName = "Typical Operator"
- carrierMergedRepo.operatorAlphaShort.value = carrierMergedOperatorName
- mobileRepo.operatorAlphaShort.value = nonCarrierMergedName
+ fakeCarrierMergedRepo.operatorAlphaShort.setValue(carrierMergedOperatorName)
+ fakeMobileRepo.operatorAlphaShort.setValue(nonCarrierMergedName)
+ isCarrierMerged.setValue(false)
- initializeRepo(startingIsCarrierMerged = true)
-
- assertThat(underTest.activeRepo.value).isEqualTo(carrierMergedRepo)
- assertThat(underTest.operatorAlphaShort.value).isEqualTo(carrierMergedOperatorName)
- verify(mobileFactory, never())
- .build(SUB_ID, tableLogBuffer, subscriptionModel, DEFAULT_NAME_MODEL, SEP)
- }
-
- @Test
- fun startingNotCarrierMerged_usesTypicalInitially() =
- testScope.runTest {
- val carrierMergedOperatorName = "Carrier Merged Operator"
- val nonCarrierMergedName = "Typical Operator"
-
- carrierMergedRepo.operatorAlphaShort.value = carrierMergedOperatorName
- mobileRepo.operatorAlphaShort.value = nonCarrierMergedName
-
- initializeRepo(startingIsCarrierMerged = false)
-
- assertThat(underTest.activeRepo.value).isEqualTo(mobileRepo)
- assertThat(underTest.operatorAlphaShort.value).isEqualTo(nonCarrierMergedName)
- verify(carrierMergedFactory, never()).build(SUB_ID, tableLogBuffer)
- }
-
- @Test
- fun activeRepo_matchesIsCarrierMerged() =
- testScope.runTest {
- initializeRepo(startingIsCarrierMerged = false)
- var latest: MobileConnectionRepository? = null
- val job = underTest.activeRepo.onEach { latest = it }.launchIn(this)
-
- underTest.setIsCarrierMerged(true)
-
- assertThat(latest).isEqualTo(carrierMergedRepo)
-
- underTest.setIsCarrierMerged(false)
-
- assertThat(latest).isEqualTo(mobileRepo)
-
- underTest.setIsCarrierMerged(true)
-
- assertThat(latest).isEqualTo(carrierMergedRepo)
-
- job.cancel()
- }
-
- @Test
- fun connectionInfo_getsUpdatesFromRepo_carrierMerged() =
- testScope.runTest {
- initializeRepo(startingIsCarrierMerged = false)
-
- var latestName: String? = null
- var latestLevel: Int? = null
-
- val nameJob = underTest.operatorAlphaShort.onEach { latestName = it }.launchIn(this)
- val levelJob = underTest.primaryLevel.onEach { latestLevel = it }.launchIn(this)
-
- underTest.setIsCarrierMerged(true)
-
- val operator1 = "Carrier Merged Operator"
- val level1 = 1
- carrierMergedRepo.operatorAlphaShort.value = operator1
- carrierMergedRepo.primaryLevel.value = level1
-
- assertThat(latestName).isEqualTo(operator1)
- assertThat(latestLevel).isEqualTo(level1)
-
- val operator2 = "Carrier Merged Operator #2"
- val level2 = 2
- carrierMergedRepo.operatorAlphaShort.value = operator2
- carrierMergedRepo.primaryLevel.value = level2
-
- assertThat(latestName).isEqualTo(operator2)
- assertThat(latestLevel).isEqualTo(level2)
-
- val operator3 = "Carrier Merged Operator #3"
- val level3 = 3
- carrierMergedRepo.operatorAlphaShort.value = operator3
- carrierMergedRepo.primaryLevel.value = level3
-
- assertThat(latestName).isEqualTo(operator3)
- assertThat(latestLevel).isEqualTo(level3)
-
- nameJob.cancel()
- levelJob.cancel()
- }
-
- @Test
- fun connectionInfo_getsUpdatesFromRepo_mobile() =
- testScope.runTest {
- initializeRepo(startingIsCarrierMerged = false)
-
- var latestName: String? = null
- var latestLevel: Int? = null
-
- val nameJob = underTest.operatorAlphaShort.onEach { latestName = it }.launchIn(this)
- val levelJob = underTest.primaryLevel.onEach { latestLevel = it }.launchIn(this)
-
- underTest.setIsCarrierMerged(false)
-
- val operator1 = "Typical Merged Operator"
- val level1 = 1
- mobileRepo.operatorAlphaShort.value = operator1
- mobileRepo.primaryLevel.value = level1
-
- assertThat(latestName).isEqualTo(operator1)
- assertThat(latestLevel).isEqualTo(level1)
-
- val operator2 = "Typical Merged Operator #2"
- val level2 = 2
- mobileRepo.operatorAlphaShort.value = operator2
- mobileRepo.primaryLevel.value = level2
-
- assertThat(latestName).isEqualTo(operator2)
- assertThat(latestLevel).isEqualTo(level2)
-
- val operator3 = "Typical Merged Operator #3"
- val level3 = 3
- mobileRepo.operatorAlphaShort.value = operator3
- mobileRepo.primaryLevel.value = level3
-
- assertThat(latestName).isEqualTo(operator3)
- assertThat(latestLevel).isEqualTo(level3)
-
- nameJob.cancel()
- levelJob.cancel()
- }
-
- @Test
- fun connectionInfo_updatesWhenCarrierMergedUpdates() =
- testScope.runTest {
- initializeRepo(startingIsCarrierMerged = false)
-
- var latestName: String? = null
- var latestLevel: Int? = null
-
- val nameJob = underTest.operatorAlphaShort.onEach { latestName = it }.launchIn(this)
- val levelJob = underTest.primaryLevel.onEach { latestLevel = it }.launchIn(this)
-
- val carrierMergedOperator = "Carrier Merged Operator"
- val carrierMergedLevel = 4
- carrierMergedRepo.operatorAlphaShort.value = carrierMergedOperator
- carrierMergedRepo.primaryLevel.value = carrierMergedLevel
-
- val mobileName = "Typical Operator"
- val mobileLevel = 2
- mobileRepo.operatorAlphaShort.value = mobileName
- mobileRepo.primaryLevel.value = mobileLevel
-
- // Start with the mobile info
- assertThat(latestName).isEqualTo(mobileName)
- assertThat(latestLevel).isEqualTo(mobileLevel)
-
- // WHEN isCarrierMerged is set to true
- underTest.setIsCarrierMerged(true)
-
- // THEN the carrier merged info is used
- assertThat(latestName).isEqualTo(carrierMergedOperator)
- assertThat(latestLevel).isEqualTo(carrierMergedLevel)
-
- val newCarrierMergedName = "New CM Operator"
- val newCarrierMergedLevel = 0
- carrierMergedRepo.operatorAlphaShort.value = newCarrierMergedName
- carrierMergedRepo.primaryLevel.value = newCarrierMergedLevel
-
- assertThat(latestName).isEqualTo(newCarrierMergedName)
- assertThat(latestLevel).isEqualTo(newCarrierMergedLevel)
-
- // WHEN isCarrierMerged is set to false
- underTest.setIsCarrierMerged(false)
-
- // THEN the typical info is used
- assertThat(latestName).isEqualTo(mobileName)
- assertThat(latestLevel).isEqualTo(mobileLevel)
-
- val newMobileName = "New MobileOperator"
- val newMobileLevel = 3
- mobileRepo.operatorAlphaShort.value = newMobileName
- mobileRepo.primaryLevel.value = newMobileLevel
-
- assertThat(latestName).isEqualTo(newMobileName)
- assertThat(latestLevel).isEqualTo(newMobileLevel)
-
- nameJob.cancel()
- levelJob.cancel()
- }
-
- @Test
- fun isAllowedDuringAirplaneMode_updatesWhenCarrierMergedUpdates() =
- testScope.runTest {
- initializeRepo(startingIsCarrierMerged = false)
-
- val latest by collectLastValue(underTest.isAllowedDuringAirplaneMode)
-
- assertThat(latest).isFalse()
-
- underTest.setIsCarrierMerged(true)
-
- assertThat(latest).isTrue()
-
- underTest.setIsCarrierMerged(false)
-
- assertThat(latest).isFalse()
- }
-
- @Test
- fun factory_reusesLogBuffersForSameConnection() =
- testScope.runTest {
- val factory =
- FullMobileConnectionRepository.Factory(
- scope = testScope.backgroundScope,
- kosmos.tableLogBufferFactory,
- mobileFactory,
- carrierMergedFactory,
- )
-
- // Create two connections for the same subId. Similar to if the connection appeared
- // and disappeared from the connectionFactory's perspective
- val connection1 =
- factory.build(
- SUB_ID,
- startingIsCarrierMerged = false,
- subscriptionModel,
- DEFAULT_NAME_MODEL,
- SEP,
- )
-
- val connection1Repeat =
- factory.build(
- SUB_ID,
- startingIsCarrierMerged = false,
- subscriptionModel,
- DEFAULT_NAME_MODEL,
- SEP,
- )
-
- assertThat(connection1.tableLogBuffer)
- .isSameInstanceAs(connection1Repeat.tableLogBuffer)
- }
-
- @Test
- fun factory_reusesLogBuffersForSameSubIDevenIfCarrierMerged() =
- testScope.runTest {
- val factory =
- FullMobileConnectionRepository.Factory(
- scope = testScope.backgroundScope,
- kosmos.tableLogBufferFactory,
- mobileFactory,
- carrierMergedFactory,
- )
-
- val connection1 =
- factory.build(
- SUB_ID,
- startingIsCarrierMerged = false,
- subscriptionModel,
- DEFAULT_NAME_MODEL,
- SEP,
- )
-
- // WHEN a connection with the same sub ID but carrierMerged = true is created
- val connection1Repeat =
- factory.build(
- SUB_ID,
- startingIsCarrierMerged = true,
- subscriptionModel,
- DEFAULT_NAME_MODEL,
- SEP,
- )
-
- // THEN the same table is re-used
- assertThat(connection1.tableLogBuffer)
- .isSameInstanceAs(connection1Repeat.tableLogBuffer)
- }
-
- @Test
- fun connectionInfo_logging_notCarrierMerged_getsUpdates() =
- testScope.runTest {
- // SETUP: Use real repositories to verify the diffing still works. (See b/267501739.)
- val telephonyManager =
- mock<TelephonyManager>().apply { whenever(this.simOperatorName).thenReturn("") }
- createRealMobileRepo(telephonyManager)
- createRealCarrierMergedRepo(telephonyManager, FakeWifiRepository())
-
- initializeRepo(startingIsCarrierMerged = false)
-
- val emergencyJob = underTest.isEmergencyOnly.launchIn(this)
- val operatorJob = underTest.operatorAlphaShort.launchIn(this)
-
- // WHEN we set up some mobile connection info
- val serviceState = ServiceState()
- serviceState.setOperatorName("longName", "OpTypical", "1")
- serviceState.isEmergencyOnly = true
- getTelephonyCallbackForType<TelephonyCallback.ServiceStateListener>(telephonyManager)
- .onServiceStateChanged(serviceState)
-
- // THEN it's logged to the buffer
- assertThat(dumpBuffer()).contains("$COL_OPERATOR${BUFFER_SEPARATOR}OpTypical")
- assertThat(dumpBuffer()).contains("$COL_EMERGENCY${BUFFER_SEPARATOR}true")
-
- // WHEN we update mobile connection info
- val serviceState2 = ServiceState()
- serviceState2.setOperatorName("longName", "OpDiff", "1")
- serviceState2.isEmergencyOnly = false
- getTelephonyCallbackForType<TelephonyCallback.ServiceStateListener>(telephonyManager)
- .onServiceStateChanged(serviceState2)
-
- // THEN the updates are logged
- assertThat(dumpBuffer()).contains("$COL_OPERATOR${BUFFER_SEPARATOR}OpDiff")
- assertThat(dumpBuffer()).contains("$COL_EMERGENCY${BUFFER_SEPARATOR}false")
-
- emergencyJob.cancel()
- operatorJob.cancel()
- }
-
- @Test
- fun connectionInfo_logging_carrierMerged_getsUpdates() =
- testScope.runTest {
- // SETUP: Use real repositories to verify the diffing still works. (See b/267501739.)
- val telephonyManager =
- mock<TelephonyManager>().apply { whenever(this.simOperatorName).thenReturn("") }
- createRealMobileRepo(telephonyManager)
- val wifiRepository = FakeWifiRepository()
- createRealCarrierMergedRepo(telephonyManager, wifiRepository)
-
- initializeRepo(startingIsCarrierMerged = true)
-
- val job = underTest.primaryLevel.launchIn(this)
-
- // WHEN we set up carrier merged info
- wifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged.of(SUB_ID, level = 3))
-
- // THEN the carrier merged info is logged
- assertThat(dumpBuffer()).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}3")
-
- // WHEN we update the info
- wifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged.of(SUB_ID, level = 1))
-
- // THEN the updates are logged
- assertThat(dumpBuffer()).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}1")
-
- job.cancel()
- }
-
- @Test
- fun connectionInfo_logging_updatesWhenCarrierMergedUpdates() =
- testScope.runTest {
- // SETUP: Use real repositories to verify the diffing still works. (See b/267501739.)
- val telephonyManager =
- mock<TelephonyManager>().apply { whenever(this.simOperatorName).thenReturn("") }
- createRealMobileRepo(telephonyManager)
-
- val wifiRepository = FakeWifiRepository()
- createRealCarrierMergedRepo(telephonyManager, wifiRepository)
-
- initializeRepo(startingIsCarrierMerged = false)
-
- val job = underTest.primaryLevel.launchIn(this)
-
- // WHEN we set up some mobile connection info
- val signalStrength = mock<SignalStrength>()
- whenever(signalStrength.level).thenReturn(1)
-
- getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>(telephonyManager)
- .onSignalStrengthsChanged(signalStrength)
-
- // THEN it's logged to the buffer
- assertThat(dumpBuffer()).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}1")
-
- // WHEN isCarrierMerged is set to true
- wifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged.of(SUB_ID, level = 3))
- underTest.setIsCarrierMerged(true)
-
- // THEN the carrier merged info is logged
- assertThat(dumpBuffer()).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}3")
-
- // WHEN the carrier merge network is updated
- wifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged.of(SUB_ID, level = 4))
-
- // THEN the new level is logged
- assertThat(dumpBuffer()).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}4")
-
- // WHEN isCarrierMerged is set to false
- underTest.setIsCarrierMerged(false)
-
- // THEN the typical info is logged
- // Note: Since our first logs also had the typical info, we need to search the log
- // contents for after our carrier merged level log.
- val fullBuffer = dumpBuffer()
- val carrierMergedContentIndex = fullBuffer.indexOf("${BUFFER_SEPARATOR}4")
- val bufferAfterCarrierMerged = fullBuffer.substring(carrierMergedContentIndex)
- assertThat(bufferAfterCarrierMerged).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}1")
-
- // WHEN the normal network is updated
- mobileRepo.primaryLevel.value = 0
-
- // THEN the new level is logged
- assertThat(dumpBuffer()).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}0")
-
- job.cancel()
- }
-
- @Test
- fun connectionInfo_logging_doesNotLogUpdatesForNotActiveRepo() =
- testScope.runTest {
- // SETUP: Use real repositories to verify the diffing still works. (See b/267501739.)
- val telephonyManager =
- mock<TelephonyManager>().apply { whenever(this.simOperatorName).thenReturn("") }
- createRealMobileRepo(telephonyManager)
-
- val wifiRepository = FakeWifiRepository()
- createRealCarrierMergedRepo(telephonyManager, wifiRepository)
-
- // WHEN isCarrierMerged = false
- initializeRepo(startingIsCarrierMerged = false)
-
- val job = underTest.primaryLevel.launchIn(this)
-
- val signalStrength = mock<SignalStrength>()
- whenever(signalStrength.level).thenReturn(1)
- getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>(telephonyManager)
- .onSignalStrengthsChanged(signalStrength)
-
- // THEN updates to the carrier merged level aren't logged
- wifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged.of(SUB_ID, level = 4))
- assertThat(dumpBuffer()).doesNotContain("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}4")
-
- wifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged.of(SUB_ID, level = 3))
- assertThat(dumpBuffer()).doesNotContain("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}3")
-
- // WHEN isCarrierMerged is set to true
- underTest.setIsCarrierMerged(true)
-
- // THEN updates to the normal level aren't logged
- whenever(signalStrength.level).thenReturn(5)
- getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>(telephonyManager)
- .onSignalStrengthsChanged(signalStrength)
- assertThat(dumpBuffer()).doesNotContain("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}5")
-
- whenever(signalStrength.level).thenReturn(6)
- getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>(telephonyManager)
- .onSignalStrengthsChanged(signalStrength)
- assertThat(dumpBuffer()).doesNotContain("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}6")
-
- job.cancel()
- }
-
- private fun initializeRepo(startingIsCarrierMerged: Boolean) {
- underTest =
- FullMobileConnectionRepositoryKairos(
- SUB_ID,
- startingIsCarrierMerged,
- tableLogBuffer,
- subscriptionModel,
- DEFAULT_NAME_MODEL,
- SEP,
- testScope.backgroundScope,
- mobileFactory,
- carrierMergedFactory,
- )
+ assertThat(underTest.activeRepo.collectLastValue().value).isEqualTo(fakeMobileRepo)
+ assertThat(underTest.operatorAlphaShort.collectLastValue().value)
+ .isEqualTo(nonCarrierMergedName)
}
- private fun createRealMobileRepo(
+ @Test
+ fun activeRepo_matchesIsCarrierMerged() = runTest {
+ isCarrierMerged.setValue(false)
+
+ val latest by underTest.activeRepo.collectLastValue()
+
+ isCarrierMerged.setValue(true)
+
+ assertThat(latest).isEqualTo(fakeCarrierMergedRepo)
+
+ isCarrierMerged.setValue(false)
+
+ assertThat(latest).isEqualTo(fakeMobileRepo)
+
+ isCarrierMerged.setValue(true)
+
+ assertThat(latest).isEqualTo(fakeCarrierMergedRepo)
+ }
+
+ @Test
+ fun connectionInfo_getsUpdatesFromRepo_carrierMerged() = runTest {
+ isCarrierMerged.setValue(false)
+
+ val latestName by underTest.operatorAlphaShort.collectLastValue()
+ val latestLevel by underTest.primaryLevel.collectLastValue()
+
+ isCarrierMerged.setValue(true)
+
+ val operator1 = "Carrier Merged Operator"
+ val level1 = 1
+ fakeCarrierMergedRepo.operatorAlphaShort.setValue(operator1)
+ fakeCarrierMergedRepo.primaryLevel.setValue(level1)
+
+ assertThat(latestName).isEqualTo(operator1)
+ assertThat(latestLevel).isEqualTo(level1)
+
+ val operator2 = "Carrier Merged Operator #2"
+ val level2 = 2
+ fakeCarrierMergedRepo.operatorAlphaShort.setValue(operator2)
+ fakeCarrierMergedRepo.primaryLevel.setValue(level2)
+
+ assertThat(latestName).isEqualTo(operator2)
+ assertThat(latestLevel).isEqualTo(level2)
+
+ val operator3 = "Carrier Merged Operator #3"
+ val level3 = 3
+ fakeCarrierMergedRepo.operatorAlphaShort.setValue(operator3)
+ fakeCarrierMergedRepo.primaryLevel.setValue(level3)
+
+ assertThat(latestName).isEqualTo(operator3)
+ assertThat(latestLevel).isEqualTo(level3)
+ }
+
+ @Test
+ fun connectionInfo_getsUpdatesFromRepo_mobile() = runTest {
+ isCarrierMerged.setValue(false)
+
+ val latestName by underTest.operatorAlphaShort.collectLastValue()
+ val latestLevel by underTest.primaryLevel.collectLastValue()
+
+ isCarrierMerged.setValue(false)
+
+ val operator1 = "Typical Merged Operator"
+ val level1 = 1
+ fakeMobileRepo.operatorAlphaShort.setValue(operator1)
+ fakeMobileRepo.primaryLevel.setValue(level1)
+
+ assertThat(latestName).isEqualTo(operator1)
+ assertThat(latestLevel).isEqualTo(level1)
+
+ val operator2 = "Typical Merged Operator #2"
+ val level2 = 2
+ fakeMobileRepo.operatorAlphaShort.setValue(operator2)
+ fakeMobileRepo.primaryLevel.setValue(level2)
+
+ assertThat(latestName).isEqualTo(operator2)
+ assertThat(latestLevel).isEqualTo(level2)
+
+ val operator3 = "Typical Merged Operator #3"
+ val level3 = 3
+ fakeMobileRepo.operatorAlphaShort.setValue(operator3)
+ fakeMobileRepo.primaryLevel.setValue(level3)
+
+ assertThat(latestName).isEqualTo(operator3)
+ assertThat(latestLevel).isEqualTo(level3)
+ }
+
+ @Test
+ fun connectionInfo_updatesWhenCarrierMergedUpdates() = runTest {
+ isCarrierMerged.setValue(false)
+
+ val latestName by underTest.operatorAlphaShort.collectLastValue()
+ val latestLevel by underTest.primaryLevel.collectLastValue()
+
+ val carrierMergedOperator = "Carrier Merged Operator"
+ val carrierMergedLevel = 4
+ fakeCarrierMergedRepo.operatorAlphaShort.setValue(carrierMergedOperator)
+ fakeCarrierMergedRepo.primaryLevel.setValue(carrierMergedLevel)
+
+ val mobileName = "Typical Operator"
+ val mobileLevel = 2
+ fakeMobileRepo.operatorAlphaShort.setValue(mobileName)
+ fakeMobileRepo.primaryLevel.setValue(mobileLevel)
+
+ // Start with the mobile info
+ assertThat(latestName).isEqualTo(mobileName)
+ assertThat(latestLevel).isEqualTo(mobileLevel)
+
+ // WHEN isCarrierMerged is set to true
+ isCarrierMerged.setValue(true)
+
+ // THEN the carrier merged info is used
+ assertThat(latestName).isEqualTo(carrierMergedOperator)
+ assertThat(latestLevel).isEqualTo(carrierMergedLevel)
+
+ val newCarrierMergedName = "New CM Operator"
+ val newCarrierMergedLevel = 0
+ fakeCarrierMergedRepo.operatorAlphaShort.setValue(newCarrierMergedName)
+ fakeCarrierMergedRepo.primaryLevel.setValue(newCarrierMergedLevel)
+
+ assertThat(latestName).isEqualTo(newCarrierMergedName)
+ assertThat(latestLevel).isEqualTo(newCarrierMergedLevel)
+
+ // WHEN isCarrierMerged is set to false
+ isCarrierMerged.setValue(false)
+
+ // THEN the typical info is used
+ assertThat(latestName).isEqualTo(mobileName)
+ assertThat(latestLevel).isEqualTo(mobileLevel)
+
+ val newMobileName = "New MobileOperator"
+ val newMobileLevel = 3
+ fakeMobileRepo.operatorAlphaShort.setValue(newMobileName)
+ fakeMobileRepo.primaryLevel.setValue(newMobileLevel)
+
+ assertThat(latestName).isEqualTo(newMobileName)
+ assertThat(latestLevel).isEqualTo(newMobileLevel)
+ }
+
+ @Test
+ fun isAllowedDuringAirplaneMode_updatesWhenCarrierMergedUpdates() = runTest {
+ isCarrierMerged.setValue(false)
+
+ val latest by underTest.isAllowedDuringAirplaneMode.collectLastValue()
+
+ assertThat(latest).isFalse()
+
+ isCarrierMerged.setValue(true)
+
+ assertThat(latest).isTrue()
+
+ isCarrierMerged.setValue(false)
+
+ assertThat(latest).isFalse()
+ }
+
+ @Test
+ fun connectionInfo_logging_notCarrierMerged_getsUpdates() = runTest {
+ // SETUP: Use real repositories to verify the diffing still works. (See b/267501739.)
+ val telephonyManager: TelephonyManager = mock {
+ on { simOperatorName } doReturn ""
+ on { subscriptionId } doReturn SUB_ID
+ }
+ fakeWifiRepository.setIsWifiEnabled(true)
+ fakeWifiRepository.setIsWifiDefault(true)
+ mobileRepo = createRealMobileRepo(telephonyManager)
+ carrierMergedRepoSpec = realCarrierMergedRepo(telephonyManager)
+
+ isCarrierMerged.setValue(false)
+
+ // Stand-up activated repository
+ underTest
+
+ // WHEN we set up some mobile connection info
+ val serviceState = ServiceState()
+ serviceState.setOperatorName("longName", "OpTypical", "1")
+ serviceState.isEmergencyOnly = true
+ getTelephonyCallbackForType<TelephonyCallback.ServiceStateListener>(telephonyManager)
+ .onServiceStateChanged(serviceState)
+
+ // THEN it's logged to the buffer
+ assertThat(dumpBuffer()).contains("$COL_OPERATOR${BUFFER_SEPARATOR}OpTypical")
+ assertThat(dumpBuffer()).contains("$COL_EMERGENCY${BUFFER_SEPARATOR}true")
+
+ // WHEN we update mobile connection info
+ val serviceState2 = ServiceState()
+ serviceState2.setOperatorName("longName", "OpDiff", "1")
+ serviceState2.isEmergencyOnly = false
+ getTelephonyCallbackForType<TelephonyCallback.ServiceStateListener>(telephonyManager)
+ .onServiceStateChanged(serviceState2)
+
+ // THEN the updates are logged
+ assertThat(dumpBuffer()).contains("$COL_OPERATOR${BUFFER_SEPARATOR}OpDiff")
+ assertThat(dumpBuffer()).contains("$COL_EMERGENCY${BUFFER_SEPARATOR}false")
+ }
+
+ @Test
+ fun connectionInfo_logging_carrierMerged_getsUpdates() = runTest {
+ // SETUP: Use real repositories to verify the diffing still works. (See b/267501739.)
+ val telephonyManager: TelephonyManager = mock {
+ on { simOperatorName } doReturn ""
+ on { subscriptionId } doReturn SUB_ID
+ }
+ fakeWifiRepository.setIsWifiEnabled(true)
+ fakeWifiRepository.setIsWifiDefault(true)
+ mobileRepo = createRealMobileRepo(telephonyManager)
+ carrierMergedRepoSpec = realCarrierMergedRepo(telephonyManager)
+
+ isCarrierMerged.setValue(true)
+
+ // Stand-up activated repository
+ underTest
+
+ // WHEN we set up carrier merged info
+ fakeWifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged.of(SUB_ID, level = 3))
+
+ // THEN the carrier merged info is logged
+ assertThat(dumpBuffer()).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}3")
+
+ // WHEN we update the info
+ fakeWifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged.of(SUB_ID, level = 1))
+
+ // THEN the updates are logged
+ assertThat(dumpBuffer()).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}1")
+ }
+
+ @Test
+ fun connectionInfo_logging_updatesWhenCarrierMergedUpdates() = runTest {
+ // SETUP: Use real repositories to verify the diffing still works. (See b/267501739.)
+ val telephonyManager: TelephonyManager = mock {
+ on { simOperatorName } doReturn ""
+ on { subscriptionId } doReturn SUB_ID
+ }
+ fakeWifiRepository.setIsWifiEnabled(true)
+ fakeWifiRepository.setIsWifiDefault(true)
+ mobileRepo = createRealMobileRepo(telephonyManager)
+ carrierMergedRepoSpec = realCarrierMergedRepo(telephonyManager)
+
+ isCarrierMerged.setValue(false)
+
+ // Stand-up activated repository
+ underTest
+
+ // WHEN we set up some mobile connection info
+ val cb =
+ getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>(telephonyManager)
+ cb.onSignalStrengthsChanged(mock(stubOnly = true) { on { level } doReturn 1 })
+
+ // THEN it's logged to the buffer
+ assertThat(dumpBuffer()).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}1")
+
+ // WHEN isCarrierMerged is set to true
+ fakeWifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged.of(SUB_ID, level = 3))
+ isCarrierMerged.setValue(true)
+
+ // THEN the carrier merged info is logged
+ assertThat(dumpBuffer()).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}3")
+
+ // WHEN the carrier merge network is updated
+ fakeWifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged.of(SUB_ID, level = 4))
+
+ // THEN the new level is logged
+ assertThat(dumpBuffer()).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}4")
+
+ // WHEN isCarrierMerged is set to false
+ isCarrierMerged.setValue(false)
+
+ // THEN the typical info is logged
+ // Note: Since our first logs also had the typical info, we need to search the log
+ // contents for after our carrier merged level log.
+ val fullBuffer = dumpBuffer()
+ val carrierMergedContentIndex = fullBuffer.indexOf("${BUFFER_SEPARATOR}4")
+ val bufferAfterCarrierMerged = fullBuffer.substring(carrierMergedContentIndex)
+ assertThat(bufferAfterCarrierMerged).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}1")
+
+ // WHEN the normal network is updated
+ cb.onSignalStrengthsChanged(mock(stubOnly = true) { on { level } doReturn 0 })
+
+ // THEN the new level is logged
+ assertThat(dumpBuffer()).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}0")
+ }
+
+ @Test
+ fun connectionInfo_logging_doesNotLogUpdatesForNotActiveRepo() = runTest {
+ // SETUP: Use real repositories to verify the diffing still works. (See b/267501739.)
+ val telephonyManager: TelephonyManager = mock {
+ on { simOperatorName } doReturn ""
+ on { subscriptionId } doReturn SUB_ID
+ }
+ fakeWifiRepository.setIsWifiEnabled(true)
+ fakeWifiRepository.setIsWifiDefault(true)
+ mobileRepo = createRealMobileRepo(telephonyManager)
+ carrierMergedRepoSpec = realCarrierMergedRepo(telephonyManager)
+
+ // WHEN isCarrierMerged = false
+ isCarrierMerged.setValue(false)
+
+ // Stand-up activated repository
+ underTest
+
+ fun setSignalLevel(newLevel: Int) {
+ val signalStrength =
+ mock<SignalStrength>(stubOnly = true) { on { level } doReturn newLevel }
+ argumentCaptor<TelephonyCallback>()
+ .apply { verify(telephonyManager).registerTelephonyCallback(any(), capture()) }
+ .allValues
+ .asSequence()
+ .filterIsInstance<TelephonyCallback.SignalStrengthsListener>()
+ .forEach { it.onSignalStrengthsChanged(signalStrength) }
+ }
+
+ // WHEN we set up some mobile connection info
+ setSignalLevel(1)
+
+ // THEN updates to the carrier merged level aren't logged
+ fakeWifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged.of(SUB_ID, level = 4))
+ assertThat(dumpBuffer()).doesNotContain("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}4")
+
+ fakeWifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged.of(SUB_ID, level = 3))
+ assertThat(dumpBuffer()).doesNotContain("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}3")
+
+ // WHEN isCarrierMerged is set to true
+ isCarrierMerged.setValue(true)
+
+ // THEN updates to the normal level aren't logged
+ setSignalLevel(5)
+ assertThat(dumpBuffer()).doesNotContain("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}5")
+
+ setSignalLevel(6)
+ assertThat(dumpBuffer()).doesNotContain("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}6")
+ }
+
+ private fun KairosTestScope.createRealMobileRepo(
telephonyManager: TelephonyManager
- ): MobileConnectionRepositoryImpl {
- whenever(telephonyManager.subscriptionId).thenReturn(SUB_ID)
- val realRepo =
- MobileConnectionRepositoryImpl(
- SUB_ID,
- context,
- subscriptionModel,
- DEFAULT_NAME_MODEL,
- SEP,
- connectivityManager,
- telephonyManager,
+ ): MobileConnectionRepositoryKairosImpl =
+ MobileConnectionRepositoryKairosImpl(
+ subId = SUB_ID,
+ context = context,
+ subscriptionModel = subscriptionModel,
+ defaultNetworkName = DEFAULT_NAME_MODEL,
+ networkNameSeparator = SEP,
+ connectivityManager = mock(stubOnly = true),
+ telephonyManager = telephonyManager,
systemUiCarrierConfig = systemUiCarrierConfig,
- fakeBroadcastDispatcher,
- mobileMappingsProxy = mock(),
- testDispatcher,
- logger = mock(),
- tableLogBuffer,
- flags,
- testScope.backgroundScope,
+ broadcastDispatcher = fakeBroadcastDispatcher,
+ mobileMappingsProxy = mock(stubOnly = true),
+ bgDispatcher = testDispatcher,
+ logger = mock(stubOnly = true),
+ tableLogBuffer = mobileLogger,
+ flags = featureFlagsClassic,
)
- whenever(mobileFactory.build(eq(SUB_ID), any(), any(), eq(DEFAULT_NAME_MODEL), eq(SEP)))
- .thenReturn(realRepo)
+ .activated()
- return realRepo
+ private fun Kosmos.realCarrierMergedRepo(
+ telephonyManager: TelephonyManager
+ ): BuildSpec<CarrierMergedConnectionRepositoryKairos> = buildSpec {
+ activated {
+ CarrierMergedConnectionRepositoryKairos(
+ subId = SUB_ID,
+ tableLogBuffer = mobileLogger,
+ telephonyManager = telephonyManager,
+ wifiRepository = wifiRepository,
+ isInEcmMode = stateOf(false),
+ )
+ }
}
- private fun createRealCarrierMergedRepo(
- telephonyManager: TelephonyManager,
- wifiRepository: FakeWifiRepository,
- ): CarrierMergedConnectionRepository {
- wifiRepository.setIsWifiEnabled(true)
- wifiRepository.setIsWifiDefault(true)
- val realRepo =
- CarrierMergedConnectionRepository(
- SUB_ID,
- tableLogBuffer,
- telephonyManager,
- testScope.backgroundScope.coroutineContext,
- testScope.backgroundScope,
- wifiRepository,
- )
- whenever(carrierMergedFactory.build(eq(SUB_ID), any())).thenReturn(realRepo)
-
- return realRepo
- }
-
- private fun dumpBuffer(): String {
+ private fun Kosmos.dumpBuffer(): String {
val outputWriter = StringWriter()
- tableLogBuffer.dump(PrintWriter(outputWriter), arrayOf())
+ mobileLogger.dump(PrintWriter(outputWriter), arrayOf())
return outputWriter.toString()
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryKairosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryKairosTest.kt
index 3a335b7..32fc359 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryKairosTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryKairosTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2025 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,8 +19,8 @@
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
-import android.net.ConnectivityManager
import android.net.ConnectivityManager.NetworkCallback
+import android.net.connectivityManager
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.telephony.AccessNetworkConstants.TRANSPORT_TYPE_WLAN
@@ -68,19 +68,32 @@
import android.telephony.TelephonyManager.EXTRA_SUBSCRIPTION_ID
import android.telephony.TelephonyManager.NETWORK_TYPE_LTE
import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
+import android.telephony.telephonyManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.mobile.MobileMappings
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.flags.FakeFeatureFlagsClassic
import com.android.systemui.flags.Flags.ROAMING_INDICATOR_VIA_DISPLAY_INFO
+import com.android.systemui.flags.fake
+import com.android.systemui.flags.featureFlagsClassic
+import com.android.systemui.kairos.ActivatedKairosFixture
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosTestScope
+import com.android.systemui.kairos.MutableState
+import com.android.systemui.kairos.kairos
+import com.android.systemui.kairos.runKairosTest
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogcatEchoTrackerAlways
import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.tableLogBufferFactory
import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
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.model.ResolvedNetworkType.DefaultNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.UnknownNetworkType
@@ -89,53 +102,495 @@
import com.android.systemui.statusbar.pipeline.mobile.data.model.testCarrierConfig
import com.android.systemui.statusbar.pipeline.mobile.data.model.testCarrierConfigWithOverride
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileMappingsProxy
import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileTelephonyHelpers.signalStrength
import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileTelephonyHelpers.telephonyDisplayInfo
-import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
-import com.android.systemui.util.mockito.withArgCaptor
+import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.Mock
import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.stub
+import org.mockito.kotlin.whenever
-@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalKairosApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class MobileConnectionRepositoryKairosTest : SysuiTestCase() {
- private lateinit var underTest: MobileConnectionRepositoryKairosImpl
- private val flags =
- FakeFeatureFlagsClassic().also { it.set(ROAMING_INDICATOR_VIA_DISPLAY_INFO, true) }
+ private val Kosmos.underTest by ActivatedKairosFixture {
+ MobileConnectionRepositoryKairosImpl(
+ SUB_1_ID,
+ context,
+ subscriptionModel,
+ DEFAULT_NAME_MODEL,
+ SEP,
+ connectivityManager,
+ telephonyManager,
+ systemUiCarrierConfig,
+ fakeBroadcastDispatcher,
+ mobileMappingsProxy,
+ testDispatcher,
+ logger,
+ tableLogger,
+ featureFlagsClassic,
+ )
+ }
- @Mock private lateinit var connectivityManager: ConnectivityManager
- @Mock private lateinit var telephonyManager: TelephonyManager
- @Mock private lateinit var logger: MobileInputLogger
- @Mock private lateinit var tableLogger: TableLogBuffer
- @Mock private lateinit var context: Context
+ private val Kosmos.logger: MobileInputLogger by Fixture {
+ MobileInputLogger(LogBuffer("test_buffer", 1, LogcatEchoTrackerAlways()))
+ }
- private val mobileMappings = FakeMobileMappingsProxy()
+ private val Kosmos.tableLogger: TableLogBuffer by Fixture {
+ tableLogBufferFactory.getOrCreate("test_buffer", 1)
+ }
+
+ private val Kosmos.context: Context by Fixture { mock() }
+
private val systemUiCarrierConfig = SystemUiCarrierConfig(SUB_1_ID, testCarrierConfig())
- private val testDispatcher = UnconfinedTestDispatcher()
- private val testScope = TestScope(testDispatcher)
+ private val Kosmos.subscriptionModel: MutableState<SubscriptionModel?> by Fixture {
+ MutableState(
+ kairos,
+ SubscriptionModel(
+ subscriptionId = SUB_1_ID,
+ carrierName = DEFAULT_NAME,
+ profileClass = PROFILE_CLASS_UNSET,
+ ),
+ )
+ }
- private val subscriptionModel: MutableStateFlow<SubscriptionModel?> =
- MutableStateFlow(
+ private val kosmos =
+ testKosmos().apply {
+ useUnconfinedTestDispatcher()
+ featureFlagsClassic.fake.set(ROAMING_INDICATOR_VIA_DISPLAY_INFO, true)
+ telephonyManager.stub { on { subscriptionId } doReturn SUB_1_ID }
+ }
+
+ private fun runTest(block: suspend KairosTestScope.() -> Unit) =
+ kosmos.run { runKairosTest { block() } }
+
+ @Test
+ fun emergencyOnly() = runTest {
+ val latest by underTest.isEmergencyOnly.collectLastValue()
+
+ val serviceState = ServiceState().apply { isEmergencyOnly = true }
+
+ getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState)
+
+ assertThat(latest).isEqualTo(true)
+ }
+
+ @Test
+ fun emergencyOnly_toggles() = runTest {
+ val latest by underTest.isEmergencyOnly.collectLastValue()
+
+ val callback = getTelephonyCallbackForType<ServiceStateListener>()
+ callback.onServiceStateChanged(ServiceState().apply { isEmergencyOnly = true })
+
+ assertThat(latest).isTrue()
+
+ callback.onServiceStateChanged(ServiceState().apply { isEmergencyOnly = false })
+
+ assertThat(latest).isFalse()
+ }
+
+ @Test
+ fun cdmaLevelUpdates() = runTest {
+ val latest by underTest.cdmaLevel.collectLastValue()
+
+ val callback = getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>()
+ var strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = true)
+ callback.onSignalStrengthsChanged(strength)
+
+ assertThat(latest).isEqualTo(2)
+
+ // gsmLevel updates, no change to cdmaLevel
+ strength = signalStrength(gsmLevel = 3, cdmaLevel = 2, isGsm = true)
+ callback.onSignalStrengthsChanged(strength)
+
+ assertThat(latest).isEqualTo(2)
+ }
+
+ @Test
+ fun gsmLevelUpdates() = runTest {
+ val latest by underTest.primaryLevel.collectLastValue()
+
+ val callback = getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>()
+ var strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = true)
+ callback.onSignalStrengthsChanged(strength)
+
+ assertThat(latest).isEqualTo(1)
+
+ strength = signalStrength(gsmLevel = 3, cdmaLevel = 2, isGsm = true)
+ callback.onSignalStrengthsChanged(strength)
+
+ assertThat(latest).isEqualTo(3)
+ }
+
+ @Test
+ fun isGsm() = runTest {
+ val latest by underTest.isGsm.collectLastValue()
+
+ val callback = getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>()
+ var strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = true)
+ callback.onSignalStrengthsChanged(strength)
+
+ assertThat(latest).isTrue()
+
+ strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = false)
+ callback.onSignalStrengthsChanged(strength)
+
+ assertThat(latest).isFalse()
+ }
+
+ @Test
+ fun dataConnectionState_connected() = runTest {
+ val latest by underTest.dataConnectionState.collectLastValue()
+
+ val callback = getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
+ callback.onDataConnectionStateChanged(DATA_CONNECTED, 200 /* unused */)
+
+ assertThat(latest).isEqualTo(DataConnectionState.Connected)
+ }
+
+ @Test
+ fun dataConnectionState_connecting() = runTest {
+ val latest by underTest.dataConnectionState.collectLastValue()
+
+ val callback = getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
+ callback.onDataConnectionStateChanged(DATA_CONNECTING, 200 /* unused */)
+
+ assertThat(latest).isEqualTo(DataConnectionState.Connecting)
+ }
+
+ @Test
+ fun dataConnectionState_disconnected() = runTest {
+ val latest by underTest.dataConnectionState.collectLastValue()
+
+ val callback = getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
+ callback.onDataConnectionStateChanged(DATA_DISCONNECTED, 200 /* unused */)
+
+ assertThat(latest).isEqualTo(DataConnectionState.Disconnected)
+ }
+
+ @Test
+ fun dataConnectionState_disconnecting() = runTest {
+ val latest by underTest.dataConnectionState.collectLastValue()
+
+ val callback = getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
+ callback.onDataConnectionStateChanged(DATA_DISCONNECTING, 200 /* unused */)
+
+ assertThat(latest).isEqualTo(DataConnectionState.Disconnecting)
+ }
+
+ @Test
+ fun dataConnectionState_suspended() = runTest {
+ val latest by underTest.dataConnectionState.collectLastValue()
+
+ val callback = getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
+ callback.onDataConnectionStateChanged(DATA_SUSPENDED, 200 /* unused */)
+
+ assertThat(latest).isEqualTo(DataConnectionState.Suspended)
+ }
+
+ @Test
+ fun dataConnectionState_handoverInProgress() = runTest {
+ val latest by underTest.dataConnectionState.collectLastValue()
+
+ val callback = getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
+ callback.onDataConnectionStateChanged(DATA_HANDOVER_IN_PROGRESS, 200 /* unused */)
+
+ assertThat(latest).isEqualTo(DataConnectionState.HandoverInProgress)
+ }
+
+ @Test
+ fun dataConnectionState_unknown() = runTest {
+ val latest by underTest.dataConnectionState.collectLastValue()
+
+ val callback = getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
+ callback.onDataConnectionStateChanged(DATA_UNKNOWN, 200 /* unused */)
+
+ assertThat(latest).isEqualTo(DataConnectionState.Unknown)
+ }
+
+ @Test
+ fun dataConnectionState_invalid() = runTest {
+ val latest by underTest.dataConnectionState.collectLastValue()
+
+ val callback = getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
+ callback.onDataConnectionStateChanged(45, 200 /* unused */)
+
+ assertThat(latest).isEqualTo(DataConnectionState.Invalid)
+ }
+
+ @Test
+ fun dataActivity() = runTest {
+ val latest by underTest.dataActivityDirection.collectLastValue()
+
+ val callback = getTelephonyCallbackForType<DataActivityListener>()
+ callback.onDataActivity(DATA_ACTIVITY_INOUT)
+
+ assertThat(latest).isEqualTo(DATA_ACTIVITY_INOUT.toMobileDataActivityModel())
+ }
+
+ @Test
+ fun carrierId_initialValueCaptured() = runTest {
+ whenever(telephonyManager.simCarrierId).thenReturn(1234)
+
+ val latest by underTest.carrierId.collectLastValue()
+
+ assertThat(latest).isEqualTo(1234)
+ }
+
+ @Test
+ fun carrierId_updatesOnBroadcast() = runTest {
+ whenever(telephonyManager.simCarrierId).thenReturn(1234)
+
+ val latest by underTest.carrierId.collectLastValue()
+
+ fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+ context,
+ carrierIdIntent(carrierId = 4321),
+ )
+
+ assertThat(latest).isEqualTo(4321)
+ }
+
+ @Test
+ fun carrierNetworkChange() = runTest {
+ val latest by underTest.carrierNetworkChangeActive.collectLastValue()
+
+ val callback = getTelephonyCallbackForType<TelephonyCallback.CarrierNetworkListener>()
+ callback.onCarrierNetworkChange(true)
+
+ assertThat(latest).isEqualTo(true)
+ }
+
+ @Test
+ fun networkType_default() = runTest {
+ val latest by underTest.resolvedNetworkType.collectLastValue()
+
+ val expected = UnknownNetworkType
+
+ assertThat(latest).isEqualTo(expected)
+ }
+
+ @Test
+ fun networkType_unknown_hasCorrectKey() = runTest {
+ val latest by underTest.resolvedNetworkType.collectLastValue()
+
+ val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>()
+ val ti =
+ telephonyDisplayInfo(
+ networkType = NETWORK_TYPE_UNKNOWN,
+ overrideNetworkType = NETWORK_TYPE_UNKNOWN,
+ )
+
+ callback.onDisplayInfoChanged(ti)
+
+ val expected = UnknownNetworkType
+ assertThat(latest).isEqualTo(expected)
+ assertThat(latest!!.lookupKey).isEqualTo(MobileMappings.toIconKey(NETWORK_TYPE_UNKNOWN))
+ }
+
+ @Test
+ fun networkType_updatesUsingDefault() = runTest {
+ val latest by underTest.resolvedNetworkType.collectLastValue()
+
+ val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>()
+ val overrideType = OVERRIDE_NETWORK_TYPE_NONE
+ val type = NETWORK_TYPE_LTE
+ val ti = telephonyDisplayInfo(networkType = type, overrideNetworkType = overrideType)
+ callback.onDisplayInfoChanged(ti)
+
+ val expected = DefaultNetworkType(mobileMappingsProxy.toIconKey(type))
+ assertThat(latest).isEqualTo(expected)
+ }
+
+ @Test
+ fun networkType_updatesUsingOverride() = runTest {
+ val latest by underTest.resolvedNetworkType.collectLastValue()
+
+ val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>()
+ val type = OVERRIDE_NETWORK_TYPE_LTE_CA
+ val ti = telephonyDisplayInfo(networkType = type, overrideNetworkType = type)
+ callback.onDisplayInfoChanged(ti)
+
+ val expected = OverrideNetworkType(mobileMappingsProxy.toIconKeyOverride(type))
+ assertThat(latest).isEqualTo(expected)
+ }
+
+ @Test
+ fun networkType_unknownNetworkWithOverride_usesOverrideKey() = runTest {
+ val latest by underTest.resolvedNetworkType.collectLastValue()
+
+ val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>()
+ val unknown = NETWORK_TYPE_UNKNOWN
+ val type = OVERRIDE_NETWORK_TYPE_LTE_CA
+ val ti = telephonyDisplayInfo(unknown, type)
+ callback.onDisplayInfoChanged(ti)
+
+ val expected = OverrideNetworkType(mobileMappingsProxy.toIconKeyOverride(type))
+ assertThat(latest).isEqualTo(expected)
+ }
+
+ @Test
+ fun dataEnabled_initial_false() = runTest {
+ whenever(telephonyManager.isDataConnectionAllowed).thenReturn(false)
+
+ val latest by underTest.dataEnabled.collectLastValue()
+
+ assertThat(latest).isFalse()
+ }
+
+ @Test
+ fun isDataEnabled_tracksTelephonyCallback() = runTest {
+ val latest by underTest.dataEnabled.collectLastValue()
+
+ whenever(telephonyManager.isDataConnectionAllowed).thenReturn(false)
+ assertThat(latest).isFalse()
+
+ val callback = getTelephonyCallbackForType<TelephonyCallback.DataEnabledListener>()
+
+ callback.onDataEnabledChanged(true, 1)
+ assertThat(latest).isTrue()
+
+ callback.onDataEnabledChanged(false, 1)
+ assertThat(latest).isFalse()
+ }
+
+ @Test
+ fun numberOfLevels_isDefault() = runTest {
+ val latest by underTest.numberOfLevels.collectLastValue()
+
+ assertThat(latest).isEqualTo(DEFAULT_NUM_LEVELS)
+ }
+
+ @Test
+ fun roaming_cdma_queriesTelephonyManager() = runTest {
+ val latest by underTest.cdmaRoaming.collectLastValue()
+
+ val cb = getTelephonyCallbackForType<ServiceStateListener>()
+
+ // CDMA roaming is off, GSM roaming is on
+ whenever(telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber).thenReturn(ERI_OFF)
+ cb.onServiceStateChanged(ServiceState().also { it.roaming = true })
+
+ assertThat(latest).isFalse()
+
+ // CDMA roaming is on, GSM roaming is off
+ whenever(telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber).thenReturn(ERI_ON)
+ cb.onServiceStateChanged(ServiceState().also { it.roaming = false })
+
+ assertThat(latest).isTrue()
+ }
+
+ /**
+ * [TelephonyManager.getCdmaEnhancedRoamingIndicatorDisplayNumber] returns -1 if the service is
+ * not running or if there is an error while retrieving the cdma ERI
+ */
+ @Test
+ fun cdmaRoaming_ignoresNegativeOne() = runTest {
+ val latest by underTest.cdmaRoaming.collectLastValue()
+
+ val serviceState = ServiceState()
+ serviceState.roaming = false
+
+ val cb = getTelephonyCallbackForType<ServiceStateListener>()
+
+ // CDMA roaming is unavailable (-1), GSM roaming is off
+ whenever(telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber).thenReturn(-1)
+ cb.onServiceStateChanged(serviceState)
+
+ assertThat(latest).isFalse()
+ }
+
+ @Test
+ fun roaming_gsm_queriesDisplayInfo_viaDisplayInfo() = runTest {
+ // GIVEN flag is true
+ featureFlagsClassic.fake.set(ROAMING_INDICATOR_VIA_DISPLAY_INFO, true)
+
+ val latest by underTest.isRoaming.collectLastValue()
+
+ val cb = getTelephonyCallbackForType<DisplayInfoListener>()
+
+ // CDMA roaming is off, GSM roaming is off
+ whenever(telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber).thenReturn(ERI_OFF)
+ cb.onDisplayInfoChanged(TelephonyDisplayInfo(NETWORK_TYPE_LTE, NETWORK_TYPE_UNKNOWN, false))
+
+ assertThat(latest).isFalse()
+
+ // CDMA roaming is off, GSM roaming is on
+ cb.onDisplayInfoChanged(TelephonyDisplayInfo(NETWORK_TYPE_LTE, NETWORK_TYPE_UNKNOWN, true))
+
+ assertThat(latest).isTrue()
+ }
+
+ @Test
+ fun roaming_gsm_queriesDisplayInfo_viaServiceState() = runTest {
+ // GIVEN flag is false
+ featureFlagsClassic.fake.set(ROAMING_INDICATOR_VIA_DISPLAY_INFO, false)
+
+ val latest by underTest.isRoaming.collectLastValue()
+
+ val cb = getTelephonyCallbackForType<ServiceStateListener>()
+
+ // CDMA roaming is off, GSM roaming is off
+ whenever(telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber).thenReturn(ERI_OFF)
+ cb.onServiceStateChanged(ServiceState().also { it.roaming = false })
+
+ assertThat(latest).isFalse()
+
+ // CDMA roaming is off, GSM roaming is on
+ cb.onServiceStateChanged(ServiceState().also { it.roaming = true })
+
+ assertThat(latest).isTrue()
+ }
+
+ @Test
+ fun activity_updatesFromCallback() = runTest {
+ val latest by underTest.dataActivityDirection.collectLastValue()
+
+ assertThat(latest)
+ .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false))
+
+ val cb = getTelephonyCallbackForType<DataActivityListener>()
+ cb.onDataActivity(DATA_ACTIVITY_IN)
+ assertThat(latest)
+ .isEqualTo(DataActivityModel(hasActivityIn = true, hasActivityOut = false))
+
+ cb.onDataActivity(DATA_ACTIVITY_OUT)
+ assertThat(latest)
+ .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = true))
+
+ cb.onDataActivity(DATA_ACTIVITY_INOUT)
+ assertThat(latest).isEqualTo(DataActivityModel(hasActivityIn = true, hasActivityOut = true))
+
+ cb.onDataActivity(DATA_ACTIVITY_NONE)
+ assertThat(latest)
+ .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false))
+
+ cb.onDataActivity(DATA_ACTIVITY_DORMANT)
+ assertThat(latest)
+ .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false))
+
+ cb.onDataActivity(1234)
+ assertThat(latest)
+ .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false))
+ }
+
+ @Test
+ fun networkNameForSubId_updates() = runTest {
+ val latest by underTest.carrierName.collectLastValue()
+
+ subscriptionModel.setValue(
SubscriptionModel(
subscriptionId = SUB_1_ID,
carrierName = DEFAULT_NAME,
@@ -143,1277 +598,596 @@
)
)
- @Before
- fun setUp() {
- MockitoAnnotations.initMocks(this)
- whenever(telephonyManager.subscriptionId).thenReturn(SUB_1_ID)
+ assertThat(latest?.name).isEqualTo(DEFAULT_NAME)
- underTest =
- MobileConnectionRepositoryKairosImpl(
- SUB_1_ID,
- context,
- subscriptionModel,
- DEFAULT_NAME_MODEL,
- SEP,
- connectivityManager,
- telephonyManager,
- systemUiCarrierConfig,
- fakeBroadcastDispatcher,
- mobileMappings,
- testDispatcher,
- logger,
- tableLogger,
- flags,
- testScope.backgroundScope,
+ val updatedName = "Derived Carrier"
+ subscriptionModel.setValue(
+ SubscriptionModel(
+ subscriptionId = SUB_1_ID,
+ carrierName = updatedName,
+ profileClass = PROFILE_CLASS_UNSET,
)
+ )
+
+ assertThat(latest?.name).isEqualTo(updatedName)
}
@Test
- fun emergencyOnly() =
- testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isEmergencyOnly.onEach { latest = it }.launchIn(this)
+ fun networkNameForSubId_defaultWhenSubscriptionModelNull() = runTest {
+ val latest by underTest.carrierName.collectLastValue()
- val serviceState = ServiceState()
- serviceState.isEmergencyOnly = true
+ subscriptionModel.setValue(null)
- getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState)
-
- assertThat(latest).isEqualTo(true)
-
- job.cancel()
- }
+ assertThat(latest?.name).isEqualTo(DEFAULT_NAME)
+ }
@Test
- fun emergencyOnly_toggles() =
- testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isEmergencyOnly.onEach { latest = it }.launchIn(this)
+ fun networkName_default() = runTest {
+ val latest by underTest.networkName.collectLastValue()
- val callback = getTelephonyCallbackForType<ServiceStateListener>()
- callback.onServiceStateChanged(ServiceState().also { it.isEmergencyOnly = true })
- assertThat(latest).isTrue()
-
- callback.onServiceStateChanged(ServiceState().also { it.isEmergencyOnly = false })
-
- assertThat(latest).isFalse()
-
- job.cancel()
- }
+ assertThat(latest).isEqualTo(DEFAULT_NAME_MODEL)
+ }
@Test
- fun cdmaLevelUpdates() =
- testScope.runTest {
- var latest: Int? = null
- val job = underTest.cdmaLevel.onEach { latest = it }.launchIn(this)
+ @EnableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
+ fun networkName_usesBroadcastInfo_returnsDerived() = runTest {
+ val latest by underTest.networkName.collectLastValue()
- val callback = getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>()
- var strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = true)
- callback.onSignalStrengthsChanged(strength)
+ val intent = spnIntent()
+ val captor = argumentCaptor<BroadcastReceiver>()
+ verify(context).registerReceiver(captor.capture(), any())
+ captor.lastValue.onReceive(context, intent)
- assertThat(latest).isEqualTo(2)
-
- // gsmLevel updates, no change to cdmaLevel
- strength = signalStrength(gsmLevel = 3, cdmaLevel = 2, isGsm = true)
- callback.onSignalStrengthsChanged(strength)
-
- assertThat(latest).isEqualTo(2)
-
- job.cancel()
- }
+ // spnIntent() sets all values to true and test strings
+ assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN"))
+ }
@Test
- fun gsmLevelUpdates() =
- testScope.runTest {
- var latest: Int? = null
- val job = underTest.primaryLevel.onEach { latest = it }.launchIn(this)
+ @DisableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
+ fun networkName_usesBroadcastInfo_returnsDerived_flagOff() = runTest {
+ val latest by underTest.networkName.collectLastValue()
- val callback = getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>()
- var strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = true)
- callback.onSignalStrengthsChanged(strength)
+ val intent = spnIntent()
+ val captor = argumentCaptor<BroadcastReceiver>()
+ verify(context).registerReceiver(captor.capture(), any())
+ captor.lastValue.onReceive(context, intent)
- assertThat(latest).isEqualTo(1)
-
- strength = signalStrength(gsmLevel = 3, cdmaLevel = 2, isGsm = true)
- callback.onSignalStrengthsChanged(strength)
-
- assertThat(latest).isEqualTo(3)
-
- job.cancel()
- }
+ // spnIntent() sets all values to true and test strings
+ assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN"))
+ }
@Test
- fun isGsm() =
- testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isGsm.onEach { latest = it }.launchIn(this)
+ @EnableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
+ fun networkName_broadcastNotForThisSubId_keepsOldValue() = runTest {
+ val latest by underTest.networkName.collectLastValue()
- val callback = getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>()
- var strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = true)
- callback.onSignalStrengthsChanged(strength)
+ val intent = spnIntent()
+ val captor = argumentCaptor<BroadcastReceiver>()
+ verify(context).registerReceiver(captor.capture(), any())
+ captor.lastValue.onReceive(context, intent)
- assertThat(latest).isTrue()
+ assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN"))
- strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = false)
- callback.onSignalStrengthsChanged(strength)
+ // WHEN an intent with a different subId is sent
+ val wrongSubIntent = spnIntent(subId = 101)
- assertThat(latest).isFalse()
+ captor.lastValue.onReceive(context, wrongSubIntent)
- job.cancel()
- }
+ // THEN the previous intent's name is still used
+ assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN"))
+ }
@Test
- fun dataConnectionState_connected() =
- testScope.runTest {
- var latest: DataConnectionState? = null
- val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this)
+ @DisableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
+ fun networkName_broadcastNotForThisSubId_keepsOldValue_flagOff() = runTest {
+ val latest by underTest.networkName.collectLastValue()
- val callback =
- getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
- callback.onDataConnectionStateChanged(DATA_CONNECTED, 200 /* unused */)
+ val intent = spnIntent()
+ val captor = argumentCaptor<BroadcastReceiver>()
+ verify(context).registerReceiver(captor.capture(), any())
+ captor.lastValue.onReceive(context, intent)
- assertThat(latest).isEqualTo(DataConnectionState.Connected)
+ assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN"))
- job.cancel()
- }
+ // WHEN an intent with a different subId is sent
+ val wrongSubIntent = spnIntent(subId = 101)
+
+ captor.lastValue.onReceive(context, wrongSubIntent)
+
+ // THEN the previous intent's name is still used
+ assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN"))
+ }
@Test
- fun dataConnectionState_connecting() =
- testScope.runTest {
- var latest: DataConnectionState? = null
- val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this)
+ @EnableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
+ fun networkName_broadcastHasNoData_updatesToDefault() = runTest {
+ val latest by underTest.networkName.collectLastValue()
- val callback =
- getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
- callback.onDataConnectionStateChanged(DATA_CONNECTING, 200 /* unused */)
+ val intent = spnIntent()
+ val captor = argumentCaptor<BroadcastReceiver>()
+ verify(context).registerReceiver(captor.capture(), any())
+ captor.lastValue.onReceive(context, intent)
- assertThat(latest).isEqualTo(DataConnectionState.Connecting)
+ assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN"))
- job.cancel()
- }
+ val intentWithoutInfo = spnIntent(showSpn = false, showPlmn = false)
+
+ captor.lastValue.onReceive(context, intentWithoutInfo)
+
+ assertThat(latest).isEqualTo(DEFAULT_NAME_MODEL)
+ }
@Test
- fun dataConnectionState_disconnected() =
- testScope.runTest {
- var latest: DataConnectionState? = null
- val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this)
+ @DisableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
+ fun networkName_broadcastHasNoData_updatesToDefault_flagOff() = runTest {
+ val latest by underTest.networkName.collectLastValue()
- val callback =
- getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
- callback.onDataConnectionStateChanged(DATA_DISCONNECTED, 200 /* unused */)
+ val intent = spnIntent()
+ val captor = argumentCaptor<BroadcastReceiver>()
+ verify(context).registerReceiver(captor.capture(), any())
+ captor.lastValue.onReceive(context, intent)
- assertThat(latest).isEqualTo(DataConnectionState.Disconnected)
+ assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN"))
- job.cancel()
- }
+ val intentWithoutInfo = spnIntent(showSpn = false, showPlmn = false)
+
+ captor.lastValue.onReceive(context, intentWithoutInfo)
+
+ assertThat(latest).isEqualTo(DEFAULT_NAME_MODEL)
+ }
@Test
- fun dataConnectionState_disconnecting() =
- testScope.runTest {
- var latest: DataConnectionState? = null
- val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this)
+ @EnableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
+ fun networkName_usingEagerStrategy_retainsNameBetweenSubscribers() = runTest {
+ // Use the [StateFlow.value] getter so we can prove that the collection happens
+ // even when there is no [Job]
- val callback =
- getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
- callback.onDataConnectionStateChanged(DATA_DISCONNECTING, 200 /* unused */)
+ // Starts out default
+ val latest by underTest.networkName.collectLastValue()
+ assertThat(latest).isEqualTo(DEFAULT_NAME_MODEL)
- assertThat(latest).isEqualTo(DataConnectionState.Disconnecting)
+ val intent = spnIntent()
+ val captor = argumentCaptor<BroadcastReceiver>()
+ verify(context).registerReceiver(captor.capture(), any())
+ captor.lastValue.onReceive(context, intent)
- job.cancel()
- }
+ // The value is still there despite no active subscribers
+ assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN"))
+ }
@Test
- fun dataConnectionState_suspended() =
- testScope.runTest {
- var latest: DataConnectionState? = null
- val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this)
+ @DisableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
+ fun networkName_usingEagerStrategy_retainsNameBetweenSubscribers_flagOff() = runTest {
+ // Use the [StateFlow.value] getter so we can prove that the collection happens
+ // even when there is no [Job]
- val callback =
- getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
- callback.onDataConnectionStateChanged(DATA_SUSPENDED, 200 /* unused */)
+ // Starts out default
+ val latest by underTest.networkName.collectLastValue()
+ assertThat(latest).isEqualTo(DEFAULT_NAME_MODEL)
- assertThat(latest).isEqualTo(DataConnectionState.Suspended)
+ val intent = spnIntent()
+ val captor = argumentCaptor<BroadcastReceiver>()
+ verify(context).registerReceiver(captor.capture(), any())
+ captor.lastValue.onReceive(context, intent)
- job.cancel()
- }
+ // The value is still there despite no active subscribers
+ assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN"))
+ }
@Test
- fun dataConnectionState_handoverInProgress() =
- testScope.runTest {
- var latest: DataConnectionState? = null
- val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this)
+ @EnableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
+ fun networkName_allFieldsSet_prioritizesDataSpnOverSpn() = runTest {
+ val latest by underTest.networkName.collectLastValue()
+ val captor = argumentCaptor<BroadcastReceiver>()
+ verify(context).registerReceiver(captor.capture(), any())
- val callback =
- getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
- callback.onDataConnectionStateChanged(DATA_HANDOVER_IN_PROGRESS, 200 /* unused */)
-
- assertThat(latest).isEqualTo(DataConnectionState.HandoverInProgress)
-
- job.cancel()
- }
+ val intent =
+ spnIntent(
+ subId = SUB_1_ID,
+ showSpn = true,
+ spn = SPN,
+ dataSpn = DATA_SPN,
+ showPlmn = true,
+ plmn = PLMN,
+ )
+ captor.lastValue.onReceive(context, intent)
+ assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN"))
+ }
@Test
- fun dataConnectionState_unknown() =
- testScope.runTest {
- var latest: DataConnectionState? = null
- val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this)
+ @EnableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
+ fun networkName_spnAndPlmn_fallbackToSpnWhenNullDataSpn() = runTest {
+ val latest by underTest.networkName.collectLastValue()
+ val captor = argumentCaptor<BroadcastReceiver>()
+ verify(context).registerReceiver(captor.capture(), any())
- val callback =
- getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
- callback.onDataConnectionStateChanged(DATA_UNKNOWN, 200 /* unused */)
-
- assertThat(latest).isEqualTo(DataConnectionState.Unknown)
-
- job.cancel()
- }
+ val intent =
+ spnIntent(
+ subId = SUB_1_ID,
+ showSpn = true,
+ spn = SPN,
+ dataSpn = null,
+ showPlmn = true,
+ plmn = PLMN,
+ )
+ captor.lastValue.onReceive(context, intent)
+ assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$SPN"))
+ }
@Test
- fun dataConnectionState_invalid() =
- testScope.runTest {
- var latest: DataConnectionState? = null
- val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this)
+ @DisableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
+ fun networkName_allFieldsSet_flagOff() = runTest {
+ val latest by underTest.networkName.collectLastValue()
+ val captor = argumentCaptor<BroadcastReceiver>()
+ verify(context).registerReceiver(captor.capture(), any())
- val callback =
- getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>()
- callback.onDataConnectionStateChanged(45, 200 /* unused */)
-
- assertThat(latest).isEqualTo(DataConnectionState.Invalid)
-
- job.cancel()
- }
+ val intent =
+ spnIntent(
+ subId = SUB_1_ID,
+ showSpn = true,
+ spn = SPN,
+ dataSpn = DATA_SPN,
+ showPlmn = true,
+ plmn = PLMN,
+ )
+ captor.lastValue.onReceive(context, intent)
+ assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN"))
+ }
@Test
- fun dataActivity() =
- testScope.runTest {
- var latest: DataActivityModel? = null
- val job = underTest.dataActivityDirection.onEach { latest = it }.launchIn(this)
-
- val callback = getTelephonyCallbackForType<DataActivityListener>()
- callback.onDataActivity(DATA_ACTIVITY_INOUT)
-
- assertThat(latest).isEqualTo(DATA_ACTIVITY_INOUT.toMobileDataActivityModel())
-
- job.cancel()
- }
+ @EnableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
+ fun networkName_showPlmn_plmnNotNull_showSpn_spnNull_dataSpnNotNull() = runTest {
+ val latest by underTest.networkName.collectLastValue()
+ val captor = argumentCaptor<BroadcastReceiver>()
+ verify(context).registerReceiver(captor.capture(), any())
+ val intent =
+ spnIntent(
+ subId = SUB_1_ID,
+ showSpn = true,
+ spn = null,
+ dataSpn = DATA_SPN,
+ showPlmn = true,
+ plmn = PLMN,
+ )
+ captor.lastValue.onReceive(context, intent)
+ assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN"))
+ }
@Test
- fun carrierId_initialValueCaptured() =
- testScope.runTest {
- whenever(telephonyManager.simCarrierId).thenReturn(1234)
-
- var latest: Int? = null
- val job = underTest.carrierId.onEach { latest = it }.launchIn(this)
-
- assertThat(latest).isEqualTo(1234)
-
- job.cancel()
- }
+ @EnableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
+ fun networkName_showPlmn_plmnNotNull_showSpn_spnNotNull_dataSpnNull() = runTest {
+ val latest by underTest.networkName.collectLastValue()
+ val captor = argumentCaptor<BroadcastReceiver>()
+ verify(context).registerReceiver(captor.capture(), any())
+ val intent =
+ spnIntent(
+ subId = SUB_1_ID,
+ showSpn = true,
+ spn = SPN,
+ dataSpn = null,
+ showPlmn = true,
+ plmn = PLMN,
+ )
+ captor.lastValue.onReceive(context, intent)
+ assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$SPN"))
+ }
@Test
- fun carrierId_updatesOnBroadcast() =
- testScope.runTest {
- whenever(telephonyManager.simCarrierId).thenReturn(1234)
+ @DisableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
+ fun networkName_showPlmn_plmnNotNull_showSpn_spnNull_dataSpnNotNull_flagOff() = runTest {
+ val latest by underTest.networkName.collectLastValue()
+ val captor = argumentCaptor<BroadcastReceiver>()
+ verify(context).registerReceiver(captor.capture(), any())
+ val intent =
+ spnIntent(
+ subId = SUB_1_ID,
+ showSpn = true,
+ spn = null,
+ dataSpn = DATA_SPN,
+ showPlmn = true,
+ plmn = PLMN,
+ )
+ captor.lastValue.onReceive(context, intent)
+ assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN"))
+ }
- var latest: Int? = null
- val job = underTest.carrierId.onEach { latest = it }.launchIn(this)
+ @Test
+ fun networkName_showPlmn_noShowSPN() = runTest {
+ val latest by underTest.networkName.collectLastValue()
+ val captor = argumentCaptor<BroadcastReceiver>()
+ verify(context).registerReceiver(captor.capture(), any())
+ val intent =
+ spnIntent(
+ subId = SUB_1_ID,
+ showSpn = false,
+ spn = SPN,
+ dataSpn = DATA_SPN,
+ showPlmn = true,
+ plmn = PLMN,
+ )
+ captor.lastValue.onReceive(context, intent)
+ assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN"))
+ }
- fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
- context,
- carrierIdIntent(carrierId = 4321),
+ @Test
+ @EnableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
+ fun networkName_showPlmn_plmnNull_showSpn() = runTest {
+ val latest by underTest.networkName.collectLastValue()
+ val captor = argumentCaptor<BroadcastReceiver>()
+ verify(context).registerReceiver(captor.capture(), any())
+ val intent =
+ spnIntent(
+ subId = SUB_1_ID,
+ showSpn = true,
+ spn = SPN,
+ dataSpn = DATA_SPN,
+ showPlmn = true,
+ plmn = null,
+ )
+ captor.lastValue.onReceive(context, intent)
+ assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$DATA_SPN"))
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
+ fun networkName_showPlmn_plmnNull_showSpn_dataSpnNull() = runTest {
+ val latest by underTest.networkName.collectLastValue()
+ val captor = argumentCaptor<BroadcastReceiver>()
+ verify(context).registerReceiver(captor.capture(), any())
+ val intent =
+ spnIntent(
+ subId = SUB_1_ID,
+ showSpn = true,
+ spn = SPN,
+ dataSpn = null,
+ showPlmn = true,
+ plmn = null,
+ )
+ captor.lastValue.onReceive(context, intent)
+ assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$SPN"))
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
+ fun networkName_showPlmn_plmnNull_showSpn_bothSpnNull() = runTest {
+ val latest by underTest.networkName.collectLastValue()
+ val captor = argumentCaptor<BroadcastReceiver>()
+ verify(context).registerReceiver(captor.capture(), any())
+ val intent =
+ spnIntent(
+ subId = SUB_1_ID,
+ showSpn = true,
+ spn = null,
+ dataSpn = null,
+ showPlmn = true,
+ plmn = null,
+ )
+ captor.lastValue.onReceive(context, intent)
+ assertThat(latest).isEqualTo(DEFAULT_NAME_MODEL)
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
+ fun networkName_showPlmn_plmnNull_showSpn_flagOff() = runTest {
+ val latest by underTest.networkName.collectLastValue()
+ val captor = argumentCaptor<BroadcastReceiver>()
+ verify(context).registerReceiver(captor.capture(), any())
+ val intent =
+ spnIntent(
+ subId = SUB_1_ID,
+ showSpn = true,
+ spn = SPN,
+ dataSpn = DATA_SPN,
+ showPlmn = true,
+ plmn = null,
+ )
+ captor.lastValue.onReceive(context, intent)
+ assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$DATA_SPN"))
+ }
+
+ @Test
+ fun operatorAlphaShort_tracked() = runTest {
+ val latest by underTest.operatorAlphaShort.collectLastValue()
+
+ val shortName = "short name"
+ val serviceState = ServiceState()
+ serviceState.setOperatorName(
+ /* longName */ "long name",
+ /* shortName */ shortName,
+ /* numeric */ "12345",
+ )
+
+ getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState)
+
+ assertThat(latest).isEqualTo(shortName)
+ }
+
+ @Test
+ fun isInService_notIwlan() = runTest {
+ val latest by underTest.isInService.collectLastValue()
+
+ val nriInService =
+ NetworkRegistrationInfo.Builder()
+ .setDomain(DOMAIN_PS)
+ .setTransportType(TRANSPORT_TYPE_WWAN)
+ .setRegistrationState(REGISTRATION_STATE_HOME)
+ .build()
+
+ getTelephonyCallbackForType<ServiceStateListener>()
+ .onServiceStateChanged(
+ ServiceState().also {
+ it.voiceRegState = STATE_IN_SERVICE
+ it.addNetworkRegistrationInfo(nriInService)
+ }
)
- assertThat(latest).isEqualTo(4321)
+ assertThat(latest).isTrue()
- job.cancel()
- }
-
- @Test
- fun carrierNetworkChange() =
- testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.carrierNetworkChangeActive.onEach { latest = it }.launchIn(this)
-
- val callback = getTelephonyCallbackForType<TelephonyCallback.CarrierNetworkListener>()
- callback.onCarrierNetworkChange(true)
-
- assertThat(latest).isEqualTo(true)
-
- job.cancel()
- }
-
- @Test
- fun networkType_default() =
- testScope.runTest {
- var latest: ResolvedNetworkType? = null
- val job = underTest.resolvedNetworkType.onEach { latest = it }.launchIn(this)
-
- val expected = UnknownNetworkType
-
- assertThat(latest).isEqualTo(expected)
-
- job.cancel()
- }
-
- @Test
- fun networkType_unknown_hasCorrectKey() =
- testScope.runTest {
- var latest: ResolvedNetworkType? = null
- val job = underTest.resolvedNetworkType.onEach { latest = it }.launchIn(this)
-
- val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>()
- val ti =
- telephonyDisplayInfo(
- networkType = NETWORK_TYPE_UNKNOWN,
- overrideNetworkType = NETWORK_TYPE_UNKNOWN,
- )
-
- callback.onDisplayInfoChanged(ti)
-
- val expected = UnknownNetworkType
- assertThat(latest).isEqualTo(expected)
- assertThat(latest!!.lookupKey).isEqualTo(MobileMappings.toIconKey(NETWORK_TYPE_UNKNOWN))
-
- job.cancel()
- }
-
- @Test
- fun networkType_updatesUsingDefault() =
- testScope.runTest {
- var latest: ResolvedNetworkType? = null
- val job = underTest.resolvedNetworkType.onEach { latest = it }.launchIn(this)
-
- val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>()
- val overrideType = OVERRIDE_NETWORK_TYPE_NONE
- val type = NETWORK_TYPE_LTE
- val ti = telephonyDisplayInfo(networkType = type, overrideNetworkType = overrideType)
- callback.onDisplayInfoChanged(ti)
-
- val expected = DefaultNetworkType(mobileMappings.toIconKey(type))
- assertThat(latest).isEqualTo(expected)
-
- job.cancel()
- }
-
- @Test
- fun networkType_updatesUsingOverride() =
- testScope.runTest {
- var latest: ResolvedNetworkType? = null
- val job = underTest.resolvedNetworkType.onEach { latest = it }.launchIn(this)
-
- val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>()
- val type = OVERRIDE_NETWORK_TYPE_LTE_CA
- val ti = telephonyDisplayInfo(networkType = type, overrideNetworkType = type)
- callback.onDisplayInfoChanged(ti)
-
- val expected = OverrideNetworkType(mobileMappings.toIconKeyOverride(type))
- assertThat(latest).isEqualTo(expected)
-
- job.cancel()
- }
-
- @Test
- fun networkType_unknownNetworkWithOverride_usesOverrideKey() =
- testScope.runTest {
- var latest: ResolvedNetworkType? = null
- val job = underTest.resolvedNetworkType.onEach { latest = it }.launchIn(this)
-
- val callback = getTelephonyCallbackForType<TelephonyCallback.DisplayInfoListener>()
- val unknown = NETWORK_TYPE_UNKNOWN
- val type = OVERRIDE_NETWORK_TYPE_LTE_CA
- val ti = telephonyDisplayInfo(unknown, type)
- callback.onDisplayInfoChanged(ti)
-
- val expected = OverrideNetworkType(mobileMappings.toIconKeyOverride(type))
- assertThat(latest).isEqualTo(expected)
-
- job.cancel()
- }
-
- @Test
- fun dataEnabled_initial_false() =
- testScope.runTest {
- whenever(telephonyManager.isDataConnectionAllowed).thenReturn(false)
-
- assertThat(underTest.dataEnabled.value).isFalse()
- }
-
- @Test
- fun isDataEnabled_tracksTelephonyCallback() =
- testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.dataEnabled.onEach { latest = it }.launchIn(this)
-
- whenever(telephonyManager.isDataConnectionAllowed).thenReturn(false)
- assertThat(underTest.dataEnabled.value).isFalse()
-
- val callback = getTelephonyCallbackForType<TelephonyCallback.DataEnabledListener>()
-
- callback.onDataEnabledChanged(true, 1)
- assertThat(latest).isTrue()
-
- callback.onDataEnabledChanged(false, 1)
- assertThat(latest).isFalse()
-
- job.cancel()
- }
-
- @Test
- fun numberOfLevels_isDefault() =
- testScope.runTest {
- var latest: Int? = null
- val job = underTest.numberOfLevels.onEach { latest = it }.launchIn(this)
-
- assertThat(latest).isEqualTo(DEFAULT_NUM_LEVELS)
-
- job.cancel()
- }
-
- @Test
- fun roaming_cdma_queriesTelephonyManager() =
- testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.cdmaRoaming.onEach { latest = it }.launchIn(this)
-
- val cb = getTelephonyCallbackForType<ServiceStateListener>()
-
- // CDMA roaming is off, GSM roaming is on
- whenever(telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber).thenReturn(ERI_OFF)
- cb.onServiceStateChanged(ServiceState().also { it.roaming = true })
-
- assertThat(latest).isFalse()
-
- // CDMA roaming is on, GSM roaming is off
- whenever(telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber).thenReturn(ERI_ON)
- cb.onServiceStateChanged(ServiceState().also { it.roaming = false })
-
- assertThat(latest).isTrue()
-
- job.cancel()
- }
-
- /**
- * [TelephonyManager.getCdmaEnhancedRoamingIndicatorDisplayNumber] returns -1 if the service is
- * not running or if there is an error while retrieving the cdma ERI
- */
- @Test
- fun cdmaRoaming_ignoresNegativeOne() =
- testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.cdmaRoaming.onEach { latest = it }.launchIn(this)
-
- val serviceState = ServiceState()
- serviceState.roaming = false
-
- val cb = getTelephonyCallbackForType<ServiceStateListener>()
-
- // CDMA roaming is unavailable (-1), GSM roaming is off
- whenever(telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber).thenReturn(-1)
- cb.onServiceStateChanged(serviceState)
-
- assertThat(latest).isFalse()
-
- job.cancel()
- }
-
- @Test
- fun roaming_gsm_queriesDisplayInfo_viaDisplayInfo() =
- testScope.runTest {
- // GIVEN flag is true
- flags.set(ROAMING_INDICATOR_VIA_DISPLAY_INFO, true)
-
- // Re-create the repository, because the flag is read at init
- underTest =
- MobileConnectionRepositoryKairosImpl(
- SUB_1_ID,
- context,
- subscriptionModel,
- DEFAULT_NAME_MODEL,
- SEP,
- connectivityManager,
- telephonyManager,
- systemUiCarrierConfig,
- fakeBroadcastDispatcher,
- mobileMappings,
- testDispatcher,
- logger,
- tableLogger,
- flags,
- testScope.backgroundScope,
- )
-
- var latest: Boolean? = null
- val job = underTest.isRoaming.onEach { latest = it }.launchIn(this)
-
- val cb = getTelephonyCallbackForType<DisplayInfoListener>()
-
- // CDMA roaming is off, GSM roaming is off
- whenever(telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber).thenReturn(ERI_OFF)
- cb.onDisplayInfoChanged(
- TelephonyDisplayInfo(NETWORK_TYPE_LTE, NETWORK_TYPE_UNKNOWN, false, false, false)
- )
-
- assertThat(latest).isFalse()
-
- // CDMA roaming is off, GSM roaming is on
- cb.onDisplayInfoChanged(
- TelephonyDisplayInfo(NETWORK_TYPE_LTE, NETWORK_TYPE_UNKNOWN, true, false, false)
- )
-
- assertThat(latest).isTrue()
-
- job.cancel()
- }
-
- @Test
- fun roaming_gsm_queriesDisplayInfo_viaServiceState() =
- testScope.runTest {
- // GIVEN flag is false
- flags.set(ROAMING_INDICATOR_VIA_DISPLAY_INFO, false)
-
- // Re-create the repository, because the flag is read at init
- underTest =
- MobileConnectionRepositoryKairosImpl(
- SUB_1_ID,
- context,
- subscriptionModel,
- DEFAULT_NAME_MODEL,
- SEP,
- connectivityManager,
- telephonyManager,
- systemUiCarrierConfig,
- fakeBroadcastDispatcher,
- mobileMappings,
- testDispatcher,
- logger,
- tableLogger,
- flags,
- testScope.backgroundScope,
- )
-
- var latest: Boolean? = null
- val job = underTest.isRoaming.onEach { latest = it }.launchIn(this)
-
- val cb = getTelephonyCallbackForType<ServiceStateListener>()
-
- // CDMA roaming is off, GSM roaming is off
- whenever(telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber).thenReturn(ERI_OFF)
- cb.onServiceStateChanged(ServiceState().also { it.roaming = false })
-
- assertThat(latest).isFalse()
-
- // CDMA roaming is off, GSM roaming is on
- cb.onServiceStateChanged(ServiceState().also { it.roaming = true })
-
- assertThat(latest).isTrue()
-
- job.cancel()
- }
-
- @Test
- fun activity_updatesFromCallback() =
- testScope.runTest {
- var latest: DataActivityModel? = null
- val job = underTest.dataActivityDirection.onEach { latest = it }.launchIn(this)
-
- assertThat(latest)
- .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false))
-
- val cb = getTelephonyCallbackForType<DataActivityListener>()
- cb.onDataActivity(DATA_ACTIVITY_IN)
- assertThat(latest)
- .isEqualTo(DataActivityModel(hasActivityIn = true, hasActivityOut = false))
-
- cb.onDataActivity(DATA_ACTIVITY_OUT)
- assertThat(latest)
- .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = true))
-
- cb.onDataActivity(DATA_ACTIVITY_INOUT)
- assertThat(latest)
- .isEqualTo(DataActivityModel(hasActivityIn = true, hasActivityOut = true))
-
- cb.onDataActivity(DATA_ACTIVITY_NONE)
- assertThat(latest)
- .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false))
-
- cb.onDataActivity(DATA_ACTIVITY_DORMANT)
- assertThat(latest)
- .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false))
-
- cb.onDataActivity(1234)
- assertThat(latest)
- .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false))
-
- job.cancel()
- }
-
- @Test
- fun networkNameForSubId_updates() =
- testScope.runTest {
- var latest: NetworkNameModel? = null
- val job = underTest.carrierName.onEach { latest = it }.launchIn(this)
-
- subscriptionModel.value =
- SubscriptionModel(
- subscriptionId = SUB_1_ID,
- carrierName = DEFAULT_NAME,
- profileClass = PROFILE_CLASS_UNSET,
- )
-
- assertThat(latest?.name).isEqualTo(DEFAULT_NAME)
-
- val updatedName = "Derived Carrier"
- subscriptionModel.value =
- SubscriptionModel(
- subscriptionId = SUB_1_ID,
- carrierName = updatedName,
- profileClass = PROFILE_CLASS_UNSET,
- )
-
- assertThat(latest?.name).isEqualTo(updatedName)
-
- job.cancel()
- }
-
- @Test
- fun networkNameForSubId_defaultWhenSubscriptionModelNull() =
- testScope.runTest {
- var latest: NetworkNameModel? = null
- val job = underTest.carrierName.onEach { latest = it }.launchIn(this)
-
- subscriptionModel.value = null
-
- assertThat(latest?.name).isEqualTo(DEFAULT_NAME)
-
- job.cancel()
- }
-
- @Test
- fun networkName_default() =
- testScope.runTest {
- var latest: NetworkNameModel? = null
- val job = underTest.networkName.onEach { latest = it }.launchIn(this)
-
- assertThat(latest).isEqualTo(DEFAULT_NAME_MODEL)
-
- job.cancel()
- }
-
- @Test
- @EnableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
- fun networkName_usesBroadcastInfo_returnsDerived() =
- testScope.runTest {
- var latest: NetworkNameModel? = null
- val job = underTest.networkName.onEach { latest = it }.launchIn(this)
-
- val intent = spnIntent()
- val captor = argumentCaptor<BroadcastReceiver>()
- verify(context).registerReceiver(captor.capture(), any())
- captor.lastValue.onReceive(context, intent)
-
- // spnIntent() sets all values to true and test strings
- assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN"))
-
- job.cancel()
- }
-
- @Test
- @DisableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
- fun networkName_usesBroadcastInfo_returnsDerived_flagOff() =
- testScope.runTest {
- var latest: NetworkNameModel? = null
- val job = underTest.networkName.onEach { latest = it }.launchIn(this)
-
- val intent = spnIntent()
- val captor = argumentCaptor<BroadcastReceiver>()
- verify(context).registerReceiver(captor.capture(), any())
- captor.lastValue.onReceive(context, intent)
-
- // spnIntent() sets all values to true and test strings
- assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN"))
-
- job.cancel()
- }
-
- @Test
- @EnableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
- fun networkName_broadcastNotForThisSubId_keepsOldValue() =
- testScope.runTest {
- var latest: NetworkNameModel? = null
- val job = underTest.networkName.onEach { latest = it }.launchIn(this)
-
- val intent = spnIntent()
- val captor = argumentCaptor<BroadcastReceiver>()
- verify(context).registerReceiver(captor.capture(), any())
- captor.lastValue.onReceive(context, intent)
-
- assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN"))
-
- // WHEN an intent with a different subId is sent
- val wrongSubIntent = spnIntent(subId = 101)
-
- captor.lastValue.onReceive(context, wrongSubIntent)
-
- // THEN the previous intent's name is still used
- assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN"))
-
- job.cancel()
- }
-
- @Test
- @DisableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
- fun networkName_broadcastNotForThisSubId_keepsOldValue_flagOff() =
- testScope.runTest {
- var latest: NetworkNameModel? = null
- val job = underTest.networkName.onEach { latest = it }.launchIn(this)
-
- val intent = spnIntent()
- val captor = argumentCaptor<BroadcastReceiver>()
- verify(context).registerReceiver(captor.capture(), any())
- captor.lastValue.onReceive(context, intent)
-
- assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN"))
-
- // WHEN an intent with a different subId is sent
- val wrongSubIntent = spnIntent(subId = 101)
-
- captor.lastValue.onReceive(context, wrongSubIntent)
-
- // THEN the previous intent's name is still used
- assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN"))
-
- job.cancel()
- }
-
- @Test
- @EnableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
- fun networkName_broadcastHasNoData_updatesToDefault() =
- testScope.runTest {
- var latest: NetworkNameModel? = null
- val job = underTest.networkName.onEach { latest = it }.launchIn(this)
-
- val intent = spnIntent()
- val captor = argumentCaptor<BroadcastReceiver>()
- verify(context).registerReceiver(captor.capture(), any())
- captor.lastValue.onReceive(context, intent)
-
- assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN"))
-
- val intentWithoutInfo = spnIntent(showSpn = false, showPlmn = false)
-
- captor.lastValue.onReceive(context, intentWithoutInfo)
-
- assertThat(latest).isEqualTo(DEFAULT_NAME_MODEL)
-
- job.cancel()
- }
-
- @Test
- @DisableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
- fun networkName_broadcastHasNoData_updatesToDefault_flagOff() =
- testScope.runTest {
- var latest: NetworkNameModel? = null
- val job = underTest.networkName.onEach { latest = it }.launchIn(this)
-
- val intent = spnIntent()
- val captor = argumentCaptor<BroadcastReceiver>()
- verify(context).registerReceiver(captor.capture(), any())
- captor.lastValue.onReceive(context, intent)
-
- assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN"))
-
- val intentWithoutInfo = spnIntent(showSpn = false, showPlmn = false)
-
- captor.lastValue.onReceive(context, intentWithoutInfo)
-
- assertThat(latest).isEqualTo(DEFAULT_NAME_MODEL)
-
- job.cancel()
- }
-
- @Test
- @EnableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
- fun networkName_usingEagerStrategy_retainsNameBetweenSubscribers() =
- testScope.runTest {
- // Use the [StateFlow.value] getter so we can prove that the collection happens
- // even when there is no [Job]
-
- // Starts out default
- assertThat(underTest.networkName.value).isEqualTo(DEFAULT_NAME_MODEL)
-
- val intent = spnIntent()
- val captor = argumentCaptor<BroadcastReceiver>()
- verify(context).registerReceiver(captor.capture(), any())
- captor.lastValue.onReceive(context, intent)
-
- // The value is still there despite no active subscribers
- assertThat(underTest.networkName.value)
- .isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN"))
- }
-
- @Test
- @DisableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
- fun networkName_usingEagerStrategy_retainsNameBetweenSubscribers_flagOff() =
- testScope.runTest {
- // Use the [StateFlow.value] getter so we can prove that the collection happens
- // even when there is no [Job]
-
- // Starts out default
- assertThat(underTest.networkName.value).isEqualTo(DEFAULT_NAME_MODEL)
-
- val intent = spnIntent()
- val captor = argumentCaptor<BroadcastReceiver>()
- verify(context).registerReceiver(captor.capture(), any())
- captor.lastValue.onReceive(context, intent)
-
- // The value is still there despite no active subscribers
- assertThat(underTest.networkName.value)
- .isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN"))
- }
-
- @Test
- @EnableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
- fun networkName_allFieldsSet_prioritizesDataSpnOverSpn() =
- testScope.runTest {
- val latest by collectLastValue(underTest.networkName)
- val captor = argumentCaptor<BroadcastReceiver>()
- verify(context).registerReceiver(captor.capture(), any())
-
- val intent =
- spnIntent(
- subId = SUB_1_ID,
- showSpn = true,
- spn = SPN,
- dataSpn = DATA_SPN,
- showPlmn = true,
- plmn = PLMN,
- )
- captor.lastValue.onReceive(context, intent)
- assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN"))
- }
-
- @Test
- @EnableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
- fun networkName_spnAndPlmn_fallbackToSpnWhenNullDataSpn() =
- testScope.runTest {
- val latest by collectLastValue(underTest.networkName)
- val captor = argumentCaptor<BroadcastReceiver>()
- verify(context).registerReceiver(captor.capture(), any())
-
- val intent =
- spnIntent(
- subId = SUB_1_ID,
- showSpn = true,
- spn = SPN,
- dataSpn = null,
- showPlmn = true,
- plmn = PLMN,
- )
- captor.lastValue.onReceive(context, intent)
- assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$SPN"))
- }
-
- @Test
- @DisableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
- fun networkName_allFieldsSet_flagOff() =
- testScope.runTest {
- val latest by collectLastValue(underTest.networkName)
- val captor = argumentCaptor<BroadcastReceiver>()
- verify(context).registerReceiver(captor.capture(), any())
-
- val intent =
- spnIntent(
- subId = SUB_1_ID,
- showSpn = true,
- spn = SPN,
- dataSpn = DATA_SPN,
- showPlmn = true,
- plmn = PLMN,
- )
- captor.lastValue.onReceive(context, intent)
- assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN"))
- }
-
- @Test
- @EnableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
- fun networkName_showPlmn_plmnNotNull_showSpn_spnNull_dataSpnNotNull() =
- testScope.runTest {
- val latest by collectLastValue(underTest.networkName)
- val captor = argumentCaptor<BroadcastReceiver>()
- verify(context).registerReceiver(captor.capture(), any())
- val intent =
- spnIntent(
- subId = SUB_1_ID,
- showSpn = true,
- spn = null,
- dataSpn = DATA_SPN,
- showPlmn = true,
- plmn = PLMN,
- )
- captor.lastValue.onReceive(context, intent)
- assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN"))
- }
-
- @Test
- @EnableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
- fun networkName_showPlmn_plmnNotNull_showSpn_spnNotNull_dataSpnNull() =
- testScope.runTest {
- val latest by collectLastValue(underTest.networkName)
- val captor = argumentCaptor<BroadcastReceiver>()
- verify(context).registerReceiver(captor.capture(), any())
- val intent =
- spnIntent(
- subId = SUB_1_ID,
- showSpn = true,
- spn = SPN,
- dataSpn = null,
- showPlmn = true,
- plmn = PLMN,
- )
- captor.lastValue.onReceive(context, intent)
- assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$SPN"))
- }
-
- @Test
- @DisableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
- fun networkName_showPlmn_plmnNotNull_showSpn_spnNull_dataSpnNotNull_flagOff() =
- testScope.runTest {
- val latest by collectLastValue(underTest.networkName)
- val captor = argumentCaptor<BroadcastReceiver>()
- verify(context).registerReceiver(captor.capture(), any())
- val intent =
- spnIntent(
- subId = SUB_1_ID,
- showSpn = true,
- spn = null,
- dataSpn = DATA_SPN,
- showPlmn = true,
- plmn = PLMN,
- )
- captor.lastValue.onReceive(context, intent)
- assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN"))
- }
-
- @Test
- fun networkName_showPlmn_noShowSPN() =
- testScope.runTest {
- val latest by collectLastValue(underTest.networkName)
- val captor = argumentCaptor<BroadcastReceiver>()
- verify(context).registerReceiver(captor.capture(), any())
- val intent =
- spnIntent(
- subId = SUB_1_ID,
- showSpn = false,
- spn = SPN,
- dataSpn = DATA_SPN,
- showPlmn = true,
- plmn = PLMN,
- )
- captor.lastValue.onReceive(context, intent)
- assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN"))
- }
-
- @Test
- @EnableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
- fun networkName_showPlmn_plmnNull_showSpn() =
- testScope.runTest {
- val latest by collectLastValue(underTest.networkName)
- val captor = argumentCaptor<BroadcastReceiver>()
- verify(context).registerReceiver(captor.capture(), any())
- val intent =
- spnIntent(
- subId = SUB_1_ID,
- showSpn = true,
- spn = SPN,
- dataSpn = DATA_SPN,
- showPlmn = true,
- plmn = null,
- )
- captor.lastValue.onReceive(context, intent)
- assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$DATA_SPN"))
- }
-
- @Test
- @EnableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
- fun networkName_showPlmn_plmnNull_showSpn_dataSpnNull() =
- testScope.runTest {
- val latest by collectLastValue(underTest.networkName)
- val captor = argumentCaptor<BroadcastReceiver>()
- verify(context).registerReceiver(captor.capture(), any())
- val intent =
- spnIntent(
- subId = SUB_1_ID,
- showSpn = true,
- spn = SPN,
- dataSpn = null,
- showPlmn = true,
- plmn = null,
- )
- captor.lastValue.onReceive(context, intent)
- assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$SPN"))
- }
-
- @Test
- @EnableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
- fun networkName_showPlmn_plmnNull_showSpn_bothSpnNull() =
- testScope.runTest {
- val latest by collectLastValue(underTest.networkName)
- val captor = argumentCaptor<BroadcastReceiver>()
- verify(context).registerReceiver(captor.capture(), any())
- val intent =
- spnIntent(
- subId = SUB_1_ID,
- showSpn = true,
- spn = null,
- dataSpn = null,
- showPlmn = true,
- plmn = null,
- )
- captor.lastValue.onReceive(context, intent)
- assertThat(latest).isEqualTo(DEFAULT_NAME_MODEL)
- }
-
- @Test
- @DisableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN)
- fun networkName_showPlmn_plmnNull_showSpn_flagOff() =
- testScope.runTest {
- val latest by collectLastValue(underTest.networkName)
- val captor = argumentCaptor<BroadcastReceiver>()
- verify(context).registerReceiver(captor.capture(), any())
- val intent =
- spnIntent(
- subId = SUB_1_ID,
- showSpn = true,
- spn = SPN,
- dataSpn = DATA_SPN,
- showPlmn = true,
- plmn = null,
- )
- captor.lastValue.onReceive(context, intent)
- assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$DATA_SPN"))
- }
-
- @Test
- fun operatorAlphaShort_tracked() =
- testScope.runTest {
- var latest: String? = null
-
- val job = underTest.operatorAlphaShort.onEach { latest = it }.launchIn(this)
-
- val shortName = "short name"
- val serviceState = ServiceState()
- serviceState.setOperatorName(
- /* longName */ "long name",
- /* shortName */ shortName,
- /* numeric */ "12345",
- )
-
- getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState)
-
- assertThat(latest).isEqualTo(shortName)
-
- job.cancel()
- }
-
- @Test
- fun isInService_notIwlan() =
- testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isInService.onEach { latest = it }.launchIn(this)
-
- val nriInService =
- NetworkRegistrationInfo.Builder()
- .setDomain(DOMAIN_PS)
- .setTransportType(TRANSPORT_TYPE_WWAN)
- .setRegistrationState(REGISTRATION_STATE_HOME)
- .build()
-
- getTelephonyCallbackForType<ServiceStateListener>()
- .onServiceStateChanged(
- ServiceState().also {
- it.voiceRegState = STATE_IN_SERVICE
- it.addNetworkRegistrationInfo(nriInService)
- }
- )
-
- assertThat(latest).isTrue()
-
- getTelephonyCallbackForType<ServiceStateListener>()
- .onServiceStateChanged(
- ServiceState().also {
- it.voiceRegState = STATE_OUT_OF_SERVICE
- it.addNetworkRegistrationInfo(nriInService)
- }
- )
- assertThat(latest).isTrue()
-
- val nriNotInService =
- NetworkRegistrationInfo.Builder()
- .setDomain(DOMAIN_PS)
- .setTransportType(TRANSPORT_TYPE_WWAN)
- .setRegistrationState(REGISTRATION_STATE_DENIED)
- .build()
- getTelephonyCallbackForType<ServiceStateListener>()
- .onServiceStateChanged(
- ServiceState().also {
- it.voiceRegState = STATE_OUT_OF_SERVICE
- it.addNetworkRegistrationInfo(nriNotInService)
- }
- )
- assertThat(latest).isFalse()
-
- job.cancel()
- }
-
- @Test
- fun isInService_isIwlan_voiceOutOfService_dataInService() =
- testScope.runTest {
- var latest: Boolean? = null
- val job = underTest.isInService.onEach { latest = it }.launchIn(this)
-
- val iwlanData =
- NetworkRegistrationInfo.Builder()
- .setDomain(DOMAIN_PS)
- .setTransportType(TRANSPORT_TYPE_WLAN)
- .setRegistrationState(REGISTRATION_STATE_HOME)
- .build()
- val serviceState =
+ getTelephonyCallbackForType<ServiceStateListener>()
+ .onServiceStateChanged(
ServiceState().also {
it.voiceRegState = STATE_OUT_OF_SERVICE
- it.addNetworkRegistrationInfo(iwlanData)
+ it.addNetworkRegistrationInfo(nriInService)
}
-
- getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState)
- assertThat(latest).isFalse()
-
- job.cancel()
- }
-
- @Test
- fun isNonTerrestrial_updatesFromCallback0() =
- testScope.runTest {
- val latest by collectLastValue(underTest.isNonTerrestrial)
-
- // Starts out false
- assertThat(latest).isFalse()
-
- val callback = getTelephonyCallbackForType<CarrierRoamingNtnListener>()
-
- callback.onCarrierRoamingNtnModeChanged(true)
- assertThat(latest).isTrue()
-
- callback.onCarrierRoamingNtnModeChanged(false)
- assertThat(latest).isFalse()
- }
-
- @Test
- fun numberOfLevels_usesCarrierConfig() =
- testScope.runTest {
- var latest: Int? = null
- val job = underTest.numberOfLevels.onEach { latest = it }.launchIn(this)
-
- assertThat(latest).isEqualTo(DEFAULT_NUM_LEVELS)
-
- systemUiCarrierConfig.processNewCarrierConfig(
- testCarrierConfigWithOverride(KEY_INFLATE_SIGNAL_STRENGTH_BOOL, true)
)
+ assertThat(latest).isTrue()
- assertThat(latest).isEqualTo(DEFAULT_NUM_LEVELS + 1)
-
- systemUiCarrierConfig.processNewCarrierConfig(
- testCarrierConfigWithOverride(KEY_INFLATE_SIGNAL_STRENGTH_BOOL, false)
+ val nriNotInService =
+ NetworkRegistrationInfo.Builder()
+ .setDomain(DOMAIN_PS)
+ .setTransportType(TRANSPORT_TYPE_WWAN)
+ .setRegistrationState(REGISTRATION_STATE_DENIED)
+ .build()
+ getTelephonyCallbackForType<ServiceStateListener>()
+ .onServiceStateChanged(
+ ServiceState().also {
+ it.voiceRegState = STATE_OUT_OF_SERVICE
+ it.addNetworkRegistrationInfo(nriNotInService)
+ }
)
-
- assertThat(latest).isEqualTo(DEFAULT_NUM_LEVELS)
-
- job.cancel()
- }
-
- @Test
- fun inflateSignalStrength_usesCarrierConfig() =
- testScope.runTest {
- val latest by collectLastValue(underTest.inflateSignalStrength)
-
- assertThat(latest).isEqualTo(false)
-
- systemUiCarrierConfig.processNewCarrierConfig(
- testCarrierConfigWithOverride(KEY_INFLATE_SIGNAL_STRENGTH_BOOL, true)
- )
-
- assertThat(latest).isEqualTo(true)
-
- systemUiCarrierConfig.processNewCarrierConfig(
- testCarrierConfigWithOverride(KEY_INFLATE_SIGNAL_STRENGTH_BOOL, false)
- )
-
- assertThat(latest).isEqualTo(false)
- }
-
- @Test
- fun allowNetworkSliceIndicator_exposesCarrierConfigValue() =
- testScope.runTest {
- val latest by collectLastValue(underTest.allowNetworkSliceIndicator)
-
- systemUiCarrierConfig.processNewCarrierConfig(
- testCarrierConfigWithOverride(KEY_SHOW_5G_SLICE_ICON_BOOL, true)
- )
-
- assertThat(latest).isTrue()
-
- systemUiCarrierConfig.processNewCarrierConfig(
- testCarrierConfigWithOverride(KEY_SHOW_5G_SLICE_ICON_BOOL, false)
- )
-
- assertThat(latest).isFalse()
- }
-
- @Test
- fun isAllowedDuringAirplaneMode_alwaysFalse() =
- testScope.runTest {
- val latest by collectLastValue(underTest.isAllowedDuringAirplaneMode)
-
- assertThat(latest).isFalse()
- }
-
- @Test
- fun hasPrioritizedCaps_defaultFalse() {
- assertThat(underTest.hasPrioritizedNetworkCapabilities.value).isFalse()
+ assertThat(latest).isFalse()
}
@Test
- fun hasPrioritizedCaps_trueWhenAvailable() =
- testScope.runTest {
- val latest by collectLastValue(underTest.hasPrioritizedNetworkCapabilities)
+ fun isInService_isIwlan_voiceOutOfService_dataInService() = runTest {
+ val latest by underTest.isInService.collectLastValue()
- val callback: NetworkCallback =
- withArgCaptor<NetworkCallback> {
- verify(connectivityManager).registerNetworkCallback(any(), capture())
- }
+ val iwlanData =
+ NetworkRegistrationInfo.Builder()
+ .setDomain(DOMAIN_PS)
+ .setTransportType(TRANSPORT_TYPE_WLAN)
+ .setRegistrationState(REGISTRATION_STATE_HOME)
+ .build()
+ val serviceState =
+ ServiceState().also {
+ it.voiceRegState = STATE_OUT_OF_SERVICE
+ it.addNetworkRegistrationInfo(iwlanData)
+ }
- callback.onAvailable(mock())
-
- assertThat(latest).isTrue()
- }
+ getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState)
+ assertThat(latest).isFalse()
+ }
@Test
- fun hasPrioritizedCaps_becomesFalseWhenNetworkLost() =
- testScope.runTest {
- val latest by collectLastValue(underTest.hasPrioritizedNetworkCapabilities)
+ fun isNonTerrestrial_updatesFromCallback0() = runTest {
+ val latest by underTest.isNonTerrestrial.collectLastValue()
- val callback: NetworkCallback =
- withArgCaptor<NetworkCallback> {
- verify(connectivityManager).registerNetworkCallback(any(), capture())
- }
+ // Starts out false
+ assertThat(latest).isFalse()
- callback.onAvailable(mock())
+ val callback = getTelephonyCallbackForType<CarrierRoamingNtnListener>()
- assertThat(latest).isTrue()
+ callback.onCarrierRoamingNtnModeChanged(true)
+ assertThat(latest).isTrue()
- callback.onLost(mock())
+ callback.onCarrierRoamingNtnModeChanged(false)
+ assertThat(latest).isFalse()
+ }
- assertThat(latest).isFalse()
- }
+ @Test
+ fun numberOfLevels_usesCarrierConfig() = runTest {
+ val latest by underTest.numberOfLevels.collectLastValue()
- private inline fun <reified T> getTelephonyCallbackForType(): T {
+ assertThat(latest).isEqualTo(DEFAULT_NUM_LEVELS)
+
+ systemUiCarrierConfig.processNewCarrierConfig(
+ testCarrierConfigWithOverride(KEY_INFLATE_SIGNAL_STRENGTH_BOOL, true)
+ )
+
+ assertThat(latest).isEqualTo(DEFAULT_NUM_LEVELS + 1)
+
+ systemUiCarrierConfig.processNewCarrierConfig(
+ testCarrierConfigWithOverride(KEY_INFLATE_SIGNAL_STRENGTH_BOOL, false)
+ )
+
+ assertThat(latest).isEqualTo(DEFAULT_NUM_LEVELS)
+ }
+
+ @Test
+ fun inflateSignalStrength_usesCarrierConfig() = runTest {
+ val latest by underTest.inflateSignalStrength.collectLastValue()
+
+ assertThat(latest).isEqualTo(false)
+
+ systemUiCarrierConfig.processNewCarrierConfig(
+ testCarrierConfigWithOverride(KEY_INFLATE_SIGNAL_STRENGTH_BOOL, true)
+ )
+
+ assertThat(latest).isEqualTo(true)
+
+ systemUiCarrierConfig.processNewCarrierConfig(
+ testCarrierConfigWithOverride(KEY_INFLATE_SIGNAL_STRENGTH_BOOL, false)
+ )
+
+ assertThat(latest).isEqualTo(false)
+ }
+
+ @Test
+ fun allowNetworkSliceIndicator_exposesCarrierConfigValue() = runTest {
+ val latest by underTest.allowNetworkSliceIndicator.collectLastValue()
+
+ systemUiCarrierConfig.processNewCarrierConfig(
+ testCarrierConfigWithOverride(KEY_SHOW_5G_SLICE_ICON_BOOL, true)
+ )
+
+ assertThat(latest).isTrue()
+
+ systemUiCarrierConfig.processNewCarrierConfig(
+ testCarrierConfigWithOverride(KEY_SHOW_5G_SLICE_ICON_BOOL, false)
+ )
+
+ assertThat(latest).isFalse()
+ }
+
+ @Test
+ fun isAllowedDuringAirplaneMode_alwaysFalse() = runTest {
+ val latest by underTest.isAllowedDuringAirplaneMode.collectLastValue()
+
+ assertThat(latest).isFalse()
+ }
+
+ @Test
+ fun hasPrioritizedCaps_defaultFalse() = runTest {
+ // stand up under-test to kick-off activation
+ underTest
+
+ assertThat(kairos.transact { underTest.hasPrioritizedNetworkCapabilities.sample() })
+ .isFalse()
+ }
+
+ @Test
+ fun hasPrioritizedCaps_trueWhenAvailable() = runTest {
+ val latest by underTest.hasPrioritizedNetworkCapabilities.collectLastValue()
+
+ val callback: NetworkCallback =
+ argumentCaptor<NetworkCallback>()
+ .apply { verify(connectivityManager).registerNetworkCallback(any(), capture()) }
+ .lastValue
+
+ callback.onAvailable(mock())
+
+ assertThat(latest).isTrue()
+ }
+
+ @Test
+ fun hasPrioritizedCaps_becomesFalseWhenNetworkLost() = runTest {
+ val latest by underTest.hasPrioritizedNetworkCapabilities.collectLastValue()
+
+ val callback: NetworkCallback =
+ argumentCaptor<NetworkCallback>()
+ .apply { verify(connectivityManager).registerNetworkCallback(any(), capture()) }
+ .lastValue
+
+ callback.onAvailable(mock())
+
+ assertThat(latest).isTrue()
+
+ callback.onLost(mock())
+
+ assertThat(latest).isFalse()
+ }
+
+ private inline fun <reified T> Kosmos.getTelephonyCallbackForType(): T {
return MobileTelephonyHelpers.getTelephonyCallbackForType(telephonyManager)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryKairosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryKairosTest.kt
index 85423218..e04a96e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryKairosTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryKairosTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2025 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,6 +18,8 @@
import android.annotation.SuppressLint
import android.content.Intent
+import android.content.applicationContext
+import android.content.testableContext
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
@@ -25,6 +27,7 @@
import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.connectivityManager
import android.net.vcn.VcnTransportInfo
import android.net.wifi.WifiInfo
import android.net.wifi.WifiManager
@@ -34,1154 +37,894 @@
import android.telephony.SubscriptionInfo
import android.telephony.SubscriptionManager
import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
+import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener
import android.telephony.SubscriptionManager.PROFILE_CLASS_UNSET
-import android.telephony.TelephonyCallback
import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
+import android.telephony.TelephonyCallback.EmergencyCallbackModeListener
import android.telephony.TelephonyManager
+import android.telephony.telephonyManager
import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.telephony.PhoneConstants
-import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.KeyguardUpdateMonitorCallback
+import com.android.keyguard.keyguardUpdateMonitor
import com.android.settingslib.R
import com.android.settingslib.mobile.MobileMappings
import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.flags.FakeFeatureFlagsClassic
+import com.android.systemui.broadcast.broadcastDispatcher
+import com.android.systemui.broadcast.broadcastDispatcherContext
import com.android.systemui.flags.Flags
+import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosTestScope
+import com.android.systemui.kairos.combine
+import com.android.systemui.kairos.kairos
+import com.android.systemui.kairos.map
+import com.android.systemui.kairos.runKairosTest
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
import com.android.systemui.log.LogBuffer
-import com.android.systemui.log.table.TableLogBufferFactory
import com.android.systemui.log.table.logcatTableLogBuffer
import com.android.systemui.statusbar.connectivity.WifiPickerTrackerFactory
-import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
-import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.airplaneModeRepository
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.CarrierConfigRepository
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.carrierConfigRepository
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Factory.Companion.tableBufferLogName
-import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
-import com.android.systemui.statusbar.pipeline.mobile.util.FakeSubscriptionManagerProxy
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepositoryKairos
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepositoryKairosImpl
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileTelephonyHelpers.getTelephonyCallbackForType
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.subscriptionManager
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.subscriptionManagerProxy
+import com.android.systemui.statusbar.pipeline.mobile.util.fake
import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlots
-import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepositoryImpl
-import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
+import com.android.systemui.statusbar.pipeline.shared.data.repository.connectivityRepository
import com.android.systemui.statusbar.pipeline.wifi.data.repository.prod.WifiRepositoryImpl
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.wifiRepository
import com.android.systemui.testKosmos
-import com.android.systemui.user.data.repository.fakeUserRepository
+import com.android.systemui.user.data.repository.userRepository
import com.android.systemui.util.concurrency.FakeExecutor
-import com.android.systemui.util.mockito.argumentCaptor
-import com.android.systemui.util.mockito.capture
-import com.android.systemui.util.mockito.eq
import com.android.systemui.util.time.FakeSystemClock
import com.android.wifitrackerlib.MergedCarrierEntry
import com.android.wifitrackerlib.WifiEntry
import com.android.wifitrackerlib.WifiPickerTracker
import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
import java.util.UUID
-import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.test.StandardTestDispatcher
-import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertTrue
-import org.junit.Before
-import org.junit.Ignore
import org.junit.Test
+import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.ArgumentMatchers.anyString
-import org.mockito.Mock
+import org.mockito.Mockito
import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
+import org.mockito.kotlin.stub
import org.mockito.kotlin.whenever
-@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@OptIn(ExperimentalKairosApi::class)
@SmallTest
// This is required because our [SubscriptionManager.OnSubscriptionsChangedListener] uses a looper
// to run the callback and this makes the looper place nicely with TestScope etc.
@TestableLooper.RunWithLooper
+@RunWith(AndroidJUnit4::class)
class MobileConnectionsRepositoryKairosTest : SysuiTestCase() {
- private val kosmos = testKosmos()
- private val flags =
- FakeFeatureFlagsClassic().also { it.set(Flags.ROAMING_INDICATOR_VIA_DISPLAY_INFO, true) }
+ private val Kosmos.wifiManager: WifiManager by Fixture { mock {} }
+ private val Kosmos.wifiPickerTrackerFactory: WifiPickerTrackerFactory by Fixture {
+ mock {
+ on { create(any(), any(), wifiPickerTrackerCallback.capture(), any()) } doReturn
+ wifiPickerTracker
+ }
+ }
+ private val Kosmos.wifiPickerTracker: WifiPickerTracker by Fixture { mock {} }
+ private val Kosmos.wifiTableLogBuffer by Fixture { logcatTableLogBuffer(this, "wifiTableLog") }
- private lateinit var connectionFactory: MobileConnectionRepositoryImpl.Factory
- private lateinit var carrierMergedFactory: CarrierMergedConnectionRepository.Factory
- private lateinit var fullConnectionFactory: FullMobileConnectionRepository.Factory
- private lateinit var connectivityRepository: ConnectivityRepository
- private lateinit var airplaneModeRepository: FakeAirplaneModeRepository
- private lateinit var wifiRepository: WifiRepository
- private lateinit var carrierConfigRepository: CarrierConfigRepository
-
- @Mock private lateinit var connectivityManager: ConnectivityManager
- @Mock private lateinit var subscriptionManager: SubscriptionManager
- @Mock private lateinit var telephonyManager: TelephonyManager
- @Mock private lateinit var logger: MobileInputLogger
- private val summaryLogger = logcatTableLogBuffer(kosmos, "summaryLogger")
- @Mock private lateinit var logBufferFactory: TableLogBufferFactory
- @Mock private lateinit var updateMonitor: KeyguardUpdateMonitor
- @Mock private lateinit var wifiManager: WifiManager
- @Mock private lateinit var wifiPickerTrackerFactory: WifiPickerTrackerFactory
- @Mock private lateinit var wifiPickerTracker: WifiPickerTracker
- private val wifiTableLogBuffer = logcatTableLogBuffer(kosmos, "wifiTableLog")
-
- private val mobileMappings = FakeMobileMappingsProxy()
- private val subscriptionManagerProxy = FakeSubscriptionManagerProxy()
private val mainExecutor = FakeExecutor(FakeSystemClock())
private val wifiLogBuffer = LogBuffer("wifi", maxSize = 100, logcatEchoTracker = mock())
private val wifiPickerTrackerCallback =
argumentCaptor<WifiPickerTracker.WifiPickerTrackerCallback>()
private val vcnTransportInfo = VcnTransportInfo.Builder().build()
- private val userRepository = kosmos.fakeUserRepository
- private val testDispatcher = StandardTestDispatcher()
- private val testScope = TestScope(testDispatcher)
+ private val Kosmos.underTest
+ get() = mobileConnectionsRepositoryKairosImpl
- private lateinit var underTest: MobileConnectionsRepositoryKairosImpl
+ private val kosmos =
+ testKosmos().apply {
+ fakeFeatureFlagsClassic.set(Flags.ROAMING_INDICATOR_VIA_DISPLAY_INFO, true)
+ broadcastDispatcherContext = testableContext
+ connectivityRepository =
+ ConnectivityRepositoryImpl(
+ connectivityManager,
+ ConnectivitySlots(applicationContext),
+ applicationContext,
+ mock(),
+ mock(),
+ applicationCoroutineScope,
+ mock(),
+ )
+ wifiRepository =
+ WifiRepositoryImpl(
+ applicationContext,
+ userRepository,
+ applicationCoroutineScope,
+ mainExecutor,
+ testDispatcher,
+ wifiPickerTrackerFactory,
+ wifiManager,
+ wifiLogBuffer,
+ wifiTableLogBuffer,
+ )
+ subscriptionManager.stub {
+ // For convenience, set up the subscription info callbacks
+ on { getActiveSubscriptionInfo(anyInt()) } doAnswer
+ { invocation ->
+ when (invocation.getArgument(0) as Int) {
+ 1 -> SUB_1
+ 2 -> SUB_2
+ 3 -> SUB_3
+ 4 -> SUB_4
+ else -> null
+ }
+ }
+ }
+ telephonyManager.stub {
+ on { simOperatorName } doReturn ""
+ // Set up so the individual connection repositories
+ on { createForSubscriptionId(anyInt()) } doAnswer
+ { invocation ->
+ telephonyManager.stub {
+ on { subscriptionId } doReturn invocation.getArgument(0)
+ }
+ }
+ }
+ testScope.runCurrent()
+ }
- @Before
- fun setUp() {
- MockitoAnnotations.initMocks(this)
- whenever(telephonyManager.simOperatorName).thenReturn("")
+ private fun runTest(block: suspend KairosTestScope.() -> Unit) =
+ kosmos.run { runKairosTest { block() } }
- // Set up so the individual connection repositories
- whenever(telephonyManager.createForSubscriptionId(anyInt())).thenAnswer { invocation ->
- telephonyManager.also {
- whenever(it.subscriptionId).thenReturn(invocation.getArgument(0))
+ @Test
+ fun testSubscriptions_initiallyEmpty() = runTest {
+ assertThat(underTest.subscriptions.collectLastValue().value)
+ .isEqualTo(listOf<SubscriptionModel>())
+ }
+
+ @Test
+ fun testSubscriptions_listUpdates() = runTest {
+ val latest by underTest.subscriptions.collectLastValue()
+
+ subscriptionManager.stub {
+ on { completeActiveSubscriptionInfoList } doReturn listOf(SUB_1, SUB_2)
+ }
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ assertThat(latest).isEqualTo(listOf(MODEL_1, MODEL_2))
+ }
+
+ @Test
+ fun testSubscriptions_removingSub_updatesList() = runTest {
+ val latest by underTest.subscriptions.collectLastValue()
+
+ // WHEN 2 networks show up
+ subscriptionManager.stub {
+ on { completeActiveSubscriptionInfoList } doReturn listOf(SUB_1, SUB_2)
+ }
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ // WHEN one network is removed
+ subscriptionManager.stub {
+ on { completeActiveSubscriptionInfoList } doReturn listOf(SUB_2)
+ }
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ // THEN the subscriptions list represents the newest change
+ assertThat(latest).isEqualTo(listOf(MODEL_2))
+ }
+
+ @Test
+ fun subscriptions_subIsOnlyNtn_modelHasExclusivelyNtnTrue() = runTest {
+ val latest by underTest.subscriptions.collectLastValue()
+
+ val onlyNtnSub =
+ mock<SubscriptionInfo> {
+ on { isOnlyNonTerrestrialNetwork } doReturn true
+ on { subscriptionId } doReturn 45
+ on { groupUuid } doReturn GROUP_1
+ on { carrierName } doReturn "NTN only"
+ on { profileClass } doReturn PROFILE_CLASS_UNSET
+ }
+
+ subscriptionManager.stub {
+ on { completeActiveSubscriptionInfoList } doReturn listOf(onlyNtnSub)
+ }
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ assertThat(latest).hasSize(1)
+ assertThat(latest!![0].isExclusivelyNonTerrestrial).isTrue()
+ }
+
+ @Test
+ fun subscriptions_subIsNotOnlyNtn_modelHasExclusivelyNtnFalse() = runTest {
+ val latest by underTest.subscriptions.collectLastValue()
+
+ val notOnlyNtnSub =
+ mock<SubscriptionInfo> {
+ on { isOnlyNonTerrestrialNetwork } doReturn false
+ on { subscriptionId } doReturn 45
+ on { groupUuid } doReturn GROUP_1
+ on { carrierName } doReturn "NTN only"
+ on { profileClass } doReturn PROFILE_CLASS_UNSET
+ }
+
+ subscriptionManager.stub {
+ on { completeActiveSubscriptionInfoList } doReturn listOf(notOnlyNtnSub)
+ }
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ assertThat(latest).hasSize(1)
+ assertThat(latest!![0].isExclusivelyNonTerrestrial).isFalse()
+ }
+
+ @Test
+ fun testSubscriptions_carrierMergedOnly_listHasCarrierMerged() = runTest {
+ val latest by underTest.subscriptions.collectLastValue()
+
+ setWifiState(isCarrierMerged = true)
+ subscriptionManager.stub {
+ on { completeActiveSubscriptionInfoList } doReturn listOf(SUB_CM)
+ }
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ assertThat(latest).isEqualTo(listOf(MODEL_CM))
+ }
+
+ @Test
+ fun testSubscriptions_carrierMergedAndOther_listHasBothWithCarrierMergedLast() = runTest {
+ val latest by underTest.subscriptions.collectLastValue()
+
+ setWifiState(isCarrierMerged = true)
+ subscriptionManager.stub {
+ on { completeActiveSubscriptionInfoList } doReturn listOf(SUB_1, SUB_2, SUB_CM)
+ }
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ assertThat(latest).isEqualTo(listOf(MODEL_1, MODEL_2, MODEL_CM))
+ }
+
+ @Test
+ fun testActiveDataSubscriptionId_initialValueIsNull() = runTest {
+ assertThat(underTest.activeMobileDataSubscriptionId.collectLastValue().value)
+ .isEqualTo(null)
+ }
+
+ @Test
+ fun testActiveDataSubscriptionId_updates() = runTest {
+ val active by underTest.activeMobileDataSubscriptionId.collectLastValue()
+ testScope.runCurrent()
+
+ getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>(telephonyManager)
+ .onActiveDataSubscriptionIdChanged(SUB_2_ID)
+
+ assertThat(active).isEqualTo(SUB_2_ID)
+ }
+
+ @Test
+ fun activeSubId_nullIfInvalidSubIdIsReceived() = runTest {
+ val latest by underTest.activeMobileDataSubscriptionId.collectLastValue()
+ testScope.runCurrent()
+
+ getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>(telephonyManager)
+ .onActiveDataSubscriptionIdChanged(SUB_2_ID)
+
+ assertThat(latest).isNotNull()
+
+ getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>(telephonyManager)
+ .onActiveDataSubscriptionIdChanged(INVALID_SUBSCRIPTION_ID)
+
+ assertThat(latest).isNull()
+ }
+
+ @Test
+ fun activeRepo_initiallyNull() = runTest {
+ assertThat(underTest.activeMobileDataRepository.collectLastValue().value).isNull()
+ }
+
+ @Test
+ fun activeRepo_updatesWithActiveDataId() = runTest {
+ val latest by underTest.activeMobileDataRepository.collectLastValue()
+ testScope.runCurrent()
+
+ // GIVEN the subscription list is then updated which includes the active data sub id
+ subscriptionManager.stub {
+ on { completeActiveSubscriptionInfoList } doReturn listOf(SUB_2)
+ }
+ getSubscriptionCallback().onSubscriptionsChanged()
+ testScope.runCurrent()
+
+ getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>(telephonyManager)
+ .onActiveDataSubscriptionIdChanged(SUB_2_ID)
+
+ assertThat(latest?.subId).isEqualTo(SUB_2_ID)
+ }
+
+ @Test
+ fun activeRepo_nullIfActiveDataSubIdBecomesInvalid() = runTest {
+ val latest by underTest.activeMobileDataRepository.collectLastValue()
+ testScope.runCurrent()
+
+ // GIVEN the subscription list is then updated which includes the active data sub id
+ subscriptionManager.stub {
+ on { completeActiveSubscriptionInfoList } doReturn listOf(SUB_2)
+ }
+ getSubscriptionCallback().onSubscriptionsChanged()
+ testScope.runCurrent()
+
+ getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>(telephonyManager)
+ .onActiveDataSubscriptionIdChanged(SUB_2_ID)
+ testScope.runCurrent()
+
+ assertThat(latest).isNotNull()
+
+ getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>(telephonyManager)
+ .onActiveDataSubscriptionIdChanged(INVALID_SUBSCRIPTION_ID)
+ testScope.runCurrent()
+
+ assertThat(latest).isNull()
+ }
+
+ @Test
+ /** Regression test for b/268146648. */
+ fun activeSubIdIsSetBeforeSubscriptionsAreUpdated_doesNotThrow() = runTest {
+ val activeRepo by underTest.activeMobileDataRepository.collectLastValue()
+ val subscriptions by underTest.subscriptions.collectLastValue()
+ testScope.runCurrent()
+
+ getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>(telephonyManager)
+ .onActiveDataSubscriptionIdChanged(SUB_2_ID)
+ testScope.runCurrent()
+
+ assertThat(subscriptions).isEmpty()
+ assertThat(activeRepo).isNull()
+ }
+
+ @Test
+ fun getRepoForSubId_activeDataSubIdIsRequestedBeforeSubscriptionsUpdate() = runTest {
+ underTest
+
+ var latestActiveRepo: MobileConnectionRepositoryKairos? = null
+ testScope.backgroundScope.launch {
+ kairos.activateSpec {
+ underTest.activeMobileDataSubscriptionId
+ .combine(underTest.mobileConnectionsBySubId) { id, conns ->
+ id?.let { conns[id] }
+ }
+ .observe {
+ if (it != null) {
+ latestActiveRepo = it
+ }
+ }
}
}
- whenever(logBufferFactory.getOrCreate(anyString(), anyInt())).thenAnswer { _ ->
- logcatTableLogBuffer(kosmos, "test")
+ val latestSubscriptions by underTest.subscriptions.collectLastValue()
+ testScope.runCurrent()
+
+ // Active data subscription id is sent, but no subscription change has been posted yet
+ getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>(telephonyManager)
+ .onActiveDataSubscriptionIdChanged(SUB_2_ID)
+ testScope.runCurrent()
+
+ // Subscriptions list is empty
+ assertThat(latestSubscriptions).isEmpty()
+
+ // getRepoForSubId does not throw
+ assertThat(latestActiveRepo).isNull()
+ }
+
+ @Test
+ fun activeDataSentBeforeSubscriptionList_subscriptionReusesActiveDataRepo() = runTest {
+ val activeRepo by underTest.activeMobileDataRepository.collectLastValue()
+ testScope.runCurrent()
+
+ // GIVEN active repo is updated before the subscription list updates
+ getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>(telephonyManager)
+ .onActiveDataSubscriptionIdChanged(SUB_2_ID)
+ testScope.runCurrent()
+
+ assertThat(activeRepo).isNull()
+
+ // GIVEN the subscription list is then updated which includes the active data sub id
+ subscriptionManager.stub {
+ on { completeActiveSubscriptionInfoList } doReturn listOf(SUB_2)
+ }
+ getSubscriptionCallback().onSubscriptionsChanged()
+ testScope.runCurrent()
+
+ // WHEN requesting a connection repository for the subscription
+ val newRepo =
+ kairos.transact { underTest.mobileConnectionsBySubId.map { it[SUB_2_ID] }.sample() }
+
+ // THEN the newly request repo has been cached and reused
+ assertThat(activeRepo).isSameInstanceAs(newRepo)
+ }
+
+ @Test
+ fun testConnectionRepository_validSubId_isCached() = runTest {
+ underTest
+
+ subscriptionManager.stub {
+ on { completeActiveSubscriptionInfoList } doReturn listOf(SUB_1)
+ }
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ val repo1 by underTest.mobileConnectionsBySubId.map { it[SUB_1_ID] }.collectLastValue()
+ val repo2 by underTest.mobileConnectionsBySubId.map { it[SUB_1_ID] }.collectLastValue()
+
+ assertThat(repo1).isNotNull()
+ assertThat(repo1).isSameInstanceAs(repo2)
+ }
+
+ @Test
+ fun testConnectionRepository_carrierMergedSubId_isCached() = runTest {
+ underTest
+
+ getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_CM)
+ setWifiState(isCarrierMerged = true)
+ subscriptionManager.stub {
+ on { completeActiveSubscriptionInfoList } doReturn listOf(SUB_CM)
+ }
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ val repo1 by underTest.mobileConnectionsBySubId.map { it[SUB_CM_ID] }.collectLastValue()
+ val repo2 by underTest.mobileConnectionsBySubId.map { it[SUB_CM_ID] }.collectLastValue()
+
+ assertThat(repo1).isNotNull()
+ assertThat(repo1).isSameInstanceAs(repo2)
+ }
+
+ @SuppressLint("UnspecifiedRegisterReceiverFlag")
+ @Test
+ fun testDeviceEmergencyCallState_eagerlyChecksState() = runTest {
+ val latest by underTest.isDeviceEmergencyCallCapable.collectLastValue()
+
+ // Value starts out false
+ assertThat(latest).isFalse()
+ telephonyManager.stub { on { activeModemCount } doReturn 1 }
+ whenever(telephonyManager.getServiceStateForSlot(any())).thenAnswer { _ ->
+ ServiceState().apply { isEmergencyOnly = true }
}
- whenever(
- wifiPickerTrackerFactory.create(
- any(),
- any(),
- capture(wifiPickerTrackerCallback),
- any(),
- )
- )
- .thenReturn(wifiPickerTracker)
+ // WHEN an appropriate intent gets sent out
+ val intent = serviceStateIntent(subId = -1)
+ broadcastDispatcher.sendIntentToMatchingReceiversOnly(applicationContext, intent)
+ testScope.runCurrent()
- // For convenience, set up the subscription info callbacks
- whenever(subscriptionManager.getActiveSubscriptionInfo(anyInt())).thenAnswer { invocation ->
+ // THEN the repo's state is updated despite no listeners
+ assertThat(latest).isEqualTo(true)
+ }
+
+ @Test
+ fun testDeviceEmergencyCallState_aggregatesAcrossSlots_oneTrue() = runTest {
+ val latest by underTest.isDeviceEmergencyCallCapable.collectLastValue()
+
+ // GIVEN there are multiple slots
+ telephonyManager.stub { on { activeModemCount } doReturn 4 }
+ // GIVEN only one of them reports ECM
+ whenever(telephonyManager.getServiceStateForSlot(any())).thenAnswer { invocation ->
when (invocation.getArgument(0) as Int) {
- 1 -> SUB_1
- 2 -> SUB_2
- 3 -> SUB_3
- 4 -> SUB_4
+ 0 -> ServiceState().apply { isEmergencyOnly = false }
+ 1 -> ServiceState().apply { isEmergencyOnly = false }
+ 2 -> ServiceState().apply { isEmergencyOnly = true }
+ 3 -> ServiceState().apply { isEmergencyOnly = false }
else -> null
}
}
- connectivityRepository =
- ConnectivityRepositoryImpl(
- connectivityManager,
- ConnectivitySlots(context),
- context,
- mock(),
- mock(),
- testScope.backgroundScope,
- mock(),
- )
-
- airplaneModeRepository = FakeAirplaneModeRepository()
-
- wifiRepository =
- WifiRepositoryImpl(
- mContext,
- userRepository,
- testScope.backgroundScope,
- mainExecutor,
- testDispatcher,
- wifiPickerTrackerFactory,
- wifiManager,
- wifiLogBuffer,
- wifiTableLogBuffer,
- )
-
- carrierConfigRepository = kosmos.carrierConfigRepository
-
- connectionFactory =
- MobileConnectionRepositoryImpl.Factory(
- context,
- fakeBroadcastDispatcher,
- connectivityManager,
- telephonyManager = telephonyManager,
- bgDispatcher = testDispatcher,
- logger = logger,
- mobileMappingsProxy = mobileMappings,
- scope = testScope.backgroundScope,
- flags = flags,
- carrierConfigRepository = carrierConfigRepository,
- )
- carrierMergedFactory =
- CarrierMergedConnectionRepository.Factory(
- telephonyManager,
- testScope.backgroundScope.coroutineContext,
- testScope.backgroundScope,
- wifiRepository,
- )
- fullConnectionFactory =
- FullMobileConnectionRepository.Factory(
- scope = testScope.backgroundScope,
- logFactory = logBufferFactory,
- mobileRepoFactory = connectionFactory,
- carrierMergedRepoFactory = carrierMergedFactory,
- )
-
- underTest =
- MobileConnectionsRepositoryKairosImpl(
- connectivityRepository,
- subscriptionManager,
- subscriptionManagerProxy,
- telephonyManager,
- logger,
- summaryLogger,
- mobileMappings,
- fakeBroadcastDispatcher,
- context,
- /* bgDispatcher = */ testDispatcher,
- testScope.backgroundScope,
- /* mainDispatcher = */ testDispatcher,
- airplaneModeRepository,
- wifiRepository,
- fullConnectionFactory,
- updateMonitor,
- mock(),
- )
-
+ // GIVEN a broadcast goes out for the appropriate subID
+ val intent = serviceStateIntent(subId = -1)
+ broadcastDispatcher.sendIntentToMatchingReceiversOnly(applicationContext, intent)
testScope.runCurrent()
+
+ // THEN the device is in ECM, because one of the service states is
+ assertThat(latest).isTrue()
}
@Test
- fun testSubscriptions_initiallyEmpty() =
- testScope.runTest {
- assertThat(underTest.subscriptions.value).isEqualTo(listOf<SubscriptionModel>())
+ fun testDeviceEmergencyCallState_aggregatesAcrossSlots_allFalse() = runTest {
+ val latest by underTest.isDeviceEmergencyCallCapable.collectLastValue()
+
+ // GIVEN there are multiple slots
+ telephonyManager.stub { on { activeModemCount } doReturn 4 }
+ // GIVEN only one of them reports ECM
+ whenever(telephonyManager.getServiceStateForSlot(any())).thenAnswer { invocation ->
+ when (invocation.getArgument(0) as Int) {
+ 0 -> ServiceState().apply { isEmergencyOnly = false }
+ 1 -> ServiceState().apply { isEmergencyOnly = false }
+ 2 -> ServiceState().apply { isEmergencyOnly = false }
+ 3 -> ServiceState().apply { isEmergencyOnly = false }
+ else -> null
+ }
}
- @Test
- fun testSubscriptions_listUpdates() =
- testScope.runTest {
- val latest by collectLastValue(underTest.subscriptions)
+ // GIVEN a broadcast goes out for the appropriate subID
+ val intent = serviceStateIntent(subId = -1)
+ broadcastDispatcher.sendIntentToMatchingReceiversOnly(applicationContext, intent)
+ testScope.runCurrent()
- whenever(subscriptionManager.completeActiveSubscriptionInfoList)
- .thenReturn(listOf(SUB_1, SUB_2))
- getSubscriptionCallback().onSubscriptionsChanged()
-
- assertThat(latest).isEqualTo(listOf(MODEL_1, MODEL_2))
- }
-
- @Test
- fun testSubscriptions_removingSub_updatesList() =
- testScope.runTest {
- val latest by collectLastValue(underTest.subscriptions)
-
- // WHEN 2 networks show up
- whenever(subscriptionManager.completeActiveSubscriptionInfoList)
- .thenReturn(listOf(SUB_1, SUB_2))
- getSubscriptionCallback().onSubscriptionsChanged()
-
- // WHEN one network is removed
- whenever(subscriptionManager.completeActiveSubscriptionInfoList)
- .thenReturn(listOf(SUB_2))
- getSubscriptionCallback().onSubscriptionsChanged()
-
- // THEN the subscriptions list represents the newest change
- assertThat(latest).isEqualTo(listOf(MODEL_2))
- }
-
- @Test
- fun subscriptions_subIsOnlyNtn_modelHasExclusivelyNtnTrue() =
- testScope.runTest {
- val latest by collectLastValue(underTest.subscriptions)
-
- val onlyNtnSub =
- mock<SubscriptionInfo>().also {
- whenever(it.isOnlyNonTerrestrialNetwork).thenReturn(true)
- whenever(it.subscriptionId).thenReturn(45)
- whenever(it.groupUuid).thenReturn(GROUP_1)
- whenever(it.carrierName).thenReturn("NTN only")
- whenever(it.profileClass).thenReturn(PROFILE_CLASS_UNSET)
- }
-
- whenever(subscriptionManager.completeActiveSubscriptionInfoList)
- .thenReturn(listOf(onlyNtnSub))
- getSubscriptionCallback().onSubscriptionsChanged()
-
- assertThat(latest).hasSize(1)
- assertThat(latest!![0].isExclusivelyNonTerrestrial).isTrue()
- }
-
- @Test
- fun subscriptions_subIsNotOnlyNtn_modelHasExclusivelyNtnFalse() =
- testScope.runTest {
- val latest by collectLastValue(underTest.subscriptions)
-
- val notOnlyNtnSub =
- mock<SubscriptionInfo>().also {
- whenever(it.isOnlyNonTerrestrialNetwork).thenReturn(false)
- whenever(it.subscriptionId).thenReturn(45)
- whenever(it.groupUuid).thenReturn(GROUP_1)
- whenever(it.carrierName).thenReturn("NTN only")
- whenever(it.profileClass).thenReturn(PROFILE_CLASS_UNSET)
- }
-
- whenever(subscriptionManager.completeActiveSubscriptionInfoList)
- .thenReturn(listOf(notOnlyNtnSub))
- getSubscriptionCallback().onSubscriptionsChanged()
-
- assertThat(latest).hasSize(1)
- assertThat(latest!![0].isExclusivelyNonTerrestrial).isFalse()
- }
-
- @Test
- fun testSubscriptions_carrierMergedOnly_listHasCarrierMerged() =
- testScope.runTest {
- val latest by collectLastValue(underTest.subscriptions)
-
- setWifiState(isCarrierMerged = true)
- whenever(subscriptionManager.completeActiveSubscriptionInfoList)
- .thenReturn(listOf(SUB_CM))
- getSubscriptionCallback().onSubscriptionsChanged()
-
- assertThat(latest).isEqualTo(listOf(MODEL_CM))
- }
-
- @Test
- fun testSubscriptions_carrierMergedAndOther_listHasBothWithCarrierMergedLast() =
- testScope.runTest {
- val latest by collectLastValue(underTest.subscriptions)
-
- setWifiState(isCarrierMerged = true)
- whenever(subscriptionManager.completeActiveSubscriptionInfoList)
- .thenReturn(listOf(SUB_1, SUB_2, SUB_CM))
- getSubscriptionCallback().onSubscriptionsChanged()
-
- assertThat(latest).isEqualTo(listOf(MODEL_1, MODEL_2, MODEL_CM))
- }
-
- @Test
- fun testActiveDataSubscriptionId_initialValueIsNull() =
- testScope.runTest {
- assertThat(underTest.activeMobileDataSubscriptionId.value).isEqualTo(null)
- }
-
- @Test
- fun testActiveDataSubscriptionId_updates() =
- testScope.runTest {
- val active by collectLastValue(underTest.activeMobileDataSubscriptionId)
-
- getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>()
- .onActiveDataSubscriptionIdChanged(SUB_2_ID)
-
- assertThat(active).isEqualTo(SUB_2_ID)
- }
-
- @Test
- fun activeSubId_nullIfInvalidSubIdIsReceived() =
- testScope.runTest {
- val latest by collectLastValue(underTest.activeMobileDataSubscriptionId)
-
- getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>()
- .onActiveDataSubscriptionIdChanged(SUB_2_ID)
-
- assertThat(latest).isNotNull()
-
- getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>()
- .onActiveDataSubscriptionIdChanged(INVALID_SUBSCRIPTION_ID)
-
- assertThat(latest).isNull()
- }
-
- @Test
- fun activeRepo_initiallyNull() {
- assertThat(underTest.activeMobileDataRepository.value).isNull()
+ // THEN the device is in ECM, because one of the service states is
+ assertThat(latest).isFalse()
}
@Test
- fun activeRepo_updatesWithActiveDataId() =
- testScope.runTest {
- val latest by collectLastValue(underTest.activeMobileDataRepository)
+ fun testConnectionCache_clearsInvalidSubscriptions() = runTest {
+ underTest
- getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>()
- .onActiveDataSubscriptionIdChanged(SUB_2_ID)
-
- assertThat(latest?.subId).isEqualTo(SUB_2_ID)
+ subscriptionManager.stub {
+ on { completeActiveSubscriptionInfoList } doReturn listOf(SUB_1, SUB_2)
}
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ val repoCache by underTest.mobileConnectionsBySubId.collectLastValue()
+
+ assertThat(repoCache?.keys).containsExactly(SUB_1_ID, SUB_2_ID)
+
+ // SUB_2 disappears
+ subscriptionManager.stub {
+ on { completeActiveSubscriptionInfoList } doReturn listOf(SUB_1)
+ }
+ getSubscriptionCallback().onSubscriptionsChanged()
+
+ assertThat(repoCache?.keys).containsExactly(SUB_1_ID)
+ }
@Test
- fun activeRepo_nullIfActiveDataSubIdBecomesInvalid() =
- testScope.runTest {
- val latest by collectLastValue(underTest.activeMobileDataRepository)
+ fun testConnectionCache_clearsInvalidSubscriptions_includingCarrierMerged() = runTest {
+ underTest
- getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>()
- .onActiveDataSubscriptionIdChanged(SUB_2_ID)
-
- assertThat(latest).isNotNull()
-
- getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>()
- .onActiveDataSubscriptionIdChanged(INVALID_SUBSCRIPTION_ID)
-
- assertThat(latest).isNull()
+ getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_CM)
+ setWifiState(isCarrierMerged = true)
+ subscriptionManager.stub {
+ on { completeActiveSubscriptionInfoList } doReturn listOf(SUB_1, SUB_2, SUB_CM)
}
+ getSubscriptionCallback().onSubscriptionsChanged()
- @Test
- /** Regression test for b/268146648. */
- fun activeSubIdIsSetBeforeSubscriptionsAreUpdated_doesNotThrow() =
- testScope.runTest {
- val activeRepo by collectLastValue(underTest.activeMobileDataRepository)
- val subscriptions by collectLastValue(underTest.subscriptions)
+ val repoCache by underTest.mobileConnectionsBySubId.collectLastValue()
- getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>()
- .onActiveDataSubscriptionIdChanged(SUB_2_ID)
+ assertThat(repoCache?.keys).containsExactly(SUB_1_ID, SUB_2_ID, SUB_CM_ID)
- assertThat(subscriptions).isEmpty()
- assertThat(activeRepo).isNotNull()
+ // SUB_2 and SUB_CM disappear
+ subscriptionManager.stub {
+ on { completeActiveSubscriptionInfoList } doReturn listOf(SUB_1)
}
+ getSubscriptionCallback().onSubscriptionsChanged()
- @Test
- fun getRepoForSubId_activeDataSubIdIsRequestedBeforeSubscriptionsUpdate() =
- testScope.runTest {
- var latestActiveRepo: MobileConnectionRepository? = null
- collectLastValue(
- underTest.activeMobileDataSubscriptionId.filterNotNull().onEach {
- latestActiveRepo = underTest.getRepoForSubId(it)
- }
- )
-
- val latestSubscriptions by collectLastValue(underTest.subscriptions)
-
- // Active data subscription id is sent, but no subscription change has been posted yet
- getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>()
- .onActiveDataSubscriptionIdChanged(SUB_2_ID)
-
- // Subscriptions list is empty
- assertThat(latestSubscriptions).isEmpty()
- // getRepoForSubId does not throw
- assertThat(latestActiveRepo).isNotNull()
- }
-
- @Test
- fun activeDataSentBeforeSubscriptionList_subscriptionReusesActiveDataRepo() =
- testScope.runTest {
- val activeRepo by collectLastValue(underTest.activeMobileDataRepository)
- collectLastValue(underTest.subscriptions)
-
- // GIVEN active repo is updated before the subscription list updates
- getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>()
- .onActiveDataSubscriptionIdChanged(SUB_2_ID)
-
- assertThat(activeRepo).isNotNull()
-
- // GIVEN the subscription list is then updated which includes the active data sub id
- whenever(subscriptionManager.completeActiveSubscriptionInfoList)
- .thenReturn(listOf(SUB_2))
- getSubscriptionCallback().onSubscriptionsChanged()
-
- // WHEN requesting a connection repository for the subscription
- val newRepo = underTest.getRepoForSubId(SUB_2_ID)
-
- // THEN the newly request repo has been cached and reused
- assertThat(activeRepo).isSameInstanceAs(newRepo)
- }
-
- @Test
- fun testConnectionRepository_validSubId_isCached() =
- testScope.runTest {
- collectLastValue(underTest.subscriptions)
-
- whenever(subscriptionManager.completeActiveSubscriptionInfoList)
- .thenReturn(listOf(SUB_1))
- getSubscriptionCallback().onSubscriptionsChanged()
-
- val repo1 = underTest.getRepoForSubId(SUB_1_ID)
- val repo2 = underTest.getRepoForSubId(SUB_1_ID)
-
- assertThat(repo1).isSameInstanceAs(repo2)
- }
-
- @Test
- fun testConnectionRepository_carrierMergedSubId_isCached() =
- testScope.runTest {
- collectLastValue(underTest.subscriptions)
-
- getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_CM)
- setWifiState(isCarrierMerged = true)
- whenever(subscriptionManager.completeActiveSubscriptionInfoList)
- .thenReturn(listOf(SUB_CM))
- getSubscriptionCallback().onSubscriptionsChanged()
-
- val repo1 = underTest.getRepoForSubId(SUB_CM_ID)
- val repo2 = underTest.getRepoForSubId(SUB_CM_ID)
-
- assertThat(repo1).isSameInstanceAs(repo2)
- }
-
- @Test
- fun testConnectionRepository_carrierMergedAndMobileSubs_usesCorrectRepos() =
- testScope.runTest {
- collectLastValue(underTest.subscriptions)
-
- getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_CM)
- setWifiState(isCarrierMerged = true)
- whenever(subscriptionManager.completeActiveSubscriptionInfoList)
- .thenReturn(listOf(SUB_1, SUB_CM))
- getSubscriptionCallback().onSubscriptionsChanged()
-
- val carrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID)
- val mobileRepo = underTest.getRepoForSubId(SUB_1_ID)
- assertThat(carrierMergedRepo.getIsCarrierMerged()).isTrue()
- assertThat(mobileRepo.getIsCarrierMerged()).isFalse()
- }
-
- @Test
- fun testSubscriptions_subNoLongerCarrierMerged_repoUpdates() =
- testScope.runTest {
- collectLastValue(underTest.subscriptions)
-
- getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_CM)
- setWifiState(isCarrierMerged = true)
- whenever(subscriptionManager.completeActiveSubscriptionInfoList)
- .thenReturn(listOf(SUB_1, SUB_CM))
- getSubscriptionCallback().onSubscriptionsChanged()
-
- val carrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID)
- var mobileRepo = underTest.getRepoForSubId(SUB_1_ID)
- assertThat(carrierMergedRepo.getIsCarrierMerged()).isTrue()
- assertThat(mobileRepo.getIsCarrierMerged()).isFalse()
-
- // WHEN the wifi network updates to be not carrier merged
- getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_ACTIVE)
- setWifiState(isCarrierMerged = false)
- runCurrent()
-
- // THEN the repos update
- val noLongerCarrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID)
- mobileRepo = underTest.getRepoForSubId(SUB_1_ID)
- assertThat(noLongerCarrierMergedRepo.getIsCarrierMerged()).isFalse()
- assertThat(mobileRepo.getIsCarrierMerged()).isFalse()
- }
-
- @Test
- fun testSubscriptions_subBecomesCarrierMerged_repoUpdates() =
- testScope.runTest {
- collectLastValue(underTest.subscriptions)
-
- getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_ACTIVE)
- setWifiState(isCarrierMerged = false)
- whenever(subscriptionManager.completeActiveSubscriptionInfoList)
- .thenReturn(listOf(SUB_1, SUB_CM))
- getSubscriptionCallback().onSubscriptionsChanged()
- runCurrent()
-
- val notYetCarrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID)
- var mobileRepo = underTest.getRepoForSubId(SUB_1_ID)
- assertThat(notYetCarrierMergedRepo.getIsCarrierMerged()).isFalse()
- assertThat(mobileRepo.getIsCarrierMerged()).isFalse()
-
- // WHEN the wifi network updates to be carrier merged
- getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_CM)
- setWifiState(isCarrierMerged = true)
- runCurrent()
-
- // THEN the repos update
- val carrierMergedRepo = underTest.getRepoForSubId(SUB_CM_ID)
- mobileRepo = underTest.getRepoForSubId(SUB_1_ID)
- assertThat(carrierMergedRepo.getIsCarrierMerged()).isTrue()
- assertThat(mobileRepo.getIsCarrierMerged()).isFalse()
- }
-
- @SuppressLint("UnspecifiedRegisterReceiverFlag")
- @Test
- fun testDeviceEmergencyCallState_eagerlyChecksState() =
- testScope.runTest {
- // Value starts out false
- assertThat(underTest.isDeviceEmergencyCallCapable.value).isFalse()
- whenever(telephonyManager.activeModemCount).thenReturn(1)
- whenever(telephonyManager.getServiceStateForSlot(any())).thenAnswer { _ ->
- ServiceState().apply { isEmergencyOnly = true }
- }
-
- // WHEN an appropriate intent gets sent out
- val intent = serviceStateIntent(subId = -1)
- fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent)
- runCurrent()
-
- // THEN the repo's state is updated despite no listeners
- assertThat(underTest.isDeviceEmergencyCallCapable.value).isEqualTo(true)
- }
-
- @Test
- fun testDeviceEmergencyCallState_aggregatesAcrossSlots_oneTrue() =
- testScope.runTest {
- val latest by collectLastValue(underTest.isDeviceEmergencyCallCapable)
-
- // GIVEN there are multiple slots
- whenever(telephonyManager.activeModemCount).thenReturn(4)
- // GIVEN only one of them reports ECM
- whenever(telephonyManager.getServiceStateForSlot(any())).thenAnswer { invocation ->
- when (invocation.getArgument(0) as Int) {
- 0 -> ServiceState().apply { isEmergencyOnly = false }
- 1 -> ServiceState().apply { isEmergencyOnly = false }
- 2 -> ServiceState().apply { isEmergencyOnly = true }
- 3 -> ServiceState().apply { isEmergencyOnly = false }
- else -> null
- }
- }
-
- // GIVEN a broadcast goes out for the appropriate subID
- val intent = serviceStateIntent(subId = -1)
- fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent)
- runCurrent()
-
- // THEN the device is in ECM, because one of the service states is
- assertThat(latest).isTrue()
- }
-
- @Test
- fun testDeviceEmergencyCallState_aggregatesAcrossSlots_allFalse() =
- testScope.runTest {
- val latest by collectLastValue(underTest.isDeviceEmergencyCallCapable)
-
- // GIVEN there are multiple slots
- whenever(telephonyManager.activeModemCount).thenReturn(4)
- // GIVEN only one of them reports ECM
- whenever(telephonyManager.getServiceStateForSlot(any())).thenAnswer { invocation ->
- when (invocation.getArgument(0) as Int) {
- 0 -> ServiceState().apply { isEmergencyOnly = false }
- 1 -> ServiceState().apply { isEmergencyOnly = false }
- 2 -> ServiceState().apply { isEmergencyOnly = false }
- 3 -> ServiceState().apply { isEmergencyOnly = false }
- else -> null
- }
- }
-
- // GIVEN a broadcast goes out for the appropriate subID
- val intent = serviceStateIntent(subId = -1)
- fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent)
- runCurrent()
-
- // THEN the device is in ECM, because one of the service states is
- assertThat(latest).isFalse()
- }
-
- @Test
- @Ignore("b/333912012")
- fun testConnectionCache_clearsInvalidSubscriptions() =
- testScope.runTest {
- collectLastValue(underTest.subscriptions)
-
- whenever(subscriptionManager.completeActiveSubscriptionInfoList)
- .thenReturn(listOf(SUB_1, SUB_2))
- getSubscriptionCallback().onSubscriptionsChanged()
-
- // Get repos to trigger caching
- val repo1 = underTest.getRepoForSubId(SUB_1_ID)
- val repo2 = underTest.getRepoForSubId(SUB_2_ID)
-
- assertThat(underTest.getSubIdRepoCache())
- .containsExactly(SUB_1_ID, repo1, SUB_2_ID, repo2)
-
- // SUB_2 disappears
- whenever(subscriptionManager.completeActiveSubscriptionInfoList)
- .thenReturn(listOf(SUB_1))
- getSubscriptionCallback().onSubscriptionsChanged()
-
- assertThat(underTest.getSubIdRepoCache()).containsExactly(SUB_1_ID, repo1)
- }
-
- @Test
- @Ignore("b/333912012")
- fun testConnectionCache_clearsInvalidSubscriptions_includingCarrierMerged() =
- testScope.runTest {
- collectLastValue(underTest.subscriptions)
-
- getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_CM)
- setWifiState(isCarrierMerged = true)
- whenever(subscriptionManager.completeActiveSubscriptionInfoList)
- .thenReturn(listOf(SUB_1, SUB_2, SUB_CM))
- getSubscriptionCallback().onSubscriptionsChanged()
-
- // Get repos to trigger caching
- val repo1 = underTest.getRepoForSubId(SUB_1_ID)
- val repo2 = underTest.getRepoForSubId(SUB_2_ID)
- val repoCarrierMerged = underTest.getRepoForSubId(SUB_CM_ID)
-
- assertThat(underTest.getSubIdRepoCache())
- .containsExactly(SUB_1_ID, repo1, SUB_2_ID, repo2, SUB_CM_ID, repoCarrierMerged)
-
- // SUB_2 and SUB_CM disappear
- whenever(subscriptionManager.completeActiveSubscriptionInfoList)
- .thenReturn(listOf(SUB_1))
- getSubscriptionCallback().onSubscriptionsChanged()
-
- assertThat(underTest.getSubIdRepoCache()).containsExactly(SUB_1_ID, repo1)
- }
+ assertThat(repoCache?.keys).containsExactly(SUB_1_ID)
+ }
/** Regression test for b/261706421 */
@Test
- @Ignore("b/333912012")
- fun testConnectionsCache_clearMultipleSubscriptionsAtOnce_doesNotThrow() =
- testScope.runTest {
- collectLastValue(underTest.subscriptions)
+ fun testConnectionsCache_clearMultipleSubscriptionsAtOnce_doesNotThrow() = runTest {
+ underTest
- whenever(subscriptionManager.completeActiveSubscriptionInfoList)
- .thenReturn(listOf(SUB_1, SUB_2))
- getSubscriptionCallback().onSubscriptionsChanged()
-
- // Get repos to trigger caching
- val repo1 = underTest.getRepoForSubId(SUB_1_ID)
- val repo2 = underTest.getRepoForSubId(SUB_2_ID)
-
- assertThat(underTest.getSubIdRepoCache())
- .containsExactly(SUB_1_ID, repo1, SUB_2_ID, repo2)
-
- // All subscriptions disappear
- whenever(subscriptionManager.completeActiveSubscriptionInfoList).thenReturn(listOf())
- getSubscriptionCallback().onSubscriptionsChanged()
-
- assertThat(underTest.getSubIdRepoCache()).isEmpty()
+ subscriptionManager.stub {
+ on { completeActiveSubscriptionInfoList } doReturn listOf(SUB_1, SUB_2)
}
+ getSubscriptionCallback().onSubscriptionsChanged()
- @Test
- fun testConnectionsCache_keepsReposCached() =
- testScope.runTest {
- // Collect subscriptions to start the job
- collectLastValue(underTest.subscriptions)
+ val repoCache by underTest.mobileConnectionsBySubId.collectLastValue()
- whenever(subscriptionManager.completeActiveSubscriptionInfoList)
- .thenReturn(listOf(SUB_1))
- getSubscriptionCallback().onSubscriptionsChanged()
+ assertThat(repoCache?.keys).containsExactly(SUB_1_ID, SUB_2_ID)
- val repo1_1 = underTest.getRepoForSubId(SUB_1_ID)
+ // All subscriptions disappear
+ subscriptionManager.stub { on { completeActiveSubscriptionInfoList } doReturn listOf() }
+ getSubscriptionCallback().onSubscriptionsChanged()
- // All subscriptions disappear
- whenever(subscriptionManager.completeActiveSubscriptionInfoList).thenReturn(listOf())
- getSubscriptionCallback().onSubscriptionsChanged()
-
- // Sub1 comes back
- whenever(subscriptionManager.completeActiveSubscriptionInfoList)
- .thenReturn(listOf(SUB_1))
- getSubscriptionCallback().onSubscriptionsChanged()
-
- val repo1_2 = underTest.getRepoForSubId(SUB_1_ID)
-
- assertThat(repo1_1).isSameInstanceAs(repo1_2)
- }
-
- @Test
- fun testConnectionsCache_doesNotDropReferencesThatHaveBeenRealized() =
- testScope.runTest {
- // Collect subscriptions to start the job
- collectLastValue(underTest.subscriptions)
-
- whenever(subscriptionManager.completeActiveSubscriptionInfoList)
- .thenReturn(listOf(SUB_1))
- getSubscriptionCallback().onSubscriptionsChanged()
-
- // Client grabs a reference to a repository, but doesn't keep it around
- underTest.getRepoForSubId(SUB_1_ID)
-
- // All subscriptions disappear
- whenever(subscriptionManager.completeActiveSubscriptionInfoList).thenReturn(listOf())
- getSubscriptionCallback().onSubscriptionsChanged()
-
- val repo1 = underTest.getRepoForSubId(SUB_1_ID)
-
- assertThat(repo1).isNotNull()
- }
-
- @Test
- fun testConnectionRepository_invalidSubId_doesNotThrow() =
- testScope.runTest {
- underTest.getRepoForSubId(SUB_1_ID)
- // No exception
- }
-
- @Test
- fun connectionRepository_logBufferContainsSubIdInItsName() =
- testScope.runTest {
- collectLastValue(underTest.subscriptions)
-
- whenever(subscriptionManager.completeActiveSubscriptionInfoList)
- .thenReturn(listOf(SUB_1, SUB_2))
- getSubscriptionCallback().onSubscriptionsChanged()
-
- // Get repos to trigger creation
- underTest.getRepoForSubId(SUB_1_ID)
- verify(logBufferFactory).getOrCreate(eq(tableBufferLogName(SUB_1_ID)), anyInt())
- underTest.getRepoForSubId(SUB_2_ID)
- verify(logBufferFactory).getOrCreate(eq(tableBufferLogName(SUB_2_ID)), anyInt())
- }
-
- @Test
- fun testDefaultDataSubId_updatesOnBroadcast() =
- testScope.runTest {
- val latest by collectLastValue(underTest.defaultDataSubId)
-
- assertThat(latest).isEqualTo(null)
-
- val intent2 =
- Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
- .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_2_ID)
- fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent2)
-
- assertThat(latest).isEqualTo(SUB_2_ID)
-
- val intent1 =
- Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
- .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_1_ID)
- fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent1)
-
- assertThat(latest).isEqualTo(SUB_1_ID)
- }
-
- @Test
- fun defaultDataSubId_fetchesInitialValueOnStart() =
- testScope.runTest {
- subscriptionManagerProxy.defaultDataSubId = 2
- val latest by collectLastValue(underTest.defaultDataSubId)
-
- assertThat(latest).isEqualTo(2)
- }
-
- @Test
- fun defaultDataSubId_filtersOutInvalidSubIds() =
- testScope.runTest {
- subscriptionManagerProxy.defaultDataSubId = INVALID_SUBSCRIPTION_ID
- val latest by collectLastValue(underTest.defaultDataSubId)
-
- assertThat(latest).isNull()
- }
-
- @Test
- fun defaultDataSubId_filtersOutInvalidSubIds_fromValidToInvalid() =
- testScope.runTest {
- subscriptionManagerProxy.defaultDataSubId = 2
- val latest by collectLastValue(underTest.defaultDataSubId)
-
- assertThat(latest).isEqualTo(2)
-
- val intent =
- Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
- .putExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID)
- fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent)
-
- assertThat(latest).isNull()
- }
-
- @Test
- fun defaultDataSubId_fetchesCurrentOnRestart() =
- testScope.runTest {
- subscriptionManagerProxy.defaultDataSubId = 2
- var latest: Int? = null
- var job = underTest.defaultDataSubId.onEach { latest = it }.launchIn(this)
- runCurrent()
-
- assertThat(latest).isEqualTo(2)
-
- job.cancel()
-
- // Collectors go away but come back later
-
- latest = null
-
- subscriptionManagerProxy.defaultDataSubId = 1
-
- job = underTest.defaultDataSubId.onEach { latest = it }.launchIn(this)
- runCurrent()
-
- assertThat(latest).isEqualTo(1)
-
- job.cancel()
- }
-
- @Test
- fun mobileIsDefault_startsAsFalse() {
- assertThat(underTest.mobileIsDefault.value).isFalse()
+ assertThat(repoCache).isEmpty()
}
@Test
- fun mobileIsDefault_capsHaveCellular_isDefault() =
- testScope.runTest {
- val caps =
- mock<NetworkCapabilities>().also {
- whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
- }
+ fun testDefaultDataSubId_updatesOnBroadcast() = runTest {
+ val latest by underTest.defaultDataSubId.collectLastValue()
- val latest by collectLastValue(underTest.mobileIsDefault)
+ assertThat(latest).isEqualTo(null)
- getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
+ val intent2 =
+ Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
+ .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_2_ID)
+ broadcastDispatcher.sendIntentToMatchingReceiversOnly(applicationContext, intent2)
- assertThat(latest).isTrue()
- }
+ assertThat(latest).isEqualTo(SUB_2_ID)
+
+ val intent1 =
+ Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
+ .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_1_ID)
+ broadcastDispatcher.sendIntentToMatchingReceiversOnly(applicationContext, intent1)
+
+ assertThat(latest).isEqualTo(SUB_1_ID)
+ }
@Test
- fun mobileIsDefault_capsDoNotHaveCellular_isNotDefault() =
- testScope.runTest {
- val caps =
- mock<NetworkCapabilities>().also {
- whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(false)
- }
+ fun defaultDataSubId_fetchesInitialValueOnStart() = runTest {
+ subscriptionManagerProxy.fake.defaultDataSubId = 2
+ val latest by underTest.defaultDataSubId.collectLastValue()
- val latest by collectLastValue(underTest.mobileIsDefault)
-
- getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
-
- assertThat(latest).isFalse()
- }
+ assertThat(latest).isEqualTo(2)
+ }
@Test
- fun mobileIsDefault_carrierMergedViaMobile_isDefault() =
- testScope.runTest {
- val carrierMergedInfo =
- mock<WifiInfo>().apply { whenever(this.isCarrierMerged).thenReturn(true) }
- val caps =
- mock<NetworkCapabilities>().also {
- whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(carrierMergedInfo)
- }
-
- val latest by collectLastValue(underTest.mobileIsDefault)
-
- getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
-
- assertThat(latest).isTrue()
- }
+ fun mobileIsDefault_startsAsFalse() = runTest {
+ assertThat(underTest.mobileIsDefault.collectLastValue().value).isFalse()
+ }
@Test
- fun mobileIsDefault_wifiDefault_mobileNotDefault() =
- testScope.runTest {
- val caps =
- mock<NetworkCapabilities>().also {
- whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
- }
+ fun mobileIsDefault_capsHaveCellular_isDefault() = runTest {
+ val caps =
+ Mockito.mock(NetworkCapabilities::class.java).stub {
+ on { hasTransport(TRANSPORT_CELLULAR) } doReturn true
+ }
- val latest by collectLastValue(underTest.mobileIsDefault)
+ val latest by underTest.mobileIsDefault.collectLastValue()
- getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
+ getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
- assertThat(latest).isFalse()
- }
+ assertThat(latest).isTrue()
+ }
@Test
- fun mobileIsDefault_ethernetDefault_mobileNotDefault() =
- testScope.runTest {
- val caps =
- mock<NetworkCapabilities>().also {
- whenever(it.hasTransport(TRANSPORT_ETHERNET)).thenReturn(true)
- }
+ fun mobileIsDefault_capsDoNotHaveCellular_isNotDefault() = runTest {
+ val caps =
+ Mockito.mock(NetworkCapabilities::class.java).stub {
+ on { hasTransport(TRANSPORT_CELLULAR) } doReturn false
+ }
- val latest by collectLastValue(underTest.mobileIsDefault)
+ val latest by underTest.mobileIsDefault.collectLastValue()
- getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
+ getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
- assertThat(latest).isFalse()
- }
+ assertThat(latest).isFalse()
+ }
+
+ @Test
+ fun mobileIsDefault_carrierMergedViaMobile_isDefault() = runTest {
+ val carrierMergedInfo = mock<WifiInfo> { on { isCarrierMerged } doReturn true }
+ val caps =
+ Mockito.mock(NetworkCapabilities::class.java).stub {
+ on { hasTransport(TRANSPORT_CELLULAR) } doReturn true
+ on { transportInfo } doReturn carrierMergedInfo
+ }
+
+ val latest by underTest.mobileIsDefault.collectLastValue()
+
+ getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
+
+ assertThat(latest).isTrue()
+ }
+
+ @Test
+ fun mobileIsDefault_wifiDefault_mobileNotDefault() = runTest {
+ val caps =
+ Mockito.mock(NetworkCapabilities::class.java).stub {
+ on { hasTransport(TRANSPORT_WIFI) } doReturn true
+ }
+
+ val latest by underTest.mobileIsDefault.collectLastValue()
+
+ getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
+
+ assertThat(latest).isFalse()
+ }
+
+ @Test
+ fun mobileIsDefault_ethernetDefault_mobileNotDefault() = runTest {
+ val caps =
+ Mockito.mock(NetworkCapabilities::class.java).stub {
+ on { hasTransport(TRANSPORT_ETHERNET) } doReturn true
+ }
+
+ val latest by underTest.mobileIsDefault.collectLastValue()
+
+ getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
+
+ assertThat(latest).isFalse()
+ }
/** Regression test for b/272586234. */
@Test
- fun hasCarrierMergedConnection_carrierMergedViaWifi_isTrue() =
- testScope.runTest {
- val carrierMergedInfo =
- mock<WifiInfo>().apply {
- whenever(this.isCarrierMerged).thenReturn(true)
- whenever(this.isPrimary).thenReturn(true)
- }
- val caps =
- mock<NetworkCapabilities>().also {
- whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(carrierMergedInfo)
- }
+ fun hasCarrierMergedConnection_carrierMergedViaWifi_isTrue() = runTest {
+ val carrierMergedInfo =
+ mock<WifiInfo> {
+ on { isCarrierMerged } doReturn true
+ on { isPrimary } doReturn true
+ }
+ val caps =
+ Mockito.mock(NetworkCapabilities::class.java).stub {
+ on { hasTransport(TRANSPORT_WIFI) } doReturn true
+ on { transportInfo } doReturn carrierMergedInfo
+ }
- val latest by collectLastValue(underTest.hasCarrierMergedConnection)
+ val latest by underTest.hasCarrierMergedConnection.collectLastValue()
- getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
- setWifiState(isCarrierMerged = true)
+ getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
+ setWifiState(isCarrierMerged = true)
- assertThat(latest).isTrue()
- }
+ assertThat(latest).isTrue()
+ }
@Test
- fun hasCarrierMergedConnection_carrierMergedViaMobile_isTrue() =
- testScope.runTest {
- val carrierMergedInfo =
- mock<WifiInfo>().apply {
- whenever(this.isCarrierMerged).thenReturn(true)
- whenever(this.isPrimary).thenReturn(true)
- }
- val caps =
- mock<NetworkCapabilities>().also {
- whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(carrierMergedInfo)
- }
+ fun hasCarrierMergedConnection_carrierMergedViaMobile_isTrue() = runTest {
+ val carrierMergedInfo =
+ mock<WifiInfo> {
+ on { isCarrierMerged } doReturn true
+ on { isPrimary } doReturn true
+ }
+ val caps =
+ Mockito.mock(NetworkCapabilities::class.java).stub {
+ on { hasTransport(TRANSPORT_CELLULAR) } doReturn true
+ on { transportInfo } doReturn carrierMergedInfo
+ }
- val latest by collectLastValue(underTest.hasCarrierMergedConnection)
+ val latest by underTest.hasCarrierMergedConnection.collectLastValue()
- getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
- setWifiState(isCarrierMerged = true)
+ getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
+ setWifiState(isCarrierMerged = true)
- assertThat(latest).isTrue()
- }
+ assertThat(latest).isTrue()
+ }
- private fun newWifiNetwork(wifiInfo: WifiInfo): Network {
+ private fun KairosTestScope.newWifiNetwork(wifiInfo: WifiInfo): Network {
val network = mock<Network>()
val capabilities =
- mock<NetworkCapabilities>().also {
- whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(wifiInfo)
+ Mockito.mock(NetworkCapabilities::class.java).stub {
+ on { hasTransport(TRANSPORT_WIFI) } doReturn true
+ on { transportInfo } doReturn wifiInfo
}
- whenever(connectivityManager.getNetworkCapabilities(network)).thenReturn(capabilities)
-
+ connectivityManager.stub { on { getNetworkCapabilities(network) } doReturn capabilities }
return network
}
/** Regression test for b/272586234. */
@Test
- fun hasCarrierMergedConnection_carrierMergedViaWifiWithVcnTransport_isTrue() =
- testScope.runTest {
- val carrierMergedInfo =
- mock<WifiInfo>().apply {
- whenever(this.isCarrierMerged).thenReturn(true)
- whenever(this.isPrimary).thenReturn(true)
- }
- val underlyingWifi = newWifiNetwork(carrierMergedInfo)
- val caps =
- mock<NetworkCapabilities>().also {
- whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(vcnTransportInfo)
- whenever(it.underlyingNetworks).thenReturn(listOf(underlyingWifi))
- }
+ fun hasCarrierMergedConnection_carrierMergedViaWifiWithVcnTransport_isTrue() = runTest {
+ val carrierMergedInfo =
+ mock<WifiInfo> {
+ on { isCarrierMerged } doReturn true
+ on { isPrimary } doReturn true
+ }
+ val underlyingWifi = newWifiNetwork(carrierMergedInfo)
+ val caps =
+ Mockito.mock(NetworkCapabilities::class.java).stub {
+ on { hasTransport(TRANSPORT_WIFI) } doReturn true
+ on { transportInfo } doReturn vcnTransportInfo
+ on { underlyingNetworks } doReturn listOf(underlyingWifi)
+ }
- val latest by collectLastValue(underTest.hasCarrierMergedConnection)
+ val latest by underTest.hasCarrierMergedConnection.collectLastValue()
- getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
- setWifiState(isCarrierMerged = true)
+ getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
+ setWifiState(isCarrierMerged = true)
- assertThat(latest).isTrue()
- }
+ assertThat(latest).isTrue()
+ }
@Test
- fun hasCarrierMergedConnection_carrierMergedViaMobileWithVcnTransport_isTrue() =
- testScope.runTest {
- val carrierMergedInfo =
- mock<WifiInfo>().apply {
- whenever(this.isCarrierMerged).thenReturn(true)
- whenever(this.isPrimary).thenReturn(true)
- }
- val underlyingWifi = newWifiNetwork(carrierMergedInfo)
- val caps =
- mock<NetworkCapabilities>().also {
- whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(vcnTransportInfo)
- whenever(it.underlyingNetworks).thenReturn(listOf(underlyingWifi))
- }
+ fun hasCarrierMergedConnection_carrierMergedViaMobileWithVcnTransport_isTrue() = runTest {
+ val carrierMergedInfo =
+ mock<WifiInfo> {
+ on { isCarrierMerged } doReturn true
+ on { isPrimary } doReturn true
+ }
+ val underlyingWifi = newWifiNetwork(carrierMergedInfo)
+ val caps =
+ Mockito.mock(NetworkCapabilities::class.java).stub {
+ on { hasTransport(TRANSPORT_CELLULAR) } doReturn true
+ on { transportInfo } doReturn vcnTransportInfo
+ on { underlyingNetworks } doReturn listOf(underlyingWifi)
+ }
- val latest by collectLastValue(underTest.hasCarrierMergedConnection)
+ val latest by underTest.hasCarrierMergedConnection.collectLastValue()
- getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
- setWifiState(isCarrierMerged = true)
+ getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
+ setWifiState(isCarrierMerged = true)
- assertThat(latest).isTrue()
- }
+ assertThat(latest).isTrue()
+ }
@Test
- fun hasCarrierMergedConnection_isCarrierMergedViaUnderlyingWifi_isTrue() =
- testScope.runTest {
- val latest by collectLastValue(underTest.hasCarrierMergedConnection)
+ fun hasCarrierMergedConnection_isCarrierMergedViaUnderlyingWifi_isTrue() = runTest {
+ val latest by underTest.hasCarrierMergedConnection.collectLastValue()
- val underlyingNetwork = mock<Network>()
- val carrierMergedInfo =
- mock<WifiInfo>().apply {
- whenever(this.isCarrierMerged).thenReturn(true)
- whenever(this.isPrimary).thenReturn(true)
- }
- val underlyingWifiCapabilities =
- mock<NetworkCapabilities>().also {
- whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(carrierMergedInfo)
- }
- whenever(connectivityManager.getNetworkCapabilities(underlyingNetwork))
- .thenReturn(underlyingWifiCapabilities)
-
- // WHEN the main capabilities have an underlying carrier merged network via WIFI
- // transport and WifiInfo
- val mainCapabilities =
- mock<NetworkCapabilities>().also {
- whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(null)
- whenever(it.underlyingNetworks).thenReturn(listOf(underlyingNetwork))
- }
-
- getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, mainCapabilities)
- setWifiState(isCarrierMerged = true)
-
- // THEN there's a carrier merged connection
- assertThat(latest).isTrue()
+ val underlyingNetwork = mock<Network>()
+ val carrierMergedInfo =
+ mock<WifiInfo> {
+ on { isCarrierMerged } doReturn true
+ on { isPrimary } doReturn true
+ }
+ val underlyingWifiCapabilities =
+ Mockito.mock(NetworkCapabilities::class.java).stub {
+ on { hasTransport(TRANSPORT_WIFI) } doReturn true
+ on { transportInfo } doReturn carrierMergedInfo
+ }
+ connectivityManager.stub {
+ on { getNetworkCapabilities(underlyingNetwork) } doReturn underlyingWifiCapabilities
}
+ // WHEN the main capabilities have an underlying carrier merged network via WIFI
+ // transport and WifiInfo
+ val mainCapabilities =
+ Mockito.mock(NetworkCapabilities::class.java).stub {
+ on { hasTransport(TRANSPORT_CELLULAR) } doReturn true
+ on { transportInfo } doReturn null
+ on { underlyingNetworks } doReturn listOf(underlyingNetwork)
+ }
+
+ getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, mainCapabilities)
+ setWifiState(isCarrierMerged = true)
+
+ // THEN there's a carrier merged connection
+ assertThat(latest).isTrue()
+ }
+
@Test
- fun hasCarrierMergedConnection_isCarrierMergedViaUnderlyingCellular_isTrue() =
- testScope.runTest {
- val latest by collectLastValue(underTest.hasCarrierMergedConnection)
+ fun hasCarrierMergedConnection_isCarrierMergedViaUnderlyingCellular_isTrue() = runTest {
+ val latest by underTest.hasCarrierMergedConnection.collectLastValue()
- val underlyingCarrierMergedNetwork = mock<Network>()
- val carrierMergedInfo =
- mock<WifiInfo>().apply {
- whenever(this.isCarrierMerged).thenReturn(true)
- whenever(this.isPrimary).thenReturn(true)
- }
+ val underlyingCarrierMergedNetwork = mock<Network>()
+ val carrierMergedInfo =
+ mock<WifiInfo> {
+ on { isCarrierMerged } doReturn true
+ on { isPrimary } doReturn true
+ }
- // The Wifi network that is under the VCN network
- val physicalWifiNetwork = newWifiNetwork(carrierMergedInfo)
+ // The Wifi network that is under the VCN network
+ val physicalWifiNetwork = newWifiNetwork(carrierMergedInfo)
- val underlyingCapabilities =
- mock<NetworkCapabilities>().also {
- whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(vcnTransportInfo)
- whenever(it.underlyingNetworks).thenReturn(listOf(physicalWifiNetwork))
- }
- whenever(connectivityManager.getNetworkCapabilities(underlyingCarrierMergedNetwork))
- .thenReturn(underlyingCapabilities)
-
- // WHEN the main capabilities have an underlying carrier merged network via CELLULAR
- // transport and VcnTransportInfo
- val mainCapabilities =
- mock<NetworkCapabilities>().also {
- whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(null)
- whenever(it.underlyingNetworks)
- .thenReturn(listOf(underlyingCarrierMergedNetwork))
- }
-
- getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, mainCapabilities)
- setWifiState(isCarrierMerged = true)
-
- // THEN there's a carrier merged connection
- assertThat(latest).isTrue()
+ val underlyingCapabilities =
+ Mockito.mock(NetworkCapabilities::class.java).stub {
+ on { hasTransport(TRANSPORT_CELLULAR) } doReturn true
+ on { transportInfo } doReturn vcnTransportInfo
+ on { underlyingNetworks } doReturn listOf(physicalWifiNetwork)
+ }
+ connectivityManager.stub {
+ on { getNetworkCapabilities(underlyingCarrierMergedNetwork) } doReturn
+ underlyingCapabilities
}
+ // WHEN the main capabilities have an underlying carrier merged network via CELLULAR
+ // transport and VcnTransportInfo
+ val mainCapabilities =
+ Mockito.mock(NetworkCapabilities::class.java).stub {
+ on { hasTransport(TRANSPORT_CELLULAR) } doReturn true
+ on { transportInfo } doReturn null
+ on { underlyingNetworks } doReturn listOf(underlyingCarrierMergedNetwork)
+ }
+
+ getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, mainCapabilities)
+ setWifiState(isCarrierMerged = true)
+
+ // THEN there's a carrier merged connection
+ assertThat(latest).isTrue()
+ }
+
/** Regression test for b/272586234. */
@Test
fun hasCarrierMergedConnection_defaultIsWifiNotCarrierMerged_wifiRepoIsCarrierMerged_isTrue() =
- testScope.runTest {
- val latest by collectLastValue(underTest.hasCarrierMergedConnection)
+ runTest {
+ val latest by underTest.hasCarrierMergedConnection.collectLastValue()
// WHEN the default callback is TRANSPORT_WIFI but not carrier merged
- val carrierMergedInfo =
- mock<WifiInfo>().apply { whenever(this.isCarrierMerged).thenReturn(false) }
+ val carrierMergedInfo = mock<WifiInfo> { on { isCarrierMerged } doReturn false }
val caps =
- mock<NetworkCapabilities>().also {
- whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(carrierMergedInfo)
+ Mockito.mock(NetworkCapabilities::class.java).stub {
+ on { hasTransport(TRANSPORT_WIFI) } doReturn true
+ on { transportInfo } doReturn carrierMergedInfo
}
getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
@@ -1194,37 +937,36 @@
/** Regression test for b/278618530. */
@Test
- fun hasCarrierMergedConnection_defaultIsCellular_wifiRepoIsCarrierMerged_isFalse() =
- testScope.runTest {
- val latest by collectLastValue(underTest.hasCarrierMergedConnection)
+ fun hasCarrierMergedConnection_defaultIsCellular_wifiRepoIsCarrierMerged_isFalse() = runTest {
+ val latest by underTest.hasCarrierMergedConnection.collectLastValue()
- // WHEN the default callback is TRANSPORT_CELLULAR and not carrier merged
- val caps =
- mock<NetworkCapabilities>().also {
- whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(null)
- }
- getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
+ // WHEN the default callback is TRANSPORT_CELLULAR and not carrier merged
+ val caps =
+ Mockito.mock(NetworkCapabilities::class.java).stub {
+ on { hasTransport(TRANSPORT_CELLULAR) } doReturn true
+ on { transportInfo } doReturn null
+ }
+ getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
- // BUT the wifi repo has gotten updates that it *is* carrier merged
- setWifiState(isCarrierMerged = true)
+ // BUT the wifi repo has gotten updates that it *is* carrier merged
+ setWifiState(isCarrierMerged = true)
- // THEN hasCarrierMergedConnection is **false** (The default network being CELLULAR
- // takes precedence over the wifi network being carrier merged.)
- assertThat(latest).isFalse()
- }
+ // THEN hasCarrierMergedConnection is **false** (The default network being CELLULAR
+ // takes precedence over the wifi network being carrier merged.)
+ assertThat(latest).isFalse()
+ }
/** Regression test for b/278618530. */
@Test
fun hasCarrierMergedConnection_defaultCellular_wifiIsCarrierMerged_airplaneMode_isTrue() =
- testScope.runTest {
- val latest by collectLastValue(underTest.hasCarrierMergedConnection)
+ runTest {
+ val latest by underTest.hasCarrierMergedConnection.collectLastValue()
// WHEN the default callback is TRANSPORT_CELLULAR and not carrier merged
val caps =
- mock<NetworkCapabilities>().also {
- whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(null)
+ Mockito.mock(NetworkCapabilities::class.java).stub {
+ on { hasTransport(TRANSPORT_CELLULAR) } doReturn true
+ on { transportInfo } doReturn null
}
getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
@@ -1238,269 +980,254 @@
}
@Test
- fun defaultConnectionIsValidated_startsAsFalse() {
- assertThat(underTest.defaultConnectionIsValidated.value).isFalse()
+ fun defaultConnectionIsValidated_startsAsFalse() = runTest {
+ assertThat(underTest.defaultConnectionIsValidated.collectLastValue().value).isFalse()
}
@Test
- fun defaultConnectionIsValidated_capsHaveValidated_isValidated() =
- testScope.runTest {
- val caps =
- mock<NetworkCapabilities>().also {
- whenever(it.hasCapability(NET_CAPABILITY_VALIDATED)).thenReturn(true)
- }
+ fun defaultConnectionIsValidated_capsHaveValidated_isValidated() = runTest {
+ val caps =
+ Mockito.mock(NetworkCapabilities::class.java).stub {
+ on { hasCapability(NET_CAPABILITY_VALIDATED) } doReturn true
+ }
- val latest by collectLastValue(underTest.defaultConnectionIsValidated)
+ val latest by underTest.defaultConnectionIsValidated.collectLastValue()
- getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
+ getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
- assertThat(latest).isTrue()
- }
+ assertThat(latest).isTrue()
+ }
@Test
- fun defaultConnectionIsValidated_capsHaveNotValidated_isNotValidated() =
- testScope.runTest {
- val caps =
- mock<NetworkCapabilities>().also {
- whenever(it.hasCapability(NET_CAPABILITY_VALIDATED)).thenReturn(false)
- }
+ fun defaultConnectionIsValidated_capsHaveNotValidated_isNotValidated() = runTest {
+ val caps =
+ Mockito.mock(NetworkCapabilities::class.java).stub {
+ on { hasCapability(NET_CAPABILITY_VALIDATED) } doReturn false
+ }
- val latest by collectLastValue(underTest.defaultConnectionIsValidated)
+ val latest by underTest.defaultConnectionIsValidated.collectLastValue()
- getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
+ getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps)
- assertThat(latest).isFalse()
- }
+ assertThat(latest).isFalse()
+ }
@Test
- fun config_initiallyFromContext() =
- testScope.runTest {
- overrideResource(R.bool.config_showMin3G, true)
- val configFromContext = MobileMappings.Config.readConfig(context)
- assertThat(configFromContext.showAtLeast3G).isTrue()
+ fun config_initiallyFromContext() = runTest {
+ overrideResource(R.bool.config_showMin3G, true)
+ val configFromContext = MobileMappings.Config.readConfig(applicationContext)
+ assertThat(configFromContext.showAtLeast3G).isTrue()
- // The initial value will be fetched when the repo is created, so we need to override
- // the resources and then re-create the repo.
- underTest =
- MobileConnectionsRepositoryKairosImpl(
- connectivityRepository,
- subscriptionManager,
- subscriptionManagerProxy,
- telephonyManager,
- logger,
- summaryLogger,
- mobileMappings,
- fakeBroadcastDispatcher,
- context,
- testDispatcher,
- testScope.backgroundScope,
- testDispatcher,
- airplaneModeRepository,
- wifiRepository,
- fullConnectionFactory,
- updateMonitor,
- mock(),
- )
+ val latest by underTest.defaultDataSubRatConfig.collectLastValue()
- val latest by collectLastValue(underTest.defaultDataSubRatConfig)
-
- assertTrue(latest!!.areEqual(configFromContext))
- assertTrue(latest!!.showAtLeast3G)
- }
+ assertTrue(latest!!.areEqual(configFromContext))
+ assertTrue(latest!!.showAtLeast3G)
+ }
@Test
- fun config_subIdChangeEvent_updated() =
- testScope.runTest {
- val latest by collectLastValue(underTest.defaultDataSubRatConfig)
+ fun config_subIdChangeEvent_updated() = runTest {
+ val latest by underTest.defaultDataSubRatConfig.collectLastValue()
- assertThat(latest!!.showAtLeast3G).isFalse()
+ assertThat(latest!!.showAtLeast3G).isFalse()
- overrideResource(R.bool.config_showMin3G, true)
- val configFromContext = MobileMappings.Config.readConfig(context)
- assertThat(configFromContext.showAtLeast3G).isTrue()
+ overrideResource(R.bool.config_showMin3G, true)
+ val configFromContext = MobileMappings.Config.readConfig(applicationContext)
+ assertThat(configFromContext.showAtLeast3G).isTrue()
- // WHEN the change event is fired
- val intent =
- Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
- .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_1_ID)
- fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent)
+ // WHEN the change event is fired
+ val intent =
+ Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
+ .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_1_ID)
+ broadcastDispatcher.sendIntentToMatchingReceiversOnly(applicationContext, intent)
- // THEN the config is updated
- assertTrue(latest!!.areEqual(configFromContext))
- assertTrue(latest!!.showAtLeast3G)
- }
+ // THEN the config is updated
+ assertThat(latest?.areEqual(configFromContext)).isEqualTo(true)
+ assertThat(latest?.showAtLeast3G).isEqualTo(true)
+ }
@Test
- fun config_carrierConfigChangeEvent_updated() =
- testScope.runTest {
- val latest by collectLastValue(underTest.defaultDataSubRatConfig)
+ fun config_carrierConfigChangeEvent_updated() = runTest {
+ val latest by underTest.defaultDataSubRatConfig.collectLastValue()
- assertThat(latest!!.showAtLeast3G).isFalse()
+ assertThat(latest!!.showAtLeast3G).isFalse()
- overrideResource(R.bool.config_showMin3G, true)
- val configFromContext = MobileMappings.Config.readConfig(context)
- assertThat(configFromContext.showAtLeast3G).isTrue()
+ overrideResource(R.bool.config_showMin3G, true)
+ val configFromContext = MobileMappings.Config.readConfig(applicationContext)
+ assertThat(configFromContext.showAtLeast3G).isTrue()
- // WHEN the change event is fired
- fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
- context,
- Intent(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED),
- )
+ // WHEN the change event is fired
+ broadcastDispatcher.sendIntentToMatchingReceiversOnly(
+ applicationContext,
+ Intent(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED),
+ )
- // THEN the config is updated
- assertThat(latest!!.areEqual(configFromContext)).isTrue()
- assertThat(latest!!.showAtLeast3G).isTrue()
- }
+ // THEN the config is updated
+ assertThat(latest?.areEqual(configFromContext)).isEqualTo(true)
+ assertThat(latest?.showAtLeast3G).isEqualTo(true)
+ }
@Test
- fun carrierConfig_initialValueIsFetched() =
- testScope.runTest {
- // Value starts out false
- assertThat(underTest.defaultDataSubRatConfig.value.showAtLeast3G).isFalse()
+ fun carrierConfig_initialValueIsFetched() = runTest {
+ underTest
+ testScope.runCurrent()
- overrideResource(R.bool.config_showMin3G, true)
- val configFromContext = MobileMappings.Config.readConfig(context)
- assertThat(configFromContext.showAtLeast3G).isTrue()
+ // Value starts out false
+ assertThat(underTest.defaultDataSubRatConfig.sample().showAtLeast3G).isFalse()
- // WHEN the change event is fired
- fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
- context,
- Intent(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED),
- )
+ overrideResource(R.bool.config_showMin3G, true)
+ val configFromContext = MobileMappings.Config.readConfig(applicationContext)
+ assertThat(configFromContext.showAtLeast3G).isTrue()
- // WHEN collection starts AFTER the broadcast is sent out
- val latest by collectLastValue(underTest.defaultDataSubRatConfig)
+ assertThat(broadcastDispatcher.numReceiversRegistered).isAtLeast(1)
- // THEN the config has the updated value
- assertThat(latest!!.areEqual(configFromContext)).isTrue()
- assertThat(latest!!.showAtLeast3G).isTrue()
- }
+ // WHEN the change event is fired
+ broadcastDispatcher.sendIntentToMatchingReceiversOnly(
+ applicationContext,
+ Intent(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED),
+ )
+ testScope.runCurrent()
+
+ // WHEN collection starts AFTER the broadcast is sent out
+ val latest by underTest.defaultDataSubRatConfig.collectLastValue()
+
+ // THEN the config has the updated value
+ assertWithMessage("showAtLeast3G is false").that(latest!!.showAtLeast3G).isTrue()
+ assertWithMessage("not equal").that(latest!!.areEqual(configFromContext)).isTrue()
+ }
@Test
- fun activeDataChange_inSameGroup_emitsUnit() =
- testScope.runTest {
- val latest by collectLastValue(underTest.activeSubChangedInGroupEvent)
-
- getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>()
- .onActiveDataSubscriptionIdChanged(SUB_3_ID_GROUPED)
- getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>()
- .onActiveDataSubscriptionIdChanged(SUB_4_ID_GROUPED)
-
- assertThat(latest).isEqualTo(Unit)
+ fun activeDataChange_inSameGroup_emitsUnit() = runTest {
+ var eventCount = 0
+ underTest
+ testScope.backgroundScope.launch {
+ kairos.activateSpec { underTest.activeSubChangedInGroupEvent.observe { eventCount++ } }
}
+ testScope.runCurrent()
+
+ getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>(telephonyManager)
+ .onActiveDataSubscriptionIdChanged(SUB_3_ID_GROUPED)
+ testScope.runCurrent()
+
+ getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>(telephonyManager)
+ .onActiveDataSubscriptionIdChanged(SUB_4_ID_GROUPED)
+ testScope.runCurrent()
+
+ assertThat(eventCount).isEqualTo(1)
+ }
@Test
- fun activeDataChange_notInSameGroup_doesNotEmit() =
- testScope.runTest {
- val latest by collectLastValue(underTest.activeSubChangedInGroupEvent)
-
- getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>()
- .onActiveDataSubscriptionIdChanged(SUB_3_ID_GROUPED)
- getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>()
- .onActiveDataSubscriptionIdChanged(SUB_1_ID)
-
- assertThat(latest).isEqualTo(null)
+ fun activeDataChange_notInSameGroup_doesNotEmit() = runTest {
+ var eventCount = 0
+ underTest
+ testScope.backgroundScope.launch {
+ kairos.activateSpec { underTest.activeSubChangedInGroupEvent.observe { eventCount++ } }
}
+ testScope.runCurrent()
+
+ getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>(telephonyManager)
+ .onActiveDataSubscriptionIdChanged(SUB_3_ID_GROUPED)
+ testScope.runCurrent()
+
+ getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>(telephonyManager)
+ .onActiveDataSubscriptionIdChanged(SUB_1_ID)
+ testScope.runCurrent()
+
+ assertThat(eventCount).isEqualTo(0)
+ }
@Test
- fun anySimSecure_propagatesStateFromKeyguardUpdateMonitor() =
- testScope.runTest {
- val latest by collectLastValue(underTest.isAnySimSecure)
- assertThat(latest).isFalse()
+ fun anySimSecure_propagatesStateFromKeyguardUpdateMonitor() = runTest {
+ val latest by underTest.isAnySimSecure.collectLastValue()
+ assertThat(latest).isFalse()
- val updateMonitorCallback = argumentCaptor<KeyguardUpdateMonitorCallback>()
- verify(updateMonitor).registerCallback(updateMonitorCallback.capture())
+ val updateMonitorCallback = argumentCaptor<KeyguardUpdateMonitorCallback>()
+ verify(keyguardUpdateMonitor).registerCallback(updateMonitorCallback.capture())
- whenever(updateMonitor.isSimPinSecure).thenReturn(true)
- updateMonitorCallback.value.onSimStateChanged(0, 0, 0)
+ keyguardUpdateMonitor.stub { on { isSimPinSecure } doReturn true }
+ updateMonitorCallback.lastValue.onSimStateChanged(0, 0, 0)
- assertThat(latest).isTrue()
+ assertThat(latest).isTrue()
- whenever(updateMonitor.isSimPinSecure).thenReturn(false)
- updateMonitorCallback.value.onSimStateChanged(0, 0, 0)
+ keyguardUpdateMonitor.stub { on { isSimPinSecure } doReturn false }
+ updateMonitorCallback.lastValue.onSimStateChanged(0, 0, 0)
- assertThat(latest).isFalse()
- }
+ assertThat(latest).isFalse()
+ }
@Test
- fun getIsAnySimSecure_delegatesCallToKeyguardUpdateMonitor() =
- testScope.runTest {
- assertThat(underTest.getIsAnySimSecure()).isFalse()
+ fun getIsAnySimSecure_delegatesCallToKeyguardUpdateMonitor() = runTest {
+ val anySimSecure by underTest.isAnySimSecure.collectLastValue()
- whenever(updateMonitor.isSimPinSecure).thenReturn(true)
+ assertThat(anySimSecure).isFalse()
- assertThat(underTest.getIsAnySimSecure()).isTrue()
- }
+ keyguardUpdateMonitor.stub { on { isSimPinSecure } doReturn true }
+ argumentCaptor<KeyguardUpdateMonitorCallback>()
+ .apply { verify(keyguardUpdateMonitor).registerCallback(capture()) }
+ .lastValue
+ .onSimStateChanged(0, 0, 0)
+
+ assertThat(anySimSecure).isTrue()
+ }
@Test
- fun noSubscriptionsInEcmMode_notInEcmMode() =
- testScope.runTest {
- whenever(telephonyManager.emergencyCallbackMode).thenReturn(false)
+ fun noSubscriptionsInEcmMode_notInEcmMode() = runTest {
+ val latest by underTest.isInEcmMode.collectLastValue()
+ testScope.runCurrent()
- runCurrent()
-
- assertThat(underTest.isInEcmMode()).isFalse()
- }
+ assertThat(latest).isFalse()
+ }
@Test
- fun someSubscriptionsInEcmMode_inEcmMode() =
- testScope.runTest {
- whenever(telephonyManager.emergencyCallbackMode).thenReturn(true)
+ fun someSubscriptionsInEcmMode_inEcmMode() = runTest {
+ val latest by underTest.isInEcmMode.collectLastValue()
+ testScope.runCurrent()
- runCurrent()
+ getTelephonyCallbackForType<EmergencyCallbackModeListener>(telephonyManager)
+ .onCallbackModeStarted(0, mock(), 0)
- assertThat(underTest.isInEcmMode()).isTrue()
- }
+ assertThat(latest).isTrue()
+ }
- private fun TestScope.getDefaultNetworkCallback(): ConnectivityManager.NetworkCallback {
- runCurrent()
+ private fun KairosTestScope.getDefaultNetworkCallback(): ConnectivityManager.NetworkCallback {
+ testScope.runCurrent()
val callbackCaptor = argumentCaptor<ConnectivityManager.NetworkCallback>()
verify(connectivityManager).registerDefaultNetworkCallback(callbackCaptor.capture())
- return callbackCaptor.value!!
+ return callbackCaptor.lastValue
}
- private fun setWifiState(isCarrierMerged: Boolean) {
+ private fun KairosTestScope.setWifiState(isCarrierMerged: Boolean) {
if (isCarrierMerged) {
val mergedEntry =
- mock<MergedCarrierEntry>().apply {
- whenever(this.isPrimaryNetwork).thenReturn(true)
- whenever(this.isDefaultNetwork).thenReturn(true)
- whenever(this.subscriptionId).thenReturn(SUB_CM_ID)
+ mock<MergedCarrierEntry> {
+ on { isPrimaryNetwork } doReturn true
+ on { isDefaultNetwork } doReturn true
+ on { subscriptionId } doReturn SUB_CM_ID
}
- whenever(wifiPickerTracker.mergedCarrierEntry).thenReturn(mergedEntry)
- whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(null)
+ wifiPickerTracker.stub {
+ on { mergedCarrierEntry } doReturn mergedEntry
+ on { connectedWifiEntry } doReturn null
+ }
} else {
val wifiEntry =
- mock<WifiEntry>().apply {
- whenever(this.isPrimaryNetwork).thenReturn(true)
- whenever(this.isDefaultNetwork).thenReturn(true)
+ mock<WifiEntry> {
+ on { isPrimaryNetwork } doReturn true
+ on { isDefaultNetwork } doReturn true
}
- whenever(wifiPickerTracker.connectedWifiEntry).thenReturn(wifiEntry)
- whenever(wifiPickerTracker.mergedCarrierEntry).thenReturn(null)
+ wifiPickerTracker.stub {
+ on { connectedWifiEntry } doReturn wifiEntry
+ on { mergedCarrierEntry } doReturn null
+ }
}
- wifiPickerTrackerCallback.value.onWifiEntriesChanged()
+ wifiPickerTrackerCallback.allValues.forEach { it.onWifiEntriesChanged() }
}
- private fun TestScope.getSubscriptionCallback():
- SubscriptionManager.OnSubscriptionsChangedListener {
- runCurrent()
- val callbackCaptor = argumentCaptor<SubscriptionManager.OnSubscriptionsChangedListener>()
- verify(subscriptionManager)
- .addOnSubscriptionsChangedListener(any(), callbackCaptor.capture())
- return callbackCaptor.value!!
- }
-
- private fun TestScope.getTelephonyCallbacks(): List<TelephonyCallback> {
- runCurrent()
- val callbackCaptor = argumentCaptor<TelephonyCallback>()
- verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture())
- return callbackCaptor.allValues
- }
-
- private inline fun <reified T> TestScope.getTelephonyCallbackForType(): T {
- val cbs = this.getTelephonyCallbacks().filterIsInstance<T>()
- assertThat(cbs.size).isEqualTo(1)
- return cbs[0]
+ private fun KairosTestScope.getSubscriptionCallback(): OnSubscriptionsChangedListener {
+ testScope.runCurrent()
+ return argumentCaptor<OnSubscriptionsChangedListener>()
+ .apply {
+ verify(subscriptionManager).addOnSubscriptionsChangedListener(any(), capture())
+ }
+ .lastValue
}
companion object {
@@ -1509,11 +1236,11 @@
private const val SUB_1_NAME = "Carrier $SUB_1_ID"
private val GROUP_1 = ParcelUuid(UUID.randomUUID())
private val SUB_1 =
- mock<SubscriptionInfo>().also {
- whenever(it.subscriptionId).thenReturn(SUB_1_ID)
- whenever(it.groupUuid).thenReturn(GROUP_1)
- whenever(it.carrierName).thenReturn(SUB_1_NAME)
- whenever(it.profileClass).thenReturn(PROFILE_CLASS_UNSET)
+ mock<SubscriptionInfo> {
+ on { subscriptionId } doReturn SUB_1_ID
+ on { groupUuid } doReturn GROUP_1
+ on { carrierName } doReturn SUB_1_NAME
+ on { profileClass } doReturn PROFILE_CLASS_UNSET
}
private val MODEL_1 =
SubscriptionModel(
@@ -1528,11 +1255,11 @@
private const val SUB_2_NAME = "Carrier $SUB_2_ID"
private val GROUP_2 = ParcelUuid(UUID.randomUUID())
private val SUB_2 =
- mock<SubscriptionInfo>().also {
- whenever(it.subscriptionId).thenReturn(SUB_2_ID)
- whenever(it.groupUuid).thenReturn(GROUP_2)
- whenever(it.carrierName).thenReturn(SUB_2_NAME)
- whenever(it.profileClass).thenReturn(PROFILE_CLASS_UNSET)
+ mock<SubscriptionInfo> {
+ on { subscriptionId } doReturn SUB_2_ID
+ on { groupUuid } doReturn GROUP_2
+ on { carrierName } doReturn SUB_2_NAME
+ on { profileClass } doReturn PROFILE_CLASS_UNSET
}
private val MODEL_2 =
SubscriptionModel(
@@ -1548,34 +1275,34 @@
// Subscription 3
private const val SUB_3_ID_GROUPED = 3
private val SUB_3 =
- mock<SubscriptionInfo>().also {
- whenever(it.subscriptionId).thenReturn(SUB_3_ID_GROUPED)
- whenever(it.groupUuid).thenReturn(GROUP_ID_3_4)
- whenever(it.profileClass).thenReturn(PROFILE_CLASS_UNSET)
+ mock<SubscriptionInfo> {
+ on { subscriptionId } doReturn SUB_3_ID_GROUPED
+ on { groupUuid } doReturn GROUP_ID_3_4
+ on { profileClass } doReturn PROFILE_CLASS_UNSET
}
// Subscription 4
private const val SUB_4_ID_GROUPED = 4
private val SUB_4 =
- mock<SubscriptionInfo>().also {
- whenever(it.subscriptionId).thenReturn(SUB_4_ID_GROUPED)
- whenever(it.groupUuid).thenReturn(GROUP_ID_3_4)
- whenever(it.profileClass).thenReturn(PROFILE_CLASS_UNSET)
+ mock<SubscriptionInfo> {
+ on { subscriptionId } doReturn SUB_4_ID_GROUPED
+ on { groupUuid } doReturn GROUP_ID_3_4
+ on { profileClass } doReturn PROFILE_CLASS_UNSET
}
// Subs 3 and 4 are considered to be in the same group ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
private const val NET_ID = 123
- private val NETWORK = mock<Network>().apply { whenever(getNetId()).thenReturn(NET_ID) }
+ private val NETWORK = mock<Network> { on { getNetId() } doReturn NET_ID }
// Carrier merged subscription
private const val SUB_CM_ID = 5
private const val SUB_CM_NAME = "Carrier $SUB_CM_ID"
private val SUB_CM =
- mock<SubscriptionInfo>().also {
- whenever(it.subscriptionId).thenReturn(SUB_CM_ID)
- whenever(it.carrierName).thenReturn(SUB_CM_NAME)
- whenever(it.profileClass).thenReturn(PROFILE_CLASS_UNSET)
+ mock<SubscriptionInfo> {
+ on { subscriptionId } doReturn SUB_CM_ID
+ on { carrierName } doReturn SUB_CM_NAME
+ on { profileClass } doReturn PROFILE_CLASS_UNSET
}
private val MODEL_CM =
SubscriptionModel(
@@ -1585,28 +1312,29 @@
)
private val WIFI_INFO_CM =
- mock<WifiInfo>().apply {
- whenever(this.isPrimary).thenReturn(true)
- whenever(this.isCarrierMerged).thenReturn(true)
- whenever(this.subscriptionId).thenReturn(SUB_CM_ID)
+ mock<WifiInfo> {
+ on { isPrimary } doReturn true
+ on { isCarrierMerged } doReturn true
+ on { subscriptionId } doReturn SUB_CM_ID
}
private val WIFI_NETWORK_CAPS_CM =
- mock<NetworkCapabilities>().also {
- whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(WIFI_INFO_CM)
- whenever(it.hasCapability(NET_CAPABILITY_VALIDATED)).thenReturn(true)
+ Mockito.mock(NetworkCapabilities::class.java).stub {
+ on { hasTransport(TRANSPORT_WIFI) } doReturn true
+ on { transportInfo } doReturn WIFI_INFO_CM
+ on { hasCapability(NET_CAPABILITY_VALIDATED) } doReturn true
}
private val WIFI_INFO_ACTIVE =
- mock<WifiInfo>().apply {
- whenever(this.isPrimary).thenReturn(true)
- whenever(this.isCarrierMerged).thenReturn(false)
+ mock<WifiInfo> {
+ on { isPrimary } doReturn true
+ on { isCarrierMerged } doReturn false
}
+
private val WIFI_NETWORK_CAPS_ACTIVE =
- mock<NetworkCapabilities>().also {
- whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(WIFI_INFO_ACTIVE)
- whenever(it.hasCapability(NET_CAPABILITY_VALIDATED)).thenReturn(true)
+ Mockito.mock(NetworkCapabilities::class.java).stub {
+ on { hasTransport(TRANSPORT_WIFI) } doReturn true
+ on { transportInfo } doReturn WIFI_INFO_ACTIVE
+ on { hasCapability(NET_CAPABILITY_VALIDATED) } doReturn true
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/log/table/Diffable.kt b/packages/SystemUI/src/com/android/systemui/log/table/Diffable.kt
index 9fddbfb..a154694 100644
--- a/packages/SystemUI/src/com/android/systemui/log/table/Diffable.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/table/Diffable.kt
@@ -16,6 +16,11 @@
package com.android.systemui.log.table
+import com.android.systemui.kairos.BuildScope
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.State
+import com.android.systemui.kairos.changes
+import com.android.systemui.kairos.effect
import com.android.systemui.util.kotlin.pairwiseBy
import kotlinx.coroutines.flow.Flow
@@ -184,3 +189,94 @@
newVal
}
}
+
+/** See [logDiffsForTable(TableLogBuffer, String, T)]. */
+@ExperimentalKairosApi
+@JvmName("logIntDiffsForTable")
+fun BuildScope.logDiffsForTable(
+ intState: State<Int?>,
+ tableLogBuffer: TableLogBuffer,
+ columnPrefix: String = "",
+ columnName: String,
+) {
+ var isInitial = true
+ intState.observe { new ->
+ tableLogBuffer.logChange(columnPrefix, columnName, new, isInitial = isInitial)
+ isInitial = false
+ }
+}
+
+/**
+ * Each time the flow is updated with a new value, logs the differences between the previous value
+ * and the new value to the given [tableLogBuffer].
+ *
+ * The new value's [Diffable.logDiffs] method will be used to log the differences to the table.
+ *
+ * @param columnPrefix a prefix that will be applied to every column name that gets logged.
+ */
+@ExperimentalKairosApi
+fun <T : Diffable<T>> BuildScope.logDiffsForTable(
+ diffableState: State<T>,
+ tableLogBuffer: TableLogBuffer,
+ columnPrefix: String = "",
+) {
+ val initialValue = diffableState.sampleDeferred()
+ effect {
+ // Fully log the initial value to the table.
+ tableLogBuffer.logChange(columnPrefix, isInitial = true) { row ->
+ initialValue.value.logFull(row)
+ }
+ }
+ diffableState.changes.observe { newState ->
+ val prevState = diffableState.sample()
+ tableLogBuffer.logDiffs(columnPrefix, prevVal = prevState, newVal = newState)
+ }
+}
+
+/** See [logDiffsForTable(TableLogBuffer, String, T)]. */
+@ExperimentalKairosApi
+@JvmName("logBooleanDiffsForTable")
+fun BuildScope.logDiffsForTable(
+ booleanState: State<Boolean>,
+ tableLogBuffer: TableLogBuffer,
+ columnPrefix: String = "",
+ columnName: String,
+) {
+ var isInitial = true
+ booleanState.observe { new ->
+ tableLogBuffer.logChange(columnPrefix, columnName, new, isInitial = isInitial)
+ isInitial = false
+ }
+}
+
+/** See [logDiffsForTable(TableLogBuffer, String, T)]. */
+@ExperimentalKairosApi
+@JvmName("logStringDiffsForTable")
+fun BuildScope.logDiffsForTable(
+ stringState: State<String?>,
+ tableLogBuffer: TableLogBuffer,
+ columnPrefix: String = "",
+ columnName: String,
+) {
+ var isInitial = true
+ stringState.observe { new ->
+ tableLogBuffer.logChange(columnPrefix, columnName, new, isInitial = isInitial)
+ isInitial = false
+ }
+}
+
+/** See [logDiffsForTable(TableLogBuffer, String, T)]. */
+@ExperimentalKairosApi
+@JvmName("logListDiffsForTable")
+fun <T> BuildScope.logDiffsForTable(
+ listState: State<List<T>>,
+ tableLogBuffer: TableLogBuffer,
+ columnPrefix: String = "",
+ columnName: String,
+) {
+ var isInitial = true
+ listState.observe { new ->
+ tableLogBuffer.logChange(columnPrefix, columnName, new.toString(), isInitial = isInitial)
+ isInitial = false
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index a18495e..61c0055 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -20,6 +20,7 @@
import com.android.systemui.CoreStartable
import com.android.systemui.Flags
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.kairos.ExperimentalKairosApi
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.LogBufferFactory
import com.android.systemui.log.table.TableLogBuffer
@@ -36,8 +37,12 @@
import com.android.systemui.statusbar.pipeline.mobile.data.repository.CarrierConfigRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.CarrierConfigRepositoryImpl
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepositoryKairosAdapter
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileRepositorySwitcher
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileRepositorySwitcherKairos
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoModeMobileConnectionDataSourceKairosImpl
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileConnectionRepositoryKairosFactoryImpl
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileConnectionsRepositoryKairosImpl
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorImpl
import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter
@@ -79,7 +84,17 @@
import javax.inject.Provider
import kotlinx.coroutines.flow.Flow
-@Module
+@OptIn(ExperimentalKairosApi::class)
+@Module(
+ includes =
+ [
+ DemoModeMobileConnectionDataSourceKairosImpl.Module::class,
+ MobileRepositorySwitcherKairos.Module::class,
+ MobileConnectionsRepositoryKairosImpl.Module::class,
+ MobileConnectionRepositoryKairosFactoryImpl.Module::class,
+ MobileConnectionsRepositoryKairosAdapter.Module::class,
+ ]
+)
abstract class StatusBarPipelineModule {
@Binds
abstract fun airplaneModeRepository(impl: AirplaneModeRepositoryImpl): AirplaneModeRepository
@@ -158,7 +173,7 @@
@Provides
fun mobileConnectionsRepository(
impl: Provider<MobileRepositorySwitcher>,
- kairosImpl: Provider<MobileRepositorySwitcherKairos>,
+ kairosImpl: Provider<MobileConnectionsRepositoryKairosAdapter>,
): MobileConnectionsRepository {
return if (Flags.statusBarMobileIconKairos()) {
kairosImpl.get()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryKairos.kt
index 8e53f64..2e79626 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryKairos.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionRepositoryKairos.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,39 +17,39 @@
package com.android.systemui.statusbar.pipeline.mobile.data.repository
import android.telephony.CellSignalStrength
-import android.telephony.SubscriptionInfo
import android.telephony.TelephonyManager
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.State
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
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.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
-import kotlinx.coroutines.flow.StateFlow
/**
* Every mobile line of service can be identified via a [SubscriptionInfo] object. We set up a
* repository for each individual, tracked subscription via [MobileConnectionsRepository], and this
* repository is responsible for setting up a [TelephonyManager] object tied to its subscriptionId
*
- * There should only ever be one [MobileConnectionRepositoryKairos] per subscription, since
+ * There should only ever be one [MobileConnectionRepository] per subscription, since
* [TelephonyManager] limits the number of callbacks that can be registered per process.
*
* This repository should have all of the relevant information for a single line of service, which
* eventually becomes a single icon in the status bar.
*/
+@ExperimentalKairosApi
interface MobileConnectionRepositoryKairos {
/** The subscriptionId that this connection represents */
val subId: Int
/** The carrierId for this connection. See [TelephonyManager.getSimCarrierId] */
- val carrierId: StateFlow<Int>
+ val carrierId: State<Int>
/** Reflects the value from the carrier config INFLATE_SIGNAL_STRENGTH for this connection */
- val inflateSignalStrength: StateFlow<Boolean>
+ val inflateSignalStrength: State<Boolean>
/** Carrier config KEY_SHOW_5G_SLICE_ICON_BOOL for this connection */
- val allowNetworkSliceIndicator: StateFlow<Boolean>
+ val allowNetworkSliceIndicator: State<Boolean>
/**
* The table log buffer created for this connection. Will have the name "MobileConnectionLog
@@ -58,17 +58,17 @@
val tableLogBuffer: TableLogBuffer
/** True if the [android.telephony.ServiceState] says this connection is emergency calls only */
- val isEmergencyOnly: StateFlow<Boolean>
+ val isEmergencyOnly: State<Boolean>
/** True if [android.telephony.ServiceState] says we are roaming */
- val isRoaming: StateFlow<Boolean>
+ val isRoaming: State<Boolean>
/**
* See [android.telephony.ServiceState.getOperatorAlphaShort], this value is defined as the
* current registered operator name in short alphanumeric format. In some cases this name might
* be preferred over other methods of calculating the network name
*/
- val operatorAlphaShort: StateFlow<String?>
+ val operatorAlphaShort: State<String?>
/**
* TODO (b/263167683): Clarify this field
@@ -78,7 +78,7 @@
* connection to be in-service if either the voice registration state is IN_SERVICE or the data
* registration state is IN_SERVICE and NOT IWLAN.
*/
- val isInService: StateFlow<Boolean>
+ val isInService: State<Boolean>
/**
* True if this subscription is actively connected to a non-terrestrial network and false
@@ -91,48 +91,48 @@
* during the lifetime of a subscription but [SubscriptionModel.isExclusivelyNonTerrestrial]
* will stay constant.
*/
- val isNonTerrestrial: StateFlow<Boolean>
+ val isNonTerrestrial: State<Boolean>
/** True if [android.telephony.SignalStrength] told us that this connection is using GSM */
- val isGsm: StateFlow<Boolean>
+ val isGsm: State<Boolean>
/**
* There is still specific logic in the pipeline that calls out CDMA level explicitly. This
* field is not completely orthogonal to [primaryLevel], because CDMA could be primary.
*/
// @IntRange(from = 0, to = 4)
- val cdmaLevel: StateFlow<Int>
+ val cdmaLevel: State<Int>
/** [android.telephony.SignalStrength]'s concept of the overall signal level */
// @IntRange(from = 0, to = 4)
- val primaryLevel: StateFlow<Int>
+ val primaryLevel: State<Int>
/**
* This level can be used to reflect the signal strength when in carrier roaming NTN mode
* (carrier-based satellite)
*/
- val satelliteLevel: StateFlow<Int>
+ val satelliteLevel: State<Int>
/** The current data connection state. See [DataConnectionState] */
- val dataConnectionState: StateFlow<DataConnectionState>
+ val dataConnectionState: State<DataConnectionState>
/** The current data activity direction. See [DataActivityModel] */
- val dataActivityDirection: StateFlow<DataActivityModel>
+ val dataActivityDirection: State<DataActivityModel>
/** True if there is currently a carrier network change in process */
- val carrierNetworkChangeActive: StateFlow<Boolean>
+ val carrierNetworkChangeActive: State<Boolean>
/**
* [resolvedNetworkType] is the [TelephonyDisplayInfo.getOverrideNetworkType] if it exists or
* [TelephonyDisplayInfo.getNetworkType]. This is used to look up the proper network type icon
*/
- val resolvedNetworkType: StateFlow<ResolvedNetworkType>
+ val resolvedNetworkType: State<ResolvedNetworkType>
/** The total number of levels. Used with [SignalDrawable]. */
- val numberOfLevels: StateFlow<Int>
+ val numberOfLevels: State<Int>
/** Observable tracking [TelephonyManager.isDataConnectionAllowed] */
- val dataEnabled: StateFlow<Boolean>
+ val dataEnabled: State<Boolean>
/**
* See [TelephonyManager.getCdmaEnhancedRoamingIndicatorDisplayNumber]. This bit only matters if
@@ -140,10 +140,10 @@
*
* True if the Enhanced Roaming Indicator (ERI) display number is not [TelephonyManager.ERI_OFF]
*/
- val cdmaRoaming: StateFlow<Boolean>
+ val cdmaRoaming: State<Boolean>
/** The service provider name for this network connection, or the default name. */
- val networkName: StateFlow<NetworkNameModel>
+ val networkName: State<NetworkNameModel>
/**
* The service provider name for this network connection, or the default name.
@@ -151,25 +151,25 @@
* TODO(b/296600321): De-duplicate this field with [networkName] after determining the data
* provided is identical
*/
- val carrierName: StateFlow<NetworkNameModel>
+ val carrierName: State<NetworkNameModel>
/**
* True if this type of connection is allowed while airplane mode is on, and false otherwise.
*/
- val isAllowedDuringAirplaneMode: StateFlow<Boolean>
+ val isAllowedDuringAirplaneMode: State<Boolean>
/**
* True if this network has NET_CAPABILITIY_PRIORITIZE_LATENCY, and can be considered to be a
* network slice
*/
- val hasPrioritizedNetworkCapabilities: StateFlow<Boolean>
+ val hasPrioritizedNetworkCapabilities: State<Boolean>
/**
* True if this connection is in emergency callback mode.
*
* @see [TelephonyManager.getEmergencyCallbackMode]
*/
- suspend fun isInEcmMode(): Boolean
+ val isInEcmMode: State<Boolean>
companion object {
/** The default number of levels to use for [numberOfLevels]. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryKairos.kt
index b3cbbfd..79bfb6e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryKairos.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryKairos.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,35 +21,42 @@
import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.settingslib.mobile.MobileMappings
import com.android.settingslib.mobile.MobileMappings.Config
+import com.android.systemui.kairos.Events
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.Incremental
+import com.android.systemui.kairos.State
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.StateFlow
/**
* Repo for monitoring the complete active subscription info list, to be consumed and filtered based
* on various policy
*/
+@ExperimentalKairosApi
interface MobileConnectionsRepositoryKairos {
+
+ /** All active mobile connections. */
+ val mobileConnectionsBySubId: Incremental<Int, MobileConnectionRepositoryKairos>
+
/** Observable list of current mobile subscriptions */
- val subscriptions: StateFlow<List<SubscriptionModel>>
+ val subscriptions: State<Collection<SubscriptionModel>>
/**
* Observable for the subscriptionId of the current mobile data connection. Null if we don't
* have a valid subscription id
*/
- val activeMobileDataSubscriptionId: StateFlow<Int?>
+ val activeMobileDataSubscriptionId: State<Int?>
/** Repo that tracks the current [activeMobileDataSubscriptionId] */
- val activeMobileDataRepository: StateFlow<MobileConnectionRepository?>
+ val activeMobileDataRepository: State<MobileConnectionRepositoryKairos?>
/**
* 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>
+ val activeSubChangedInGroupEvent: Events<Unit>
- /** Tracks [SubscriptionManager.getDefaultDataSubscriptionId]. Null if there is no default */
- val defaultDataSubId: StateFlow<Int?>
+ /** Tracks [SubscriptionManager.getDefaultDataSubscriptionId]. `null` if there is no default. */
+ val defaultDataSubId: State<Int?>
/**
* True if the default network connection is a mobile-like connection and false otherwise.
@@ -58,20 +65,17 @@
* there are edge cases (like carrier merged wifi) that could also result in the default
* connection being mobile-like.
*/
- val mobileIsDefault: StateFlow<Boolean>
+ val mobileIsDefault: State<Boolean>
/**
* True if the device currently has a carrier merged connection.
*
* See [CarrierMergedConnectionRepository] for more info.
*/
- val hasCarrierMergedConnection: Flow<Boolean>
+ val hasCarrierMergedConnection: State<Boolean>
/** True if the default network connection is validated and false otherwise. */
- val defaultConnectionIsValidated: StateFlow<Boolean>
-
- /** Get or create a repository for the line of service for the given subscription ID */
- fun getRepoForSubId(subId: Int): MobileConnectionRepository
+ val defaultConnectionIsValidated: State<Boolean>
/**
* [Config] is an object that tracks relevant configuration flags for a given subscription ID.
@@ -83,13 +87,13 @@
*
* This flow will produce whenever the default data subscription or the carrier config changes.
*/
- val defaultDataSubRatConfig: StateFlow<Config>
+ val defaultDataSubRatConfig: State<Config>
/** The icon mapping from network type to [MobileIconGroup] for the default subscription */
- val defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>>
+ val defaultMobileIconMapping: State<Map<String, MobileIconGroup>>
/** Fallback [MobileIconGroup] in the case where there is no icon in the mapping */
- val defaultMobileIconGroup: Flow<MobileIconGroup>
+ val defaultMobileIconGroup: State<MobileIconGroup>
/**
* Can the device make emergency calls using the device-based service state? This field is only
@@ -100,7 +104,7 @@
*
* This is an eager flow, and re-evaluates whenever ACTION_SERVICE_STATE is sent for subId = -1.
*/
- val isDeviceEmergencyCallCapable: StateFlow<Boolean>
+ val isDeviceEmergencyCallCapable: State<Boolean>
/**
* If any active SIM on the device is in
@@ -108,22 +112,11 @@
* [android.telephony.TelephonyManager.SIM_STATE_PUK_REQUIRED] or
* [android.telephony.TelephonyManager.SIM_STATE_PERM_DISABLED]
*/
- val isAnySimSecure: Flow<Boolean>
-
- /**
- * Returns whether any active SIM on the device is in
- * [android.telephony.TelephonyManager.SIM_STATE_PIN_REQUIRED] or
- * [android.telephony.TelephonyManager.SIM_STATE_PUK_REQUIRED] or
- * [android.telephony.TelephonyManager.SIM_STATE_PERM_DISABLED].
- *
- * Note: Unfortunately, we cannot name this [isAnySimSecure] due to a conflict with the flow
- * name above (Java code-gen is having issues with it).
- */
- fun getIsAnySimSecure(): Boolean
+ val isAnySimSecure: State<Boolean>
/**
* Checks if any subscription has [android.telephony.TelephonyManager.getEmergencyCallbackMode]
* == true
*/
- suspend fun isInEcmMode(): Boolean
+ val isInEcmMode: State<Boolean>
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryKairosAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryKairosAdapter.kt
new file mode 100644
index 0000000..64144d9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryKairosAdapter.kt
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository
+
+import android.content.Context
+import com.android.settingslib.SignalIcon
+import com.android.settingslib.mobile.MobileMappings
+import com.android.systemui.Flags
+import com.android.systemui.KairosActivatable
+import com.android.systemui.KairosBuilder
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosNetwork
+import com.android.systemui.kairos.buildSpec
+import com.android.systemui.kairos.combine
+import com.android.systemui.kairos.map
+import com.android.systemui.kairos.mapValues
+import com.android.systemui.kairos.toColdConflatedFlow
+import com.android.systemui.kairosBuilder
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileConnectionRepositoryKairosAdapter
+import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
+import dagger.Provides
+import dagger.multibindings.ElementsIntoSet
+import javax.inject.Inject
+import javax.inject.Provider
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.stateIn
+
+@ExperimentalKairosApi
+@SysUISingleton
+class MobileConnectionsRepositoryKairosAdapter
+@Inject
+constructor(
+ private val kairosRepo: MobileConnectionsRepositoryKairos,
+ private val kairosNetwork: KairosNetwork,
+ @Application scope: CoroutineScope,
+ connectivityRepository: ConnectivityRepository,
+ context: Context,
+ carrierConfigRepo: CarrierConfigRepository,
+) : MobileConnectionsRepository, KairosBuilder by kairosBuilder() {
+ override val subscriptions: StateFlow<List<SubscriptionModel>> =
+ kairosRepo.subscriptions
+ .map { it.toList() }
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), emptyList())
+
+ override val activeMobileDataSubscriptionId: StateFlow<Int?> =
+ kairosRepo.activeMobileDataSubscriptionId
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), null)
+
+ private val reposBySubIdK = buildIncremental {
+ kairosRepo.mobileConnectionsBySubId
+ .mapValues { (subId, repo) ->
+ buildSpec {
+ MobileConnectionRepositoryKairosAdapter(
+ kairosRepo = repo,
+ carrierConfig = carrierConfigRepo.getOrCreateConfigForSubId(subId),
+ )
+ }
+ }
+ .applyLatestSpecForKey()
+ }
+
+ private val reposBySubId =
+ reposBySubIdK
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(scope, SharingStarted.Eagerly, emptyMap())
+
+ override val activeMobileDataRepository: StateFlow<MobileConnectionRepository?> =
+ combine(kairosRepo.activeMobileDataSubscriptionId, reposBySubIdK) { id, repos -> repos[id] }
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), null)
+
+ override val activeSubChangedInGroupEvent: Flow<Unit> =
+ kairosRepo.activeSubChangedInGroupEvent.toColdConflatedFlow(kairosNetwork)
+
+ override val defaultDataSubId: StateFlow<Int?> =
+ kairosRepo.defaultDataSubId
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(scope, SharingStarted.WhileSubscribed(), null)
+
+ override val mobileIsDefault: StateFlow<Boolean> =
+ kairosRepo.mobileIsDefault
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(
+ scope,
+ SharingStarted.WhileSubscribed(),
+ connectivityRepository.defaultConnections.value.mobile.isDefault,
+ )
+
+ override val hasCarrierMergedConnection: Flow<Boolean> =
+ kairosRepo.hasCarrierMergedConnection.toColdConflatedFlow(kairosNetwork)
+
+ override val defaultConnectionIsValidated: StateFlow<Boolean> =
+ kairosRepo.defaultConnectionIsValidated
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(
+ scope,
+ SharingStarted.WhileSubscribed(),
+ connectivityRepository.defaultConnections.value.isValidated,
+ )
+
+ override fun getRepoForSubId(subId: Int): MobileConnectionRepository =
+ reposBySubId.value[subId] ?: error("Unknown subscription id: $subId")
+
+ override val defaultDataSubRatConfig: StateFlow<MobileMappings.Config> =
+ kairosRepo.defaultDataSubRatConfig
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(
+ scope,
+ SharingStarted.WhileSubscribed(),
+ MobileMappings.Config.readConfig(context),
+ )
+
+ override val defaultMobileIconMapping: Flow<Map<String, SignalIcon.MobileIconGroup>> =
+ kairosRepo.defaultMobileIconMapping.toColdConflatedFlow(kairosNetwork)
+
+ override val defaultMobileIconGroup: Flow<SignalIcon.MobileIconGroup> =
+ kairosRepo.defaultMobileIconGroup.toColdConflatedFlow(kairosNetwork)
+
+ override val isDeviceEmergencyCallCapable: StateFlow<Boolean> =
+ kairosRepo.isDeviceEmergencyCallCapable
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(scope, SharingStarted.Eagerly, false)
+
+ override val isAnySimSecure: StateFlow<Boolean> =
+ kairosRepo.isAnySimSecure
+ .toColdConflatedFlow(kairosNetwork)
+ .stateIn(scope, SharingStarted.Eagerly, false)
+
+ override fun getIsAnySimSecure(): Boolean = isAnySimSecure.value
+
+ override suspend fun isInEcmMode(): Boolean =
+ kairosNetwork.transact { kairosRepo.isInEcmMode.sample() }
+
+ @dagger.Module
+ object Module {
+ @Provides
+ @ElementsIntoSet
+ fun kairosActivatable(
+ impl: Provider<MobileConnectionsRepositoryKairosAdapter>
+ ): Set<@JvmSuppressWildcards KairosActivatable> =
+ if (Flags.statusBarMobileIconKairos()) setOf(impl.get()) else emptySet()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherKairos.kt
index 3c855a9..1f5b849 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherKairos.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherKairos.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,23 +20,32 @@
import androidx.annotation.VisibleForTesting
import com.android.settingslib.SignalIcon
import com.android.settingslib.mobile.MobileMappings
+import com.android.systemui.Flags
+import com.android.systemui.KairosActivatable
+import com.android.systemui.KairosBuilder
+import com.android.systemui.activated
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.demomode.DemoMode
import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.kairos.Events
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.Incremental
+import com.android.systemui.kairos.State
+import com.android.systemui.kairos.flatMap
+import com.android.systemui.kairos.map
+import com.android.systemui.kairos.switchEvents
+import com.android.systemui.kairos.switchIncremental
+import com.android.systemui.kairosBuilder
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoMobileConnectionsRepository
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileConnectionsRepositoryImpl
-import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoMobileConnectionsRepositoryKairos
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileConnectionsRepositoryKairosImpl
+import dagger.Binds
+import dagger.Provides
+import dagger.multibindings.ElementsIntoSet
import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
+import javax.inject.Provider
import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.mapLatest
-import kotlinx.coroutines.flow.stateIn
/**
* A provider for the [MobileConnectionsRepository] interface that can choose between the Demo and
@@ -60,18 +69,17 @@
* a change (due to `distinctUntilChanged`) and will not refresh their data providers to the demo
* implementation.
*/
-@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@ExperimentalKairosApi
@SysUISingleton
class MobileRepositorySwitcherKairos
@Inject
constructor(
- @Background scope: CoroutineScope,
- val realRepository: MobileConnectionsRepositoryImpl,
- val demoMobileConnectionsRepository: DemoMobileConnectionsRepository,
+ private val realRepository: MobileConnectionsRepositoryKairosImpl,
+ private val demoRepositoryFactory: DemoMobileConnectionsRepositoryKairos.Factory,
demoModeController: DemoModeController,
-) : MobileConnectionsRepository {
+) : MobileConnectionsRepositoryKairos, KairosBuilder by kairosBuilder() {
- val isDemoMode: StateFlow<Boolean> =
+ private val isDemoMode: State<Boolean> = buildState {
conflatedCallbackFlow {
val callback =
object : DemoMode {
@@ -80,12 +88,10 @@
}
override fun onDemoModeStarted() {
- demoMobileConnectionsRepository.startProcessingCommands()
trySend(true)
}
override fun onDemoModeFinished() {
- demoMobileConnectionsRepository.stopProcessingCommands()
trySend(false)
}
}
@@ -93,114 +99,73 @@
demoModeController.addCallback(callback)
awaitClose { demoModeController.removeCallback(callback) }
}
- .stateIn(scope, SharingStarted.WhileSubscribed(), demoModeController.isInDemoMode)
+ .toState(demoModeController.isInDemoMode)
+ }
// Convenient definition flow for the currently active repo (based on demo mode or not)
@VisibleForTesting
- val activeRepo: StateFlow<MobileConnectionsRepository> =
- isDemoMode
- .mapLatest { demoMode ->
- if (demoMode) {
- demoMobileConnectionsRepository
- } else {
- realRepository
- }
+ val activeRepo: State<MobileConnectionsRepositoryKairos> = buildState {
+ isDemoMode.mapLatestBuild { demoMode ->
+ if (demoMode) {
+ activated { demoRepositoryFactory.create() }
+ } else {
+ realRepository
}
- .stateIn(scope, SharingStarted.WhileSubscribed(), realRepository)
-
- override val subscriptions: StateFlow<List<SubscriptionModel>> =
- activeRepo
- .flatMapLatest { it.subscriptions }
- .stateIn(scope, SharingStarted.WhileSubscribed(), realRepository.subscriptions.value)
-
- override val activeMobileDataSubscriptionId: StateFlow<Int?> =
- activeRepo
- .flatMapLatest { it.activeMobileDataSubscriptionId }
- .stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- realRepository.activeMobileDataSubscriptionId.value,
- )
-
- override val activeMobileDataRepository: StateFlow<MobileConnectionRepository?> =
- activeRepo
- .flatMapLatest { it.activeMobileDataRepository }
- .stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- realRepository.activeMobileDataRepository.value,
- )
-
- override val activeSubChangedInGroupEvent: Flow<Unit> =
- activeRepo.flatMapLatest { it.activeSubChangedInGroupEvent }
-
- override val defaultDataSubRatConfig: StateFlow<MobileMappings.Config> =
- activeRepo
- .flatMapLatest { it.defaultDataSubRatConfig }
- .stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- realRepository.defaultDataSubRatConfig.value,
- )
-
- override val defaultMobileIconMapping: Flow<Map<String, SignalIcon.MobileIconGroup>> =
- activeRepo.flatMapLatest { it.defaultMobileIconMapping }
-
- override val defaultMobileIconGroup: Flow<SignalIcon.MobileIconGroup> =
- activeRepo.flatMapLatest { it.defaultMobileIconGroup }
-
- override val isDeviceEmergencyCallCapable: StateFlow<Boolean> =
- activeRepo
- .flatMapLatest { it.isDeviceEmergencyCallCapable }
- .stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- realRepository.isDeviceEmergencyCallCapable.value,
- )
-
- override val isAnySimSecure: Flow<Boolean> = activeRepo.flatMapLatest { it.isAnySimSecure }
-
- override fun getIsAnySimSecure(): Boolean = activeRepo.value.getIsAnySimSecure()
-
- override val defaultDataSubId: StateFlow<Int?> =
- activeRepo
- .flatMapLatest { it.defaultDataSubId }
- .stateIn(scope, SharingStarted.WhileSubscribed(), realRepository.defaultDataSubId.value)
-
- override val mobileIsDefault: StateFlow<Boolean> =
- activeRepo
- .flatMapLatest { it.mobileIsDefault }
- .stateIn(scope, SharingStarted.WhileSubscribed(), realRepository.mobileIsDefault.value)
-
- override val hasCarrierMergedConnection: StateFlow<Boolean> =
- activeRepo
- .flatMapLatest { it.hasCarrierMergedConnection }
- .stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- realRepository.hasCarrierMergedConnection.value,
- )
-
- override val defaultConnectionIsValidated: StateFlow<Boolean> =
- activeRepo
- .flatMapLatest { it.defaultConnectionIsValidated }
- .stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- realRepository.defaultConnectionIsValidated.value,
- )
-
- override fun getRepoForSubId(subId: Int): MobileConnectionRepository {
- if (isDemoMode.value) {
- return demoMobileConnectionsRepository.getRepoForSubId(subId)
}
- return realRepository.getRepoForSubId(subId)
}
- override suspend fun isInEcmMode(): Boolean =
- if (isDemoMode.value) {
- demoMobileConnectionsRepository.isInEcmMode()
- } else {
- realRepository.isInEcmMode()
+ override val mobileConnectionsBySubId: Incremental<Int, MobileConnectionRepositoryKairos> =
+ activeRepo.map { it.mobileConnectionsBySubId }.switchIncremental()
+
+ override val subscriptions: State<Collection<SubscriptionModel>> =
+ activeRepo.flatMap { it.subscriptions }
+
+ override val activeMobileDataSubscriptionId: State<Int?> =
+ activeRepo.flatMap { it.activeMobileDataSubscriptionId }
+
+ override val activeMobileDataRepository: State<MobileConnectionRepositoryKairos?> =
+ activeRepo.flatMap { it.activeMobileDataRepository }
+
+ override val activeSubChangedInGroupEvent: Events<Unit> =
+ activeRepo.map { it.activeSubChangedInGroupEvent }.switchEvents()
+
+ override val defaultDataSubRatConfig: State<MobileMappings.Config> =
+ activeRepo.flatMap { it.defaultDataSubRatConfig }
+
+ override val defaultMobileIconMapping: State<Map<String, SignalIcon.MobileIconGroup>> =
+ activeRepo.flatMap { it.defaultMobileIconMapping }
+
+ override val defaultMobileIconGroup: State<SignalIcon.MobileIconGroup> =
+ activeRepo.flatMap { it.defaultMobileIconGroup }
+
+ override val isDeviceEmergencyCallCapable: State<Boolean> =
+ activeRepo.flatMap { it.isDeviceEmergencyCallCapable }
+
+ override val isAnySimSecure: State<Boolean> = activeRepo.flatMap { it.isAnySimSecure }
+
+ override val defaultDataSubId: State<Int?> = activeRepo.flatMap { it.defaultDataSubId }
+
+ override val mobileIsDefault: State<Boolean> = activeRepo.flatMap { it.mobileIsDefault }
+
+ override val hasCarrierMergedConnection: State<Boolean> =
+ activeRepo.flatMap { it.hasCarrierMergedConnection }
+
+ override val defaultConnectionIsValidated: State<Boolean> =
+ activeRepo.flatMap { it.defaultConnectionIsValidated }
+
+ override val isInEcmMode: State<Boolean> = activeRepo.flatMap { it.isInEcmMode }
+
+ @dagger.Module
+ interface Module {
+ @Binds fun bindImpl(impl: MobileRepositorySwitcherKairos): MobileConnectionsRepositoryKairos
+
+ companion object {
+ @Provides
+ @ElementsIntoSet
+ fun kairosActivatable(
+ impl: Provider<MobileRepositorySwitcherKairos>
+ ): Set<@JvmSuppressWildcards KairosActivatable> =
+ if (Flags.statusBarMobileIconKairos()) setOf(impl.get()) else emptySet()
}
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepositoryKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepositoryKairos.kt
index 712ebdc..a244feb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepositoryKairos.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionRepositoryKairos.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,38 +16,46 @@
package com.android.systemui.statusbar.pipeline.mobile.data.repository.demo
-import android.telephony.CellSignalStrength
import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
import android.telephony.TelephonyManager
+import com.android.settingslib.SignalIcon
+import com.android.systemui.KairosBuilder
+import com.android.systemui.kairos.Events
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.State
+import com.android.systemui.kairos.TransactionScope
+import com.android.systemui.kairos.map
+import com.android.systemui.kairos.mapCheap
+import com.android.systemui.kairos.mergeLeft
+import com.android.systemui.kairos.stateOf
+import com.android.systemui.kairos.util.Either
+import com.android.systemui.kairos.util.Either.First
+import com.android.systemui.kairos.util.Either.Second
+import com.android.systemui.kairos.util.firstOrNull
+import com.android.systemui.kairosBuilder
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.log.table.logDiffsForTable
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
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.model.ResolvedNetworkType.DefaultNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepositoryKairos
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_CARRIER_ID
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_CARRIER_NETWORK_CHANGE
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_CDMA_LEVEL
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_EMERGENCY
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_IS_GSM
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_IS_IN_SERVICE
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_IS_NTN
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_OPERATOR
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_PRIMARY_LEVEL
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_ROAMING
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Companion.COL_SATELLITE_LEVEL
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.Mobile as FakeMobileEvent
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepositoryKairos.Companion.COL_CARRIER_ID
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepositoryKairos.Companion.COL_CARRIER_NETWORK_CHANGE
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepositoryKairos.Companion.COL_CDMA_LEVEL
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepositoryKairos.Companion.COL_IS_IN_SERVICE
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepositoryKairos.Companion.COL_IS_NTN
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepositoryKairos.Companion.COL_OPERATOR
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepositoryKairos.Companion.COL_PRIMARY_LEVEL
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepositoryKairos.Companion.COL_ROAMING
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepositoryKairos.Companion.COL_SATELLITE_LEVEL
import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
-import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel
-import kotlinx.coroutines.CoroutineScope
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel.CarrierMerged as FakeCarrierMergedEvent
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
/**
* Demo version of [MobileConnectionRepository]. Note that this class shares all of its flows using
@@ -55,241 +63,206 @@
* [MutableStateFlow] while still logging all of the inputs in the same manor as the production
* repos.
*/
+@ExperimentalKairosApi
class DemoMobileConnectionRepositoryKairos(
override val subId: Int,
override val tableLogBuffer: TableLogBuffer,
- val scope: CoroutineScope,
-) : MobileConnectionRepository, MobileConnectionRepositoryKairos {
- private val _carrierId = MutableStateFlow(INVALID_SUBSCRIPTION_ID)
- override val carrierId =
- _carrierId
- .logDiffsForTable(
- tableLogBuffer,
- columnName = COL_CARRIER_ID,
- initialValue = _carrierId.value,
+ mobileEvents: Events<FakeMobileEvent>,
+ carrierMergedResetEvents: Events<Any?>,
+ wifiEvents: Events<FakeCarrierMergedEvent>,
+ private val mobileMappingsReverseLookup: State<Map<SignalIcon.MobileIconGroup, String>>,
+) : MobileConnectionRepositoryKairos, KairosBuilder by kairosBuilder() {
+
+ private val initialState =
+ FakeMobileEvent(
+ level = null,
+ dataType = null,
+ subId = subId,
+ carrierId = null,
+ activity = null,
+ carrierNetworkChange = false,
+ roaming = false,
+ name = DEMO_CARRIER_NAME,
+ )
+
+ private val lastMobileEvent: State<FakeMobileEvent> = buildState {
+ mobileEvents.holdState(initialState)
+ }
+
+ private val lastEvent: State<Either<FakeMobileEvent, FakeCarrierMergedEvent>> = buildState {
+ mergeLeft(
+ mobileEvents.mapCheap { First(it) },
+ wifiEvents.mapCheap { Second(it) },
+ carrierMergedResetEvents.mapCheap { First(lastMobileEvent.sample()) },
)
- .stateIn(scope, SharingStarted.WhileSubscribed(), _carrierId.value)
+ .holdState(First(initialState))
+ }
- private val _inflateSignalStrength: MutableStateFlow<Boolean> = MutableStateFlow(false)
- override val inflateSignalStrength =
- _inflateSignalStrength
- .logDiffsForTable(
- tableLogBuffer,
- columnName = "inflate",
- initialValue = _inflateSignalStrength.value,
- )
- .stateIn(scope, SharingStarted.WhileSubscribed(), _inflateSignalStrength.value)
-
- // I don't see a reason why we would turn the config off for demo mode.
- override val allowNetworkSliceIndicator = MutableStateFlow(true)
-
- private val _isEmergencyOnly = MutableStateFlow(false)
- override val isEmergencyOnly =
- _isEmergencyOnly
- .logDiffsForTable(
- tableLogBuffer,
- columnName = COL_EMERGENCY,
- initialValue = _isEmergencyOnly.value,
- )
- .stateIn(scope, SharingStarted.WhileSubscribed(), _isEmergencyOnly.value)
-
- private val _isRoaming = MutableStateFlow(false)
- override val isRoaming =
- _isRoaming
- .logDiffsForTable(
- tableLogBuffer,
- columnName = COL_ROAMING,
- initialValue = _isRoaming.value,
- )
- .stateIn(scope, SharingStarted.WhileSubscribed(), _isRoaming.value)
-
- private val _operatorAlphaShort: MutableStateFlow<String?> = MutableStateFlow(null)
- override val operatorAlphaShort =
- _operatorAlphaShort
- .logDiffsForTable(
- tableLogBuffer,
- columnName = COL_OPERATOR,
- initialValue = _operatorAlphaShort.value,
- )
- .stateIn(scope, SharingStarted.WhileSubscribed(), _operatorAlphaShort.value)
-
- private val _isInService = MutableStateFlow(false)
- override val isInService =
- _isInService
- .logDiffsForTable(
- tableLogBuffer,
- columnName = COL_IS_IN_SERVICE,
- initialValue = _isInService.value,
- )
- .stateIn(scope, SharingStarted.WhileSubscribed(), _isInService.value)
-
- private val _isNonTerrestrial = MutableStateFlow(false)
- override val isNonTerrestrial =
- _isNonTerrestrial
- .logDiffsForTable(
- tableLogBuffer,
- columnName = COL_IS_NTN,
- initialValue = _isNonTerrestrial.value,
- )
- .stateIn(scope, SharingStarted.WhileSubscribed(), _isNonTerrestrial.value)
-
- private val _isGsm = MutableStateFlow(false)
- override val isGsm =
- _isGsm
- .logDiffsForTable(tableLogBuffer, columnName = COL_IS_GSM, initialValue = _isGsm.value)
- .stateIn(scope, SharingStarted.WhileSubscribed(), _isGsm.value)
-
- private val _cdmaLevel = MutableStateFlow(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN)
- override val cdmaLevel =
- _cdmaLevel
- .logDiffsForTable(
- tableLogBuffer,
- columnName = COL_CDMA_LEVEL,
- initialValue = _cdmaLevel.value,
- )
- .stateIn(scope, SharingStarted.WhileSubscribed(), _cdmaLevel.value)
-
- private val _primaryLevel = MutableStateFlow(CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN)
- override val primaryLevel =
- _primaryLevel
- .logDiffsForTable(
- tableLogBuffer,
- columnName = COL_PRIMARY_LEVEL,
- initialValue = _primaryLevel.value,
- )
- .stateIn(scope, SharingStarted.WhileSubscribed(), _primaryLevel.value)
-
- private val _satelliteLevel = MutableStateFlow(0)
- override val satelliteLevel: StateFlow<Int> =
- _satelliteLevel
- .logDiffsForTable(
- tableLogBuffer,
- columnName = COL_SATELLITE_LEVEL,
- initialValue = _satelliteLevel.value,
- )
- .stateIn(scope, SharingStarted.WhileSubscribed(), _satelliteLevel.value)
-
- private val _dataConnectionState = MutableStateFlow(DataConnectionState.Disconnected)
- override val dataConnectionState =
- _dataConnectionState
- .logDiffsForTable(tableLogBuffer, initialValue = _dataConnectionState.value)
- .stateIn(scope, SharingStarted.WhileSubscribed(), _dataConnectionState.value)
-
- private val _dataActivityDirection =
- MutableStateFlow(DataActivityModel(hasActivityIn = false, hasActivityOut = false))
- override val dataActivityDirection =
- _dataActivityDirection
- .logDiffsForTable(tableLogBuffer, initialValue = _dataActivityDirection.value)
- .stateIn(scope, SharingStarted.WhileSubscribed(), _dataActivityDirection.value)
-
- private val _carrierNetworkChangeActive = MutableStateFlow(false)
- override val carrierNetworkChangeActive =
- _carrierNetworkChangeActive
- .logDiffsForTable(
- tableLogBuffer,
- columnName = COL_CARRIER_NETWORK_CHANGE,
- initialValue = _carrierNetworkChangeActive.value,
- )
- .stateIn(scope, SharingStarted.WhileSubscribed(), _carrierNetworkChangeActive.value)
-
- private val _resolvedNetworkType: MutableStateFlow<ResolvedNetworkType> =
- MutableStateFlow(ResolvedNetworkType.UnknownNetworkType)
- override val resolvedNetworkType =
- _resolvedNetworkType
- .logDiffsForTable(tableLogBuffer, initialValue = _resolvedNetworkType.value)
- .stateIn(scope, SharingStarted.WhileSubscribed(), _resolvedNetworkType.value)
-
- override val numberOfLevels =
- _inflateSignalStrength
- .map { shouldInflate ->
- if (shouldInflate) {
- DEFAULT_NUM_LEVELS + 1
- } else {
- DEFAULT_NUM_LEVELS
+ override val carrierId: State<Int> =
+ lastEvent
+ .map { it.firstOrNull()?.carrierId ?: INVALID_SUBSCRIPTION_ID }
+ .also {
+ onActivated {
+ logDiffsForTable(
+ intState = it,
+ tableLogBuffer = tableLogBuffer,
+ columnName = COL_CARRIER_ID,
+ )
}
}
- .stateIn(scope, SharingStarted.WhileSubscribed(), DEFAULT_NUM_LEVELS)
- override val dataEnabled = MutableStateFlow(true)
-
- override val cdmaRoaming = MutableStateFlow(false)
-
- override val networkName = MutableStateFlow(NetworkNameModel.IntentDerived(DEMO_CARRIER_NAME))
-
- override val carrierName =
- MutableStateFlow(NetworkNameModel.SubscriptionDerived(DEMO_CARRIER_NAME))
-
- override val isAllowedDuringAirplaneMode = MutableStateFlow(false)
-
- override val hasPrioritizedNetworkCapabilities = MutableStateFlow(false)
-
- override suspend fun isInEcmMode(): Boolean = false
-
- /**
- * Process a new demo mobile event. Note that [resolvedNetworkType] must be passed in separately
- * from the event, due to the requirement to reverse the mobile mappings lookup in the top-level
- * repository.
- */
- fun processDemoMobileEvent(
- event: FakeNetworkEventModel.Mobile,
- resolvedNetworkType: ResolvedNetworkType,
- ) {
- // This is always true here, because we split out disabled states at the data-source level
- dataEnabled.value = true
- networkName.value = NetworkNameModel.IntentDerived(event.name)
- carrierName.value = NetworkNameModel.SubscriptionDerived("${event.name} ${event.subId}")
-
- _carrierId.value = event.carrierId ?: INVALID_SUBSCRIPTION_ID
-
- _inflateSignalStrength.value = event.inflateStrength
-
- cdmaRoaming.value = event.roaming
- _isRoaming.value = event.roaming
- // TODO(b/261029387): not yet supported
- _isEmergencyOnly.value = false
- _operatorAlphaShort.value = event.name
- _isInService.value = (event.level ?: 0) > 0
- // TODO(b/261029387): not yet supported
- _isGsm.value = false
- _cdmaLevel.value = event.level ?: 0
- _primaryLevel.value = event.level ?: 0
- // TODO(b/261029387): not yet supported
- _dataConnectionState.value = DataConnectionState.Connected
- _dataActivityDirection.value =
- (event.activity ?: TelephonyManager.DATA_ACTIVITY_NONE).toMobileDataActivityModel()
- _carrierNetworkChangeActive.value = event.carrierNetworkChange
- _resolvedNetworkType.value = resolvedNetworkType
- _isNonTerrestrial.value = event.ntn
-
- isAllowedDuringAirplaneMode.value = false
- hasPrioritizedNetworkCapabilities.value = event.slice
+ override val inflateSignalStrength: State<Boolean> = buildState {
+ mobileEvents
+ .map { ev -> ev.inflateStrength }
+ .holdState(false)
+ .also { logDiffsForTable(it, tableLogBuffer, "", columnName = "inflate") }
}
- fun processCarrierMergedEvent(event: FakeWifiEventModel.CarrierMerged) {
- // This is always true here, because we split out disabled states at the data-source level
- dataEnabled.value = true
- networkName.value = NetworkNameModel.IntentDerived(CARRIER_MERGED_NAME)
- carrierName.value = NetworkNameModel.SubscriptionDerived(CARRIER_MERGED_NAME)
- // TODO(b/276943904): is carrierId a thing with carrier merged networks?
- _carrierId.value = INVALID_SUBSCRIPTION_ID
- cdmaRoaming.value = false
- _primaryLevel.value = event.level
- _cdmaLevel.value = event.level
- _dataActivityDirection.value = event.activity.toMobileDataActivityModel()
+ // I don't see a reason why we would turn the config off for demo mode.
+ override val allowNetworkSliceIndicator: State<Boolean> = stateOf(true)
- // These fields are always the same for carrier-merged networks
- _resolvedNetworkType.value = ResolvedNetworkType.CarrierMergedNetworkType
- _dataConnectionState.value = DataConnectionState.Connected
- _isRoaming.value = false
- _isEmergencyOnly.value = false
- _operatorAlphaShort.value = null
- _isInService.value = true
- _isGsm.value = false
- _carrierNetworkChangeActive.value = false
- isAllowedDuringAirplaneMode.value = true
- hasPrioritizedNetworkCapabilities.value = false
+ // TODO(b/261029387): not yet supported
+ override val isEmergencyOnly: State<Boolean> = stateOf(false)
+
+ override val isRoaming: State<Boolean> =
+ lastEvent
+ .map { it.firstOrNull()?.roaming ?: false }
+ .also { onActivated { logDiffsForTable(it, tableLogBuffer, columnName = COL_ROAMING) } }
+
+ override val operatorAlphaShort: State<String?> =
+ lastEvent
+ .map { it.firstOrNull()?.name }
+ .also {
+ onActivated { logDiffsForTable(it, tableLogBuffer, columnName = COL_OPERATOR) }
+ }
+
+ override val isInService: State<Boolean> =
+ lastEvent
+ .map {
+ when (it) {
+ is First -> it.value.level?.let { level -> level > 0 } ?: false
+ is Second -> true
+ }
+ }
+ .also {
+ onActivated { logDiffsForTable(it, tableLogBuffer, columnName = COL_IS_IN_SERVICE) }
+ }
+
+ override val isNonTerrestrial: State<Boolean> = buildState {
+ mobileEvents
+ .map { it.ntn }
+ .holdState(false)
+ .also { logDiffsForTable(it, tableLogBuffer, columnName = COL_IS_NTN) }
}
+ // TODO(b/261029387): not yet supported
+ override val isGsm: State<Boolean> = stateOf(false)
+
+ override val cdmaLevel: State<Int> =
+ lastEvent
+ .map {
+ when (it) {
+ is First -> it.value.level ?: 0
+ is Second -> it.value.level
+ }
+ }
+ .also {
+ onActivated { logDiffsForTable(it, tableLogBuffer, columnName = COL_CDMA_LEVEL) }
+ }
+
+ override val primaryLevel: State<Int> =
+ lastEvent
+ .map {
+ when (it) {
+ is First -> it.value.level ?: 0
+ is Second -> it.value.level
+ }
+ }
+ .also {
+ onActivated { logDiffsForTable(it, tableLogBuffer, columnName = COL_PRIMARY_LEVEL) }
+ }
+
+ override val satelliteLevel: State<Int> =
+ stateOf(0).also {
+ onActivated { logDiffsForTable(it, tableLogBuffer, columnName = COL_SATELLITE_LEVEL) }
+ }
+
+ // TODO(b/261029387): not yet supported
+ override val dataConnectionState: State<DataConnectionState> =
+ buildState {
+ mergeLeft(mobileEvents, wifiEvents)
+ .map { DataConnectionState.Connected }
+ .holdState(DataConnectionState.Disconnected)
+ }
+ .also {
+ onActivated {
+ logDiffsForTable(diffableState = it, tableLogBuffer = tableLogBuffer)
+ }
+ }
+
+ override val dataActivityDirection: State<DataActivityModel> =
+ lastEvent
+ .map {
+ val activity =
+ when (it) {
+ is First -> it.value.activity ?: TelephonyManager.DATA_ACTIVITY_NONE
+ is Second -> it.value.activity
+ }
+ activity.toMobileDataActivityModel()
+ }
+ .also { onActivated { logDiffsForTable(it, tableLogBuffer, columnPrefix = "") } }
+
+ override val carrierNetworkChangeActive: State<Boolean> =
+ lastEvent
+ .map { it.firstOrNull()?.carrierNetworkChange ?: false }
+ .also {
+ onActivated {
+ logDiffsForTable(it, tableLogBuffer, columnName = COL_CARRIER_NETWORK_CHANGE)
+ }
+ }
+
+ override val resolvedNetworkType: State<ResolvedNetworkType> = buildState {
+ lastEvent
+ .mapTransactionally {
+ it.firstOrNull()?.dataType?.let { resolvedNetworkTypeForIconGroup(it) }
+ ?: ResolvedNetworkType.CarrierMergedNetworkType
+ }
+ .also { logDiffsForTable(it, tableLogBuffer, columnPrefix = "") }
+ }
+
+ override val numberOfLevels: State<Int> =
+ inflateSignalStrength.map { shouldInflate ->
+ if (shouldInflate) DEFAULT_NUM_LEVELS + 1 else DEFAULT_NUM_LEVELS
+ }
+
+ override val dataEnabled: State<Boolean> = stateOf(true)
+
+ override val cdmaRoaming: State<Boolean> = lastEvent.map { it.firstOrNull()?.roaming ?: false }
+
+ override val networkName: State<NetworkNameModel.IntentDerived> =
+ lastEvent.map {
+ NetworkNameModel.IntentDerived(it.firstOrNull()?.name ?: CARRIER_MERGED_NAME)
+ }
+
+ override val carrierName: State<NetworkNameModel.SubscriptionDerived> =
+ lastEvent.map {
+ NetworkNameModel.SubscriptionDerived(
+ it.firstOrNull()?.let { event -> "${event.name} ${event.subId}" }
+ ?: CARRIER_MERGED_NAME
+ )
+ }
+
+ override val isAllowedDuringAirplaneMode: State<Boolean> = lastEvent.map { it is Second }
+
+ override val hasPrioritizedNetworkCapabilities: State<Boolean> =
+ lastEvent.map { it.firstOrNull()?.slice ?: false }
+
+ override val isInEcmMode: State<Boolean> = stateOf(false)
+
+ private fun TransactionScope.resolvedNetworkTypeForIconGroup(
+ iconGroup: SignalIcon.MobileIconGroup?
+ ) = DefaultNetworkType(mobileMappingsReverseLookup.sample()[iconGroup] ?: "dis")
+
companion object {
private const val DEMO_CARRIER_NAME = "Demo Carrier"
private const val CARRIER_MERGED_NAME = "Carrier Merged Network"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryKairos.kt
index dee59bd..925ee54 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryKairos.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryKairos.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,17 +20,35 @@
import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
import android.telephony.SubscriptionManager.PROFILE_CLASS_UNSET
import android.util.Log
-import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.settingslib.SignalIcon
import com.android.settingslib.mobile.MobileMappings
import com.android.settingslib.mobile.TelephonyIcons
-import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.KairosBuilder
+import com.android.systemui.activated
+import com.android.systemui.kairos.BuildScope
+import com.android.systemui.kairos.Events
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.GroupedEvents
+import com.android.systemui.kairos.Incremental
+import com.android.systemui.kairos.State
+import com.android.systemui.kairos.TransactionScope
+import com.android.systemui.kairos.asIncremental
+import com.android.systemui.kairos.buildSpec
+import com.android.systemui.kairos.combine
+import com.android.systemui.kairos.emptyEvents
+import com.android.systemui.kairos.filter
+import com.android.systemui.kairos.filterIsInstance
+import com.android.systemui.kairos.groupBy
+import com.android.systemui.kairos.groupByKey
+import com.android.systemui.kairos.map
+import com.android.systemui.kairos.mapCheap
+import com.android.systemui.kairos.mapNotNull
+import com.android.systemui.kairos.mapValues
+import com.android.systemui.kairos.mergeLeft
+import com.android.systemui.kairos.stateOf
+import com.android.systemui.kairosBuilder
import com.android.systemui.log.table.TableLogBufferFactory
-import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType
-import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType
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
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepositoryKairos
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.Mobile
@@ -38,111 +56,149 @@
import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Factory.Companion.MOBILE_CONNECTION_BUFFER_SIZE
import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource
import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model.FakeWifiEventModel
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.mapLatest
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.stateIn
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
/** This repository vends out data based on demo mode commands */
+@ExperimentalKairosApi
class DemoMobileConnectionsRepositoryKairos
-@Inject
+@AssistedInject
constructor(
- private val mobileDataSource: DemoModeMobileConnectionDataSource,
+ mobileDataSource: DemoModeMobileConnectionDataSourceKairos,
private val wifiDataSource: DemoModeWifiDataSource,
- @Background private val scope: CoroutineScope,
context: Context,
private val logFactory: TableLogBufferFactory,
-) : MobileConnectionsRepository, MobileConnectionsRepositoryKairos {
+) : MobileConnectionsRepositoryKairos, KairosBuilder by kairosBuilder() {
- private var mobileDemoCommandJob: Job? = null
- private var wifiDemoCommandJob: Job? = null
-
- private var carrierMergedSubId: Int? = null
-
- private var connectionRepoCache = mutableMapOf<Int, CacheContainer>()
- private val subscriptionInfoCache = mutableMapOf<Int, SubscriptionModel>()
- val demoModeFinishedEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
-
- private val _subscriptions = MutableStateFlow<List<SubscriptionModel>>(listOf())
- override val subscriptions =
- _subscriptions
- .onEach { infos -> dropUnusedReposFromCache(infos) }
- .stateIn(scope, SharingStarted.WhileSubscribed(), _subscriptions.value)
-
- private fun dropUnusedReposFromCache(newInfos: List<SubscriptionModel>) {
- // Remove any connection repository from the cache that isn't in the new set of IDs. They
- // will get garbage collected once their subscribers go away
- val currentValidSubscriptionIds = newInfos.map { it.subscriptionId }
-
- connectionRepoCache =
- connectionRepoCache
- .filter { currentValidSubscriptionIds.contains(it.key) }
- .toMutableMap()
+ @AssistedFactory
+ fun interface Factory {
+ fun create(): DemoMobileConnectionsRepositoryKairos
}
- private fun maybeCreateSubscription(subId: Int) {
- if (!subscriptionInfoCache.containsKey(subId)) {
- SubscriptionModel(
+ private val wifiEvents: Events<FakeWifiEventModel?> = buildEvents {
+ wifiDataSource.wifiEvents.toEvents()
+ }
+
+ private val mobileEventsWithSubId: Events<Pair<Int, FakeNetworkEventModel>> =
+ mobileDataSource.mobileEvents.mapNotNull { event ->
+ event?.let { (event.subId ?: lastSeenSubId.sample())?.let { it to event } }
+ }
+
+ private val mobileEventsBySubId: GroupedEvents<Int, FakeNetworkEventModel> =
+ mobileEventsWithSubId.map { mapOf(it) }.groupByKey()
+
+ private val carrierMergedEvents: Events<FakeWifiEventModel.CarrierMerged> =
+ wifiEvents.filterIsInstance<FakeWifiEventModel.CarrierMerged>()
+
+ private val wifiEventsBySubId: GroupedEvents<Int, FakeWifiEventModel.CarrierMerged> =
+ carrierMergedEvents.groupBy { it.subscriptionId }
+
+ private val lastSeenSubId: State<Int?> = buildState {
+ mergeLeft(
+ mobileEventsWithSubId.mapCheap { it.first },
+ carrierMergedEvents.mapCheap { it.subscriptionId },
+ )
+ .holdState(null)
+ }
+
+ private val activeCarrierMergedSubscription: State<Int?> = buildState {
+ mergeLeft(
+ carrierMergedEvents.mapCheap { it.subscriptionId },
+ wifiEvents
+ .filter {
+ it is FakeWifiEventModel.Wifi || it is FakeWifiEventModel.WifiDisabled
+ }
+ .map { null },
+ )
+ .holdState(null)
+ }
+
+ private val activeMobileSubscriptions: State<Set<Int>> = buildState {
+ mobileDataSource.mobileEvents
+ .mapNotNull { event ->
+ when (event) {
+ null -> null
+ is Mobile -> event.subId?.let { subId -> { subs: Set<Int> -> subs + subId } }
+ is MobileDisabled ->
+ (event.subId ?: maybeGetOnlySubIdForRemoval())?.let { subId ->
+ { subs: Set<Int> -> subs - subId }
+ }
+ }
+ }
+ .foldState(emptySet()) { f, s -> f(s) }
+ }
+
+ private val subscriptionIds: State<Set<Int>> =
+ combine(activeMobileSubscriptions, activeCarrierMergedSubscription) { mobile, carrierMerged
+ ->
+ carrierMerged?.let { mobile + carrierMerged } ?: mobile
+ }
+
+ private val subscriptionsById: State<Map<Int, SubscriptionModel>> =
+ subscriptionIds.map { subs ->
+ subs.associateWith { subId ->
+ SubscriptionModel(
subscriptionId = subId,
isOpportunistic = false,
carrierName = DEFAULT_CARRIER_NAME,
profileClass = PROFILE_CLASS_UNSET,
)
- .also { subscriptionInfoCache[subId] = it }
+ }
+ }
- _subscriptions.value = subscriptionInfoCache.values.toList()
+ override val subscriptions: State<Collection<SubscriptionModel>> =
+ subscriptionsById.map { it.values }
+
+ private fun TransactionScope.maybeGetOnlySubIdForRemoval(): Int? {
+ val subIds = activeMobileSubscriptions.sample()
+ return if (subIds.size == 1) {
+ subIds.first()
+ } else {
+ Log.d(
+ TAG,
+ "processDisabledMobileState: Unable to infer subscription to " +
+ "disable. Specify subId using '-e slot <subId>'. " +
+ "Known subIds: [${subIds.joinToString(",")}]",
+ )
+ null
}
}
- // TODO(b/261029387): add a command for this value
- override val activeMobileDataSubscriptionId =
- subscriptions
- .mapLatest { infos ->
- // For now, active is just the first in the list
- infos.firstOrNull()?.subscriptionId ?: INVALID_SUBSCRIPTION_ID
- }
- .stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- subscriptions.value.firstOrNull()?.subscriptionId ?: INVALID_SUBSCRIPTION_ID,
- )
+ private val reposBySubId: Incremental<Int, DemoMobileConnectionRepositoryKairos> =
+ buildIncremental {
+ subscriptionsById
+ .asIncremental()
+ .mapValues { (id, _) -> buildSpec { newRepo(id) } }
+ .applyLatestSpecForKey()
+ }
- override val activeMobileDataRepository: StateFlow<MobileConnectionRepository?> =
- activeMobileDataSubscriptionId
- .map { getRepoForSubId(it) }
- .stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- getRepoForSubId(activeMobileDataSubscriptionId.value),
- )
+ // TODO(b/261029387): add a command for this value
+ override val activeMobileDataSubscriptionId: State<Int> =
+ // For now, active is just the first in the list
+ subscriptions.map { infos ->
+ infos.firstOrNull()?.subscriptionId ?: INVALID_SUBSCRIPTION_ID
+ }
+
+ override val activeMobileDataRepository: State<DemoMobileConnectionRepositoryKairos?> =
+ combine(activeMobileDataSubscriptionId, reposBySubId) { subId, repoMap -> repoMap[subId] }
// TODO(b/261029387): consider adding a demo command for this
- override val activeSubChangedInGroupEvent: Flow<Unit> = flowOf()
+ override val activeSubChangedInGroupEvent: Events<Unit> = emptyEvents
/** Demo mode doesn't currently support modifications to the mobile mappings */
- override val defaultDataSubRatConfig =
- MutableStateFlow(MobileMappings.Config.readConfig(context))
+ override val defaultDataSubRatConfig: State<MobileMappings.Config> =
+ stateOf(MobileMappings.Config.readConfig(context))
- override val defaultMobileIconGroup = flowOf(TelephonyIcons.THREE_G)
+ override val defaultMobileIconGroup: State<SignalIcon.MobileIconGroup> =
+ stateOf(TelephonyIcons.THREE_G)
// TODO(b/339023069): demo command for device-based emergency calls state
- override val isDeviceEmergencyCallCapable: StateFlow<Boolean> = MutableStateFlow(false)
+ override val isDeviceEmergencyCallCapable: State<Boolean> = stateOf(false)
- override val isAnySimSecure: Flow<Boolean> = flowOf(getIsAnySimSecure())
+ override val isAnySimSecure: State<Boolean> = stateOf(false)
- override fun getIsAnySimSecure(): Boolean = false
-
- override val defaultMobileIconMapping = MutableStateFlow(TelephonyIcons.ICON_NAME_TO_ICON)
+ override val defaultMobileIconMapping: State<Map<String, SignalIcon.MobileIconGroup>> =
+ stateOf(TelephonyIcons.ICON_NAME_TO_ICON)
/**
* In order to maintain compatibility with the old demo mode shell command API, reverse the
@@ -153,185 +209,47 @@
* Note: collisions don't matter here, because the data source (the command line) only cares
* about the resulting icon, not the underlying network type.
*/
- private val mobileMappingsReverseLookup: StateFlow<Map<SignalIcon.MobileIconGroup, String>> =
- defaultMobileIconMapping
- .mapLatest { networkToIconMap -> networkToIconMap.reverse() }
- .stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- defaultMobileIconMapping.value.reverse(),
- )
+ private val mobileMappingsReverseLookup: State<Map<SignalIcon.MobileIconGroup, String>> =
+ defaultMobileIconMapping.map { networkToIconMap -> networkToIconMap.reverse() }
- private fun <K, V> Map<K, V>.reverse() = entries.associateBy({ it.value }) { it.key }
+ private fun <K, V> Map<K, V>.reverse() = entries.associate { (k, v) -> v to k }
// TODO(b/261029387): add a command for this value
- override val defaultDataSubId: MutableStateFlow<Int?> = MutableStateFlow(null)
+ override val defaultDataSubId: State<Int?> = stateOf(null)
// TODO(b/261029387): not yet supported
- override val mobileIsDefault: StateFlow<Boolean> = MutableStateFlow(true)
+ override val mobileIsDefault: State<Boolean> = stateOf(true)
// TODO(b/261029387): not yet supported
- override val hasCarrierMergedConnection = MutableStateFlow(false)
+ override val hasCarrierMergedConnection: State<Boolean> = stateOf(false)
// TODO(b/261029387): not yet supported
- override val defaultConnectionIsValidated: StateFlow<Boolean> = MutableStateFlow(true)
+ override val defaultConnectionIsValidated: State<Boolean> = stateOf(true)
- override fun getRepoForSubId(subId: Int): DemoMobileConnectionRepository {
- val current = connectionRepoCache[subId]?.repo
- if (current != null) {
- return current
- }
+ override val isInEcmMode: State<Boolean> = stateOf(false)
- val new = createDemoMobileConnectionRepo(subId)
- connectionRepoCache[subId] = new
- return new.repo
- }
+ override val mobileConnectionsBySubId: Incremental<Int, DemoMobileConnectionRepositoryKairos>
+ get() = reposBySubId
- private fun createDemoMobileConnectionRepo(subId: Int): CacheContainer {
- val tableLogBuffer =
- logFactory.getOrCreate("DemoMobileConnectionLog[$subId]", MOBILE_CONNECTION_BUFFER_SIZE)
-
- val repo = DemoMobileConnectionRepository(subId, tableLogBuffer, scope)
- return CacheContainer(repo, lastMobileState = null)
- }
-
- fun startProcessingCommands() {
- mobileDemoCommandJob =
- scope.launch {
- mobileDataSource.mobileEvents.filterNotNull().collect { event ->
- processMobileEvent(event)
- }
- }
- wifiDemoCommandJob =
- scope.launch {
- wifiDataSource.wifiEvents.filterNotNull().collect { event ->
- processWifiEvent(event)
- }
- }
- }
-
- fun stopProcessingCommands() {
- mobileDemoCommandJob?.cancel()
- wifiDemoCommandJob?.cancel()
- _subscriptions.value = listOf()
- connectionRepoCache.clear()
- subscriptionInfoCache.clear()
- }
-
- override suspend fun isInEcmMode(): Boolean = false
-
- private fun processMobileEvent(event: FakeNetworkEventModel) {
- when (event) {
- is Mobile -> {
- processEnabledMobileState(event)
- }
- is MobileDisabled -> {
- maybeRemoveSubscription(event.subId)
- }
- }
- }
-
- private fun processWifiEvent(event: FakeWifiEventModel) {
- when (event) {
- is FakeWifiEventModel.WifiDisabled -> disableCarrierMerged()
- is FakeWifiEventModel.Wifi -> disableCarrierMerged()
- is FakeWifiEventModel.CarrierMerged -> processCarrierMergedWifiState(event)
- }
- }
-
- private fun processEnabledMobileState(event: Mobile) {
- // get or create the connection repo, and set its values
- val subId = event.subId ?: DEFAULT_SUB_ID
- maybeCreateSubscription(subId)
-
- val connection = getRepoForSubId(subId)
- connectionRepoCache[subId]?.lastMobileState = event
-
- // TODO(b/261029387): until we have a command, use the most recent subId
- defaultDataSubId.value = subId
-
- connection.processDemoMobileEvent(event, event.dataType.toResolvedNetworkType())
- }
-
- private fun processCarrierMergedWifiState(event: FakeWifiEventModel.CarrierMerged) {
- // The new carrier merged connection is for a different sub ID, so disable carrier merged
- // for the current (now old) sub
- if (carrierMergedSubId != event.subscriptionId) {
- disableCarrierMerged()
- }
-
- // get or create the connection repo, and set its values
- val subId = event.subscriptionId
- maybeCreateSubscription(subId)
- carrierMergedSubId = subId
-
- // TODO(b/261029387): until we have a command, use the most recent subId
- defaultDataSubId.value = subId
-
- val connection = getRepoForSubId(subId)
- connection.processCarrierMergedEvent(event)
- }
-
- private fun maybeRemoveSubscription(subId: Int?) {
- if (_subscriptions.value.isEmpty()) {
- // Nothing to do here
- return
- }
-
- val finalSubId =
- subId
- ?: run {
- // For sake of usability, we can allow for no subId arg if there is only one
- // subscription
- if (_subscriptions.value.size > 1) {
- Log.d(
- TAG,
- "processDisabledMobileState: Unable to infer subscription to " +
- "disable. Specify subId using '-e slot <subId>'" +
- "Known subIds: [${subIdsString()}]",
- )
- return
- }
-
- // Use the only existing subscription as our arg, since there is only one
- _subscriptions.value[0].subscriptionId
- }
-
- removeSubscription(finalSubId)
- }
-
- private fun disableCarrierMerged() {
- val currentCarrierMergedSubId = carrierMergedSubId ?: return
-
- // If this sub ID was previously not carrier merged, we should reset it to its previous
- // connection.
- val lastMobileState = connectionRepoCache[carrierMergedSubId]?.lastMobileState
- if (lastMobileState != null) {
- processEnabledMobileState(lastMobileState)
- } else {
- // Otherwise, just remove the subscription entirely
- removeSubscription(currentCarrierMergedSubId)
- }
- }
-
- private fun removeSubscription(subId: Int) {
- val currentSubscriptions = _subscriptions.value
- subscriptionInfoCache.remove(subId)
- _subscriptions.value = currentSubscriptions.filter { it.subscriptionId != subId }
- }
-
- private fun subIdsString(): String =
- _subscriptions.value.joinToString(",") { it.subscriptionId.toString() }
-
- private fun SignalIcon.MobileIconGroup?.toResolvedNetworkType(): ResolvedNetworkType {
- val key = mobileMappingsReverseLookup.value[this] ?: "dis"
- return DefaultNetworkType(key)
+ private fun BuildScope.newRepo(subId: Int) = activated {
+ DemoMobileConnectionRepositoryKairos(
+ subId = subId,
+ tableLogBuffer =
+ logFactory.getOrCreate(
+ "DemoMobileConnectionLog[$subId]",
+ MOBILE_CONNECTION_BUFFER_SIZE,
+ ),
+ mobileEvents = mobileEventsBySubId[subId].filterIsInstance(),
+ carrierMergedResetEvents =
+ wifiEvents.mapNotNull { it?.takeIf { it !is FakeWifiEventModel.CarrierMerged } },
+ wifiEvents = wifiEventsBySubId[subId],
+ mobileMappingsReverseLookup = mobileMappingsReverseLookup,
+ )
}
companion object {
private const val TAG = "DemoMobileConnectionsRepo"
- private const val DEFAULT_SUB_ID = 1
private const val DEFAULT_CARRIER_NAME = "demo carrier"
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSourceKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSourceKairos.kt
index b379384..f329383 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSourceKairos.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoModeMobileConnectionDataSourceKairos.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2025 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -24,33 +24,51 @@
import android.telephony.TelephonyManager.DATA_ACTIVITY_OUT
import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.settingslib.mobile.TelephonyIcons
+import com.android.systemui.Flags
+import com.android.systemui.KairosActivatable
+import com.android.systemui.KairosBuilder
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.demomode.DemoMode.COMMAND_NETWORK
import com.android.systemui.demomode.DemoModeController
+import com.android.systemui.kairos.Events
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairosBuilder
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.Mobile
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel.MobileDisabled
+import dagger.Binds
+import dagger.Provides
+import dagger.multibindings.ElementsIntoSet
import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.SharingStarted
+import javax.inject.Provider
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.shareIn
/**
* Data source that can map from demo mode commands to inputs into the
- * [DemoMobileConnectionsRepository]'s flows
+ * [DemoMobileConnectionsRepositoryKairos]
*/
+@ExperimentalKairosApi
+interface DemoModeMobileConnectionDataSourceKairos {
+ val mobileEvents: Events<FakeNetworkEventModel?>
+}
+
+@ExperimentalKairosApi
@SysUISingleton
-class DemoModeMobileConnectionDataSourceKairos
+class DemoModeMobileConnectionDataSourceKairosImpl
@Inject
-constructor(demoModeController: DemoModeController, @Background scope: CoroutineScope) {
- private val demoCommandStream = demoModeController.demoFlowForCommand(COMMAND_NETWORK)
+constructor(demoModeController: DemoModeController) :
+ KairosBuilder by kairosBuilder(), DemoModeMobileConnectionDataSourceKairos {
+ private val demoCommandStream: Flow<Bundle> =
+ demoModeController.demoFlowForCommand(COMMAND_NETWORK)
// If the args contains "mobile", then all of the args are relevant. It's just the way demo mode
// commands work and it's a little silly
- private val _mobileCommands = demoCommandStream.map { args -> args.toMobileEvent() }
- val mobileEvents = _mobileCommands.shareIn(scope, SharingStarted.WhileSubscribed())
+ private val _mobileCommands: Flow<FakeNetworkEventModel?> =
+ demoCommandStream.map { args -> args.toMobileEvent() }
+ override val mobileEvents: Events<FakeNetworkEventModel?> = buildEvents {
+ _mobileCommands.toEvents()
+ }
private fun Bundle.toMobileEvent(): FakeNetworkEventModel? {
val mobile = getString("mobile") ?: return null
@@ -90,6 +108,23 @@
ntn = ntn,
)
}
+
+ @dagger.Module
+ interface Module {
+ @Binds
+ fun bindImpl(
+ impl: DemoModeMobileConnectionDataSourceKairosImpl
+ ): DemoModeMobileConnectionDataSourceKairos
+
+ companion object {
+ @Provides
+ @ElementsIntoSet
+ fun kairosActivatable(
+ impl: Provider<DemoModeMobileConnectionDataSourceKairosImpl>
+ ): Set<@JvmSuppressWildcards KairosActivatable> =
+ if (Flags.statusBarMobileIconKairos()) setOf(impl.get()) else emptySet()
+ }
+ }
}
private fun String.toDataType(): MobileIconGroup =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt
index 42171d0..54162bb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/model/FakeNetworkEventModel.kt
@@ -25,11 +25,13 @@
* Nullable fields represent optional command line arguments
*/
sealed interface FakeNetworkEventModel {
+ // Null means the default (chosen by the repository)
+ val subId: Int?
+
data class Mobile(
val level: Int?,
val dataType: SignalIcon.MobileIconGroup?,
- // Null means the default (chosen by the repository)
- val subId: Int?,
+ override val subId: Int?,
val carrierId: Int?,
val inflateStrength: Boolean = false,
@DataActivityType val activity: Int?,
@@ -40,8 +42,5 @@
val ntn: Boolean = false,
) : FakeNetworkEventModel
- data class MobileDisabled(
- // Null means the default (chosen by the repository)
- val subId: Int?
- ) : FakeNetworkEventModel
+ data class MobileDisabled(override val subId: Int?) : FakeNetworkEventModel
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryKairos.kt
index 4d80efc..d61d11b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryKairos.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryKairos.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2025 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,29 +20,24 @@
import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
import android.telephony.TelephonyManager
import android.util.Log
+import com.android.systemui.KairosBuilder
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.State
+import com.android.systemui.kairos.combine
+import com.android.systemui.kairos.map
+import com.android.systemui.kairos.stateOf
+import com.android.systemui.kairosBuilder
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
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
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepositoryKairos
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepositoryKairos.Companion.DEFAULT_NUM_LEVELS
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
import javax.inject.Inject
-import kotlin.coroutines.CoroutineContext
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.withContext
/**
* A repository implementation for a carrier merged (aka VCN) network. A carrier merged network is
@@ -54,33 +49,40 @@
* See [MobileConnectionRepositoryImpl] for a repository implementation of a typical mobile
* connection.
*/
+@ExperimentalKairosApi
class CarrierMergedConnectionRepositoryKairos(
override val subId: Int,
override val tableLogBuffer: TableLogBuffer,
private val telephonyManager: TelephonyManager,
- private val bgContext: CoroutineContext,
- @Background private val scope: CoroutineScope,
val wifiRepository: WifiRepository,
-) : MobileConnectionRepository, MobileConnectionRepositoryKairos {
+ override val isInEcmMode: State<Boolean>,
+) : MobileConnectionRepositoryKairos, KairosBuilder by kairosBuilder() {
init {
if (telephonyManager.subscriptionId != subId) {
- throw IllegalStateException(
- "CarrierMergedRepo: TelephonyManager should be created with subId($subId). " +
- "Found ${telephonyManager.subscriptionId} instead."
+ error(
+ """CarrierMergedRepo: TelephonyManager should be created with subId($subId).
+ | Found ${telephonyManager.subscriptionId} instead."""
+ .trimMargin()
)
}
}
+ private val isWifiEnabled: State<Boolean> = buildState {
+ wifiRepository.isWifiEnabled.toState()
+ }
+ private val isWifiDefault: State<Boolean> = buildState {
+ wifiRepository.isWifiDefault.toState()
+ }
+ private val wifiNetwork: State<WifiNetworkModel> = buildState {
+ wifiRepository.wifiNetwork.toState()
+ }
+
/**
* Outputs the carrier merged network to use, or null if we don't have a valid carrier merged
* network.
*/
- private val network: Flow<WifiNetworkModel.CarrierMerged?> =
- combine(
- wifiRepository.isWifiEnabled,
- wifiRepository.isWifiDefault,
- wifiRepository.wifiNetwork,
- ) { isEnabled, isDefault, network ->
+ private val network: State<WifiNetworkModel.CarrierMerged?> =
+ combine(isWifiEnabled, isWifiDefault, wifiNetwork) { isEnabled, isDefault, network ->
when {
!isEnabled -> null
!isDefault -> null
@@ -88,9 +90,9 @@
network.subscriptionId != subId -> {
Log.w(
TAG,
- "Connection repo subId=$subId " +
- "does not equal wifi repo subId=${network.subscriptionId}; " +
- "not showing carrier merged",
+ """Connection repo subId=$subId does not equal wifi repo
+ | subId=${network.subscriptionId}; not showing carrier merged"""
+ .trimMargin(),
)
null
}
@@ -98,101 +100,82 @@
}
}
- override val cdmaRoaming: StateFlow<Boolean> = MutableStateFlow(ROAMING).asStateFlow()
+ override val cdmaRoaming: State<Boolean> = stateOf(ROAMING)
- override val networkName: StateFlow<NetworkNameModel> =
- network
- // The SIM operator name should be the same throughout the lifetime of a subId, **but**
- // it may not be available when this repo is created because it takes time to load. To
- // be safe, we re-fetch it each time the network has changed.
- .map { NetworkNameModel.SimDerived(telephonyManager.simOperatorName) }
- .stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- NetworkNameModel.SimDerived(telephonyManager.simOperatorName),
- )
+ override val networkName: State<NetworkNameModel> =
+ // The SIM operator name should be the same throughout the lifetime of a subId, **but**
+ // it may not be available when this repo is created because it takes time to load. To
+ // be safe, we re-fetch it each time the network has changed.
+ network.map { NetworkNameModel.SimDerived(telephonyManager.simOperatorName) }
- override val carrierName: StateFlow<NetworkNameModel> = networkName
+ override val carrierName: State<NetworkNameModel>
+ get() = networkName
- override val numberOfLevels: StateFlow<Int> =
- wifiRepository.wifiNetwork
- .map {
- if (it is WifiNetworkModel.CarrierMerged) {
- it.numberOfLevels
- } else {
- DEFAULT_NUM_LEVELS
- }
+ override val numberOfLevels: State<Int> =
+ wifiNetwork.map {
+ if (it is WifiNetworkModel.CarrierMerged) {
+ it.numberOfLevels
+ } else {
+ DEFAULT_NUM_LEVELS
}
- .stateIn(scope, SharingStarted.WhileSubscribed(), DEFAULT_NUM_LEVELS)
+ }
- override val primaryLevel =
- network
- .map { it?.level ?: SIGNAL_STRENGTH_NONE_OR_UNKNOWN }
- .stateIn(scope, SharingStarted.WhileSubscribed(), SIGNAL_STRENGTH_NONE_OR_UNKNOWN)
+ override val primaryLevel: State<Int> =
+ network.map { it?.level ?: SIGNAL_STRENGTH_NONE_OR_UNKNOWN }
- override val cdmaLevel =
- network
- .map { it?.level ?: SIGNAL_STRENGTH_NONE_OR_UNKNOWN }
- .stateIn(scope, SharingStarted.WhileSubscribed(), SIGNAL_STRENGTH_NONE_OR_UNKNOWN)
+ override val cdmaLevel: State<Int> =
+ network.map { it?.level ?: SIGNAL_STRENGTH_NONE_OR_UNKNOWN }
- override val dataActivityDirection = wifiRepository.wifiActivity
+ override val dataActivityDirection: State<DataActivityModel> = buildState {
+ wifiRepository.wifiActivity.toState()
+ }
- override val resolvedNetworkType =
- network
- .map {
- if (it != null) {
- ResolvedNetworkType.CarrierMergedNetworkType
- } else {
- ResolvedNetworkType.UnknownNetworkType
- }
+ override val resolvedNetworkType: State<ResolvedNetworkType> =
+ network.map {
+ if (it != null) {
+ ResolvedNetworkType.CarrierMergedNetworkType
+ } else {
+ ResolvedNetworkType.UnknownNetworkType
}
- .stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- ResolvedNetworkType.UnknownNetworkType,
- )
+ }
- override val dataConnectionState =
- network
- .map {
- if (it != null) {
- DataConnectionState.Connected
- } else {
- DataConnectionState.Disconnected
- }
+ override val dataConnectionState: State<DataConnectionState> =
+ network.map {
+ if (it != null) {
+ DataConnectionState.Connected
+ } else {
+ DataConnectionState.Disconnected
}
- .stateIn(scope, SharingStarted.WhileSubscribed(), DataConnectionState.Disconnected)
+ }
- override val isRoaming = MutableStateFlow(false).asStateFlow()
- override val carrierId = MutableStateFlow(INVALID_SUBSCRIPTION_ID).asStateFlow()
- override val inflateSignalStrength = MutableStateFlow(false).asStateFlow()
- override val allowNetworkSliceIndicator = MutableStateFlow(false).asStateFlow()
- override val isEmergencyOnly = MutableStateFlow(false).asStateFlow()
- override val operatorAlphaShort = MutableStateFlow(null).asStateFlow()
- override val isInService = MutableStateFlow(true).asStateFlow()
- override val isNonTerrestrial = MutableStateFlow(false).asStateFlow()
- override val isGsm = MutableStateFlow(false).asStateFlow()
- override val carrierNetworkChangeActive = MutableStateFlow(false).asStateFlow()
- override val satelliteLevel = MutableStateFlow(0)
+ override val isRoaming: State<Boolean> = stateOf(false)
+ override val carrierId: State<Int> = stateOf(INVALID_SUBSCRIPTION_ID)
+ override val inflateSignalStrength: State<Boolean> = stateOf(false)
+ override val allowNetworkSliceIndicator: State<Boolean> = stateOf(false)
+ override val isEmergencyOnly: State<Boolean> = stateOf(false)
+ override val operatorAlphaShort: State<String?> = stateOf(null)
+ override val isInService: State<Boolean> = stateOf(true)
+ override val isNonTerrestrial: State<Boolean> = stateOf(false)
+ override val isGsm: State<Boolean> = stateOf(false)
+ override val carrierNetworkChangeActive: State<Boolean> = stateOf(false)
+ override val satelliteLevel: State<Int> = stateOf(0)
/**
* Carrier merged connections happen over wifi but are displayed as a mobile triangle. Because
* they occur over wifi, it's possible to have a valid carrier merged connection even during
* airplane mode. See b/291993542.
*/
- override val isAllowedDuringAirplaneMode = MutableStateFlow(true).asStateFlow()
+ override val isAllowedDuringAirplaneMode: State<Boolean> = stateOf(true)
/**
* It's not currently considered possible that a carrier merged network can have these
* prioritized capabilities. If we need to track them, we can add the same check as is in
* [MobileConnectionRepositoryImpl].
*/
- override val hasPrioritizedNetworkCapabilities = MutableStateFlow(false).asStateFlow()
+ override val hasPrioritizedNetworkCapabilities: State<Boolean> = stateOf(false)
- override val dataEnabled: StateFlow<Boolean> = wifiRepository.isWifiEnabled
-
- override suspend fun isInEcmMode(): Boolean =
- withContext(bgContext) { telephonyManager.emergencyCallbackMode }
+ override val dataEnabled: State<Boolean>
+ get() = isWifiEnabled
companion object {
// Carrier merged is never roaming
@@ -204,18 +187,19 @@
@Inject
constructor(
private val telephonyManager: TelephonyManager,
- @Background private val bgContext: CoroutineContext,
- @Background private val scope: CoroutineScope,
private val wifiRepository: WifiRepository,
) {
- fun build(subId: Int, mobileLogger: TableLogBuffer): MobileConnectionRepository {
- return CarrierMergedConnectionRepository(
+ fun build(
+ subId: Int,
+ mobileLogger: TableLogBuffer,
+ mobileRepo: MobileConnectionRepositoryKairos,
+ ): CarrierMergedConnectionRepositoryKairos {
+ return CarrierMergedConnectionRepositoryKairos(
subId,
mobileLogger,
telephonyManager.createForSubscriptionId(subId),
- bgContext,
- scope,
wifiRepository,
+ mobileRepo.isInEcmMode,
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryKairos.kt
index 38e6216..1a8ca95 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryKairos.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryKairos.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,24 +18,23 @@
import android.util.IndentingPrintWriter
import androidx.annotation.VisibleForTesting
-import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.KairosBuilder
+import com.android.systemui.kairos.BuildSpec
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.State
+import com.android.systemui.kairos.flatMap
+import com.android.systemui.kairosBuilder
import com.android.systemui.log.table.TableLogBuffer
-import com.android.systemui.log.table.TableLogBufferFactory
import com.android.systemui.log.table.logDiffsForTable
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
-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.model.ResolvedNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepositoryKairos
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import java.io.PrintWriter
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.mapLatest
-import kotlinx.coroutines.flow.stateIn
/**
* A repository that fully implements a mobile connection.
@@ -43,383 +42,200 @@
* This connection could either be a typical mobile connection (see [MobileConnectionRepositoryImpl]
* or a carrier merged connection (see [CarrierMergedConnectionRepository]). This repository
* switches between the two types of connections based on whether the connection is currently
- * carrier merged (see [setIsCarrierMerged]).
+ * carrier merged.
*/
-@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
-class FullMobileConnectionRepositoryKairos(
- override val subId: Int,
- startingIsCarrierMerged: Boolean,
- override val tableLogBuffer: TableLogBuffer,
- subscriptionModel: Flow<SubscriptionModel?>,
- private val defaultNetworkName: NetworkNameModel,
- private val networkNameSeparator: String,
- @Background scope: CoroutineScope,
- private val mobileRepoFactory: MobileConnectionRepositoryImpl.Factory,
- private val carrierMergedRepoFactory: CarrierMergedConnectionRepository.Factory,
-) : MobileConnectionRepository, MobileConnectionRepositoryKairos {
- /**
- * Sets whether this connection is a typical mobile connection or a carrier merged connection.
- */
- fun setIsCarrierMerged(isCarrierMerged: Boolean) {
- _isCarrierMerged.value = isCarrierMerged
- }
+@ExperimentalKairosApi
+class FullMobileConnectionRepositoryKairos
+@AssistedInject
+constructor(
+ @Assisted override val subId: Int,
+ @Assisted override val tableLogBuffer: TableLogBuffer,
+ @Assisted private val mobileRepo: MobileConnectionRepositoryKairos,
+ @Assisted private val carrierMergedRepoSpec: BuildSpec<MobileConnectionRepositoryKairos>,
+ @Assisted private val isCarrierMerged: State<Boolean>,
+) : MobileConnectionRepositoryKairos, KairosBuilder by kairosBuilder() {
- /**
- * Returns true if this repo is currently for a carrier merged connection and false otherwise.
- */
- @VisibleForTesting fun getIsCarrierMerged() = _isCarrierMerged.value
-
- private val _isCarrierMerged = MutableStateFlow(startingIsCarrierMerged)
- private val isCarrierMerged: StateFlow<Boolean> =
- _isCarrierMerged
- .logDiffsForTable(
- tableLogBuffer,
- columnName = "isCarrierMerged",
- initialValue = startingIsCarrierMerged,
- )
- .stateIn(scope, SharingStarted.WhileSubscribed(), startingIsCarrierMerged)
-
- private val mobileRepo: MobileConnectionRepository by lazy {
- mobileRepoFactory.build(
- subId,
- tableLogBuffer,
- subscriptionModel,
- defaultNetworkName,
- networkNameSeparator,
- )
- }
-
- private val carrierMergedRepo: MobileConnectionRepository by lazy {
- carrierMergedRepoFactory.build(subId, tableLogBuffer)
+ init {
+ onActivated {
+ logDiffsForTable(isCarrierMerged, tableLogBuffer, columnName = "isCarrierMerged")
+ }
}
@VisibleForTesting
- val activeRepo: StateFlow<MobileConnectionRepository> = run {
- val initial =
- if (startingIsCarrierMerged) {
- carrierMergedRepo
+ val activeRepo: State<MobileConnectionRepositoryKairos> = buildState {
+ isCarrierMerged.mapLatestBuild { merged ->
+ if (merged) {
+ carrierMergedRepoSpec.applySpec()
} else {
mobileRepo
}
-
- this.isCarrierMerged
- .mapLatest { isCarrierMerged ->
- if (isCarrierMerged) {
- carrierMergedRepo
- } else {
- mobileRepo
- }
- }
- .stateIn(scope, SharingStarted.WhileSubscribed(), initial)
+ }
}
- override val carrierId =
- activeRepo
- .flatMapLatest { it.carrierId }
- .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.carrierId.value)
+ override val carrierId: State<Int> = activeRepo.flatMap { it.carrierId }
- override val cdmaRoaming =
- activeRepo
- .flatMapLatest { it.cdmaRoaming }
- .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.cdmaRoaming.value)
+ override val cdmaRoaming: State<Boolean> = activeRepo.flatMap { it.cdmaRoaming }
- override val isEmergencyOnly =
+ override val isEmergencyOnly: State<Boolean> =
activeRepo
- .flatMapLatest { it.isEmergencyOnly }
- .logDiffsForTable(
- tableLogBuffer,
- columnName = COL_EMERGENCY,
- initialValue = activeRepo.value.isEmergencyOnly.value,
- )
- .stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- activeRepo.value.isEmergencyOnly.value,
- )
+ .flatMap { it.isEmergencyOnly }
+ .also {
+ onActivated { logDiffsForTable(it, tableLogBuffer, columnName = COL_EMERGENCY) }
+ }
- override val isRoaming =
+ override val isRoaming: State<Boolean> =
activeRepo
- .flatMapLatest { it.isRoaming }
- .logDiffsForTable(
- tableLogBuffer,
- columnName = COL_ROAMING,
- initialValue = activeRepo.value.isRoaming.value,
- )
- .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.isRoaming.value)
+ .flatMap { it.isRoaming }
+ .also { onActivated { logDiffsForTable(it, tableLogBuffer, columnName = COL_ROAMING) } }
- override val operatorAlphaShort =
+ override val operatorAlphaShort: State<String?> =
activeRepo
- .flatMapLatest { it.operatorAlphaShort }
- .logDiffsForTable(
- tableLogBuffer,
- columnName = COL_OPERATOR,
- initialValue = activeRepo.value.operatorAlphaShort.value,
- )
- .stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- activeRepo.value.operatorAlphaShort.value,
- )
+ .flatMap { it.operatorAlphaShort }
+ .also {
+ onActivated { logDiffsForTable(it, tableLogBuffer, columnName = COL_OPERATOR) }
+ }
- override val isInService =
+ override val isInService: State<Boolean> =
activeRepo
- .flatMapLatest { it.isInService }
- .logDiffsForTable(
- tableLogBuffer,
- columnName = COL_IS_IN_SERVICE,
- initialValue = activeRepo.value.isInService.value,
- )
- .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.isInService.value)
+ .flatMap { it.isInService }
+ .also {
+ onActivated { logDiffsForTable(it, tableLogBuffer, columnName = COL_IS_IN_SERVICE) }
+ }
- override val isNonTerrestrial =
+ override val isNonTerrestrial: State<Boolean> =
activeRepo
- .flatMapLatest { it.isNonTerrestrial }
- .logDiffsForTable(
- tableLogBuffer,
- columnName = COL_IS_NTN,
- initialValue = activeRepo.value.isNonTerrestrial.value,
- )
- .stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- activeRepo.value.isNonTerrestrial.value,
- )
+ .flatMap { it.isNonTerrestrial }
+ .also { onActivated { logDiffsForTable(it, tableLogBuffer, columnName = COL_IS_NTN) } }
- override val isGsm =
+ override val isGsm: State<Boolean> =
activeRepo
- .flatMapLatest { it.isGsm }
- .logDiffsForTable(
- tableLogBuffer,
- columnName = COL_IS_GSM,
- initialValue = activeRepo.value.isGsm.value,
- )
- .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.isGsm.value)
+ .flatMap { it.isGsm }
+ .also { onActivated { logDiffsForTable(it, tableLogBuffer, columnName = COL_IS_GSM) } }
- override val cdmaLevel =
+ override val cdmaLevel: State<Int> =
activeRepo
- .flatMapLatest { it.cdmaLevel }
- .logDiffsForTable(
- tableLogBuffer,
- columnName = COL_CDMA_LEVEL,
- initialValue = activeRepo.value.cdmaLevel.value,
- )
- .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.cdmaLevel.value)
+ .flatMap { it.cdmaLevel }
+ .also {
+ onActivated { logDiffsForTable(it, tableLogBuffer, columnName = COL_CDMA_LEVEL) }
+ }
- override val primaryLevel =
+ override val primaryLevel: State<Int> =
activeRepo
- .flatMapLatest { it.primaryLevel }
- .logDiffsForTable(
- tableLogBuffer,
- columnName = COL_PRIMARY_LEVEL,
- initialValue = activeRepo.value.primaryLevel.value,
- )
- .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.primaryLevel.value)
+ .flatMap { it.primaryLevel }
+ .also {
+ onActivated { logDiffsForTable(it, tableLogBuffer, columnName = COL_PRIMARY_LEVEL) }
+ }
- override val satelliteLevel: StateFlow<Int> =
+ override val satelliteLevel: State<Int> =
activeRepo
- .flatMapLatest { it.satelliteLevel }
- .logDiffsForTable(
- tableLogBuffer,
- columnName = COL_SATELLITE_LEVEL,
- initialValue = activeRepo.value.satelliteLevel.value,
- )
- .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.satelliteLevel.value)
+ .flatMap { it.satelliteLevel }
+ .also {
+ onActivated {
+ logDiffsForTable(it, tableLogBuffer, columnName = COL_SATELLITE_LEVEL)
+ }
+ }
- override val dataConnectionState =
+ override val dataConnectionState: State<DataConnectionState> =
activeRepo
- .flatMapLatest { it.dataConnectionState }
- .logDiffsForTable(
- tableLogBuffer,
- initialValue = activeRepo.value.dataConnectionState.value,
- )
- .stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- activeRepo.value.dataConnectionState.value,
- )
+ .flatMap { it.dataConnectionState }
+ .also { onActivated { logDiffsForTable(it, tableLogBuffer, columnPrefix = "") } }
- override val dataActivityDirection =
+ override val dataActivityDirection: State<DataActivityModel> =
activeRepo
- .flatMapLatest { it.dataActivityDirection }
- .logDiffsForTable(
- tableLogBuffer,
- initialValue = activeRepo.value.dataActivityDirection.value,
- )
- .stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- activeRepo.value.dataActivityDirection.value,
- )
+ .flatMap { it.dataActivityDirection }
+ .also { onActivated { logDiffsForTable(it, tableLogBuffer, columnPrefix = "") } }
- override val carrierNetworkChangeActive =
+ override val carrierNetworkChangeActive: State<Boolean> =
activeRepo
- .flatMapLatest { it.carrierNetworkChangeActive }
- .logDiffsForTable(
- tableLogBuffer,
- columnName = COL_CARRIER_NETWORK_CHANGE,
- initialValue = activeRepo.value.carrierNetworkChangeActive.value,
- )
- .stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- activeRepo.value.carrierNetworkChangeActive.value,
- )
+ .flatMap { it.carrierNetworkChangeActive }
+ .also {
+ onActivated {
+ logDiffsForTable(it, tableLogBuffer, columnName = COL_CARRIER_NETWORK_CHANGE)
+ }
+ }
- override val resolvedNetworkType =
+ override val resolvedNetworkType: State<ResolvedNetworkType> =
activeRepo
- .flatMapLatest { it.resolvedNetworkType }
- .logDiffsForTable(
- tableLogBuffer,
- initialValue = activeRepo.value.resolvedNetworkType.value,
- )
- .stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- activeRepo.value.resolvedNetworkType.value,
- )
+ .flatMap { it.resolvedNetworkType }
+ .also { onActivated { logDiffsForTable(it, tableLogBuffer, columnPrefix = "") } }
- override val dataEnabled =
+ override val dataEnabled: State<Boolean> =
activeRepo
- .flatMapLatest { it.dataEnabled }
- .logDiffsForTable(
- tableLogBuffer,
- columnName = "dataEnabled",
- initialValue = activeRepo.value.dataEnabled.value,
- )
- .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.dataEnabled.value)
+ .flatMap { it.dataEnabled }
+ .also {
+ onActivated { logDiffsForTable(it, tableLogBuffer, columnName = "dataEnabled") }
+ }
- override val inflateSignalStrength =
+ override val inflateSignalStrength: State<Boolean> =
activeRepo
- .flatMapLatest { it.inflateSignalStrength }
- .logDiffsForTable(
- tableLogBuffer,
- columnName = "inflate",
- initialValue = activeRepo.value.inflateSignalStrength.value,
- )
- .stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- activeRepo.value.inflateSignalStrength.value,
- )
+ .flatMap { it.inflateSignalStrength }
+ .also { onActivated { logDiffsForTable(it, tableLogBuffer, columnName = "inflate") } }
- override val allowNetworkSliceIndicator =
+ override val allowNetworkSliceIndicator: State<Boolean> =
activeRepo
- .flatMapLatest { it.allowNetworkSliceIndicator }
- .logDiffsForTable(
- tableLogBuffer,
- columnName = "allowSlice",
- initialValue = activeRepo.value.allowNetworkSliceIndicator.value,
- )
- .stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- activeRepo.value.allowNetworkSliceIndicator.value,
- )
+ .flatMap { it.allowNetworkSliceIndicator }
+ .also {
+ onActivated { logDiffsForTable(it, tableLogBuffer, columnName = "allowSlice") }
+ }
- override val numberOfLevels =
+ override val numberOfLevels: State<Int> = activeRepo.flatMap { it.numberOfLevels }
+
+ override val networkName: State<NetworkNameModel> =
activeRepo
- .flatMapLatest { it.numberOfLevels }
- .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.numberOfLevels.value)
+ .flatMap { it.networkName }
+ .also { onActivated { logDiffsForTable(it, tableLogBuffer, columnPrefix = "intent") } }
- override val networkName =
+ override val carrierName: State<NetworkNameModel> =
activeRepo
- .flatMapLatest { it.networkName }
- .logDiffsForTable(
- tableLogBuffer,
- columnPrefix = "intent",
- initialValue = activeRepo.value.networkName.value,
- )
- .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.networkName.value)
+ .flatMap { it.carrierName }
+ .also { onActivated { logDiffsForTable(it, tableLogBuffer, columnPrefix = "sub") } }
- override val carrierName =
- activeRepo
- .flatMapLatest { it.carrierName }
- .logDiffsForTable(
- tableLogBuffer,
- columnPrefix = "sub",
- initialValue = activeRepo.value.carrierName.value,
- )
- .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.carrierName.value)
+ override val isAllowedDuringAirplaneMode: State<Boolean> =
+ activeRepo.flatMap { it.isAllowedDuringAirplaneMode }
- override val isAllowedDuringAirplaneMode =
- activeRepo
- .flatMapLatest { it.isAllowedDuringAirplaneMode }
- .stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- activeRepo.value.isAllowedDuringAirplaneMode.value,
- )
+ override val hasPrioritizedNetworkCapabilities: State<Boolean> =
+ activeRepo.flatMap { it.hasPrioritizedNetworkCapabilities }
- override val hasPrioritizedNetworkCapabilities =
- activeRepo
- .flatMapLatest { it.hasPrioritizedNetworkCapabilities }
- .stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- activeRepo.value.hasPrioritizedNetworkCapabilities.value,
- )
+ override val isInEcmMode: State<Boolean> = activeRepo.flatMap { it.isInEcmMode }
- override suspend fun isInEcmMode(): Boolean = activeRepo.value.isInEcmMode()
+ private var dumpCache: DumpCache? = null
+
+ private data class DumpCache(
+ val isCarrierMerged: Boolean,
+ val activeRepo: MobileConnectionRepositoryKairos,
+ )
fun dump(pw: PrintWriter) {
+ val cache = dumpCache ?: return
val ipw = IndentingPrintWriter(pw, " ")
ipw.println("MobileConnectionRepository[$subId]")
ipw.increaseIndent()
- ipw.println("carrierMerged=${_isCarrierMerged.value}")
+ ipw.println("carrierMerged=${cache.isCarrierMerged}")
ipw.print("Type (cellular or carrier merged): ")
- when (activeRepo.value) {
- is CarrierMergedConnectionRepository -> ipw.println("Carrier merged")
- is MobileConnectionRepositoryImpl -> ipw.println("Cellular")
+ when (cache.activeRepo) {
+ is CarrierMergedConnectionRepositoryKairos -> ipw.println("Carrier merged")
+ is MobileConnectionRepositoryKairosImpl -> ipw.println("Cellular")
}
ipw.increaseIndent()
- ipw.println("Provider: ${activeRepo.value}")
+ ipw.println("Provider: ${cache.activeRepo}")
ipw.decreaseIndent()
ipw.decreaseIndent()
}
- class Factory
- @Inject
- constructor(
- @Background private val scope: CoroutineScope,
- private val logFactory: TableLogBufferFactory,
- private val mobileRepoFactory: MobileConnectionRepositoryImpl.Factory,
- private val carrierMergedRepoFactory: CarrierMergedConnectionRepository.Factory,
- ) {
- fun build(
+ @AssistedFactory
+ interface Factory {
+ fun create(
subId: Int,
- startingIsCarrierMerged: Boolean,
- subscriptionModel: Flow<SubscriptionModel?>,
- defaultNetworkName: NetworkNameModel,
- networkNameSeparator: String,
- ): FullMobileConnectionRepositoryKairos {
- val mobileLogger =
- logFactory.getOrCreate(tableBufferLogName(subId), MOBILE_CONNECTION_BUFFER_SIZE)
-
- return FullMobileConnectionRepositoryKairos(
- subId,
- startingIsCarrierMerged,
- mobileLogger,
- subscriptionModel,
- defaultNetworkName,
- networkNameSeparator,
- scope,
- mobileRepoFactory,
- carrierMergedRepoFactory,
- )
- }
-
- companion object {
- /** The buffer size to use for logging. */
- const val MOBILE_CONNECTION_BUFFER_SIZE = 100
-
- /** Returns a log buffer name for a mobile connection with the given [subId]. */
- fun tableBufferLogName(subId: Int): String = "MobileConnectionLog[$subId]"
- }
+ mobileLogger: TableLogBuffer,
+ isCarrierMerged: State<Boolean>,
+ mobileRepo: MobileConnectionRepositoryKairos,
+ mergedRepoSpec: BuildSpec<MobileConnectionRepositoryKairos>,
+ ): FullMobileConnectionRepositoryKairos
}
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
index b4a45e2..bf7c299 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
@@ -542,6 +542,10 @@
data class OnCarrierRoamingNtnSignalStrengthChanged(val signalStrength: NtnSignalStrength) :
CallbackEvent
+
+ data class OnCallBackModeStarted(val type: Int) : CallbackEvent
+
+ data class OnCallBackModeStopped(val type: Int) : CallbackEvent
}
/**
@@ -560,6 +564,8 @@
val onCarrierRoamingNtnSignalStrengthChanged:
CallbackEvent.OnCarrierRoamingNtnSignalStrengthChanged? =
null,
+ val addedCallbackModes: Set<Int> = emptySet(),
+ val removedCallbackModes: Set<Int> = emptySet(),
) {
fun applyEvent(event: CallbackEvent): TelephonyCallbackState {
return when (event) {
@@ -578,6 +584,37 @@
is CallbackEvent.OnSignalStrengthChanged -> copy(onSignalStrengthChanged = event)
is CallbackEvent.OnCarrierRoamingNtnSignalStrengthChanged ->
copy(onCarrierRoamingNtnSignalStrengthChanged = event)
+ is CallbackEvent.OnCallBackModeStarted -> {
+ copy(
+ addedCallbackModes =
+ if (event.type !in removedCallbackModes) {
+ addedCallbackModes + event.type
+ } else {
+ addedCallbackModes
+ },
+ removedCallbackModes =
+ if (event.type !in addedCallbackModes) {
+ removedCallbackModes - event.type
+ } else {
+ removedCallbackModes
+ },
+ )
+ }
+ is CallbackEvent.OnCallBackModeStopped ->
+ copy(
+ addedCallbackModes =
+ if (event.type !in removedCallbackModes) {
+ addedCallbackModes - event.type
+ } else {
+ addedCallbackModes
+ },
+ removedCallbackModes =
+ if (event.type !in addedCallbackModes) {
+ removedCallbackModes + event.type
+ } else {
+ removedCallbackModes
+ },
+ )
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryKairosAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryKairosAdapter.kt
new file mode 100644
index 0000000..9b37f48
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryKairosAdapter.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
+
+import com.android.systemui.kairos.BuildScope
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
+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.model.SystemUiCarrierConfig
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepositoryKairos
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
+import com.android.systemui.util.kotlin.Producer
+import kotlinx.coroutines.flow.StateFlow
+
+@ExperimentalKairosApi
+fun BuildScope.MobileConnectionRepositoryKairosAdapter(
+ kairosRepo: MobileConnectionRepositoryKairos,
+ carrierConfig: SystemUiCarrierConfig,
+) =
+ MobileConnectionRepositoryKairosAdapter(
+ subId = kairosRepo.subId,
+ carrierId = kairosRepo.carrierId.toStateFlow(),
+ inflateSignalStrength = carrierConfig.shouldInflateSignalStrength,
+ allowNetworkSliceIndicator = carrierConfig.allowNetworkSliceIndicator,
+ tableLogBuffer = kairosRepo.tableLogBuffer,
+ isEmergencyOnly = kairosRepo.isEmergencyOnly.toStateFlow(),
+ isRoaming = kairosRepo.isRoaming.toStateFlow(),
+ operatorAlphaShort = kairosRepo.operatorAlphaShort.toStateFlow(),
+ isInService = kairosRepo.isInService.toStateFlow(),
+ isNonTerrestrial = kairosRepo.isNonTerrestrial.toStateFlow(),
+ isGsm = kairosRepo.isGsm.toStateFlow(),
+ cdmaLevel = kairosRepo.cdmaLevel.toStateFlow(),
+ primaryLevel = kairosRepo.primaryLevel.toStateFlow(),
+ satelliteLevel = kairosRepo.satelliteLevel.toStateFlow(),
+ dataConnectionState = kairosRepo.dataConnectionState.toStateFlow(),
+ dataActivityDirection = kairosRepo.dataActivityDirection.toStateFlow(),
+ carrierNetworkChangeActive = kairosRepo.carrierNetworkChangeActive.toStateFlow(),
+ resolvedNetworkType = kairosRepo.resolvedNetworkType.toStateFlow(),
+ numberOfLevels = kairosRepo.numberOfLevels.toStateFlow(),
+ dataEnabled = kairosRepo.dataEnabled.toStateFlow(),
+ cdmaRoaming = kairosRepo.cdmaRoaming.toStateFlow(),
+ networkName = kairosRepo.networkName.toStateFlow(),
+ carrierName = kairosRepo.carrierName.toStateFlow(),
+ isAllowedDuringAirplaneMode = kairosRepo.isAllowedDuringAirplaneMode.toStateFlow(),
+ hasPrioritizedNetworkCapabilities =
+ kairosRepo.hasPrioritizedNetworkCapabilities.toStateFlow(),
+ isInEcmMode = { kairosNetwork.transact { kairosRepo.isInEcmMode.sample() } },
+ )
+
+@ExperimentalKairosApi
+class MobileConnectionRepositoryKairosAdapter(
+ override val subId: Int,
+ override val carrierId: StateFlow<Int>,
+ override val inflateSignalStrength: StateFlow<Boolean>,
+ override val allowNetworkSliceIndicator: StateFlow<Boolean>,
+ override val tableLogBuffer: TableLogBuffer,
+ override val isEmergencyOnly: StateFlow<Boolean>,
+ override val isRoaming: StateFlow<Boolean>,
+ override val operatorAlphaShort: StateFlow<String?>,
+ override val isInService: StateFlow<Boolean>,
+ override val isNonTerrestrial: StateFlow<Boolean>,
+ override val isGsm: StateFlow<Boolean>,
+ override val cdmaLevel: StateFlow<Int>,
+ override val primaryLevel: StateFlow<Int>,
+ override val satelliteLevel: StateFlow<Int>,
+ override val dataConnectionState: StateFlow<DataConnectionState>,
+ override val dataActivityDirection: StateFlow<DataActivityModel>,
+ override val carrierNetworkChangeActive: StateFlow<Boolean>,
+ override val resolvedNetworkType: StateFlow<ResolvedNetworkType>,
+ override val numberOfLevels: StateFlow<Int>,
+ override val dataEnabled: StateFlow<Boolean>,
+ override val cdmaRoaming: StateFlow<Boolean>,
+ override val networkName: StateFlow<NetworkNameModel>,
+ override val carrierName: StateFlow<NetworkNameModel>,
+ override val isAllowedDuringAirplaneMode: StateFlow<Boolean>,
+ override val hasPrioritizedNetworkCapabilities: StateFlow<Boolean>,
+ private val isInEcmMode: Producer<Boolean>,
+) : MobileConnectionRepository {
+ override suspend fun isInEcmMode(): Boolean = isInEcmMode.get()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryKairosImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryKairosImpl.kt
index a074acd..abe72e1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryKairosImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryKairosImpl.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -41,16 +41,30 @@
import android.telephony.TelephonyManager.EXTRA_SUBSCRIPTION_ID
import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN
import android.telephony.TelephonyManager.UNKNOWN_CARRIER_ID
-import android.telephony.satellite.NtnSignalStrength
import com.android.settingslib.Utils
+import com.android.systemui.KairosBuilder
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.flags.FeatureFlagsClassic
import com.android.systemui.flags.Flags.ROAMING_INDICATOR_VIA_DISPLAY_INFO
+import com.android.systemui.kairos.Events
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.State
+import com.android.systemui.kairos.Transactional
+import com.android.systemui.kairos.awaitClose
+import com.android.systemui.kairos.coalescingEvents
+import com.android.systemui.kairos.conflatedEvents
+import com.android.systemui.kairos.map
+import com.android.systemui.kairos.mapNotNull
+import com.android.systemui.kairos.stateOf
+import com.android.systemui.kairos.transactionally
+import com.android.systemui.kairosBuilder
import com.android.systemui.log.table.TableLogBuffer
import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState.Disconnected
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.model.ResolvedNetworkType.DefaultNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType
import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.UnknownNetworkType
@@ -58,57 +72,47 @@
import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfig
import com.android.systemui.statusbar.pipeline.mobile.data.model.toDataConnectionType
import com.android.systemui.statusbar.pipeline.mobile.data.model.toNetworkNameModel
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.CarrierConfigRepository
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepositoryKairos
import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
-import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
-import javax.inject.Inject
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import java.time.Duration
import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.asExecutor
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.mapLatest
-import kotlinx.coroutines.flow.mapNotNull
-import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.scan
-import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
/**
* A repository implementation for a typical mobile connection (as opposed to a carrier merged
* connection -- see [CarrierMergedConnectionRepository]).
*/
-@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
-class MobileConnectionRepositoryKairosImpl(
- override val subId: Int,
+@ExperimentalKairosApi
+class MobileConnectionRepositoryKairosImpl
+@AssistedInject
+constructor(
+ @Assisted override val subId: Int,
private val context: Context,
- subscriptionModel: Flow<SubscriptionModel?>,
- defaultNetworkName: NetworkNameModel,
- networkNameSeparator: String,
+ @Assisted subscriptionModel: State<SubscriptionModel?>,
+ @Assisted defaultNetworkName: NetworkNameModel,
+ @Assisted networkNameSeparator: String,
connectivityManager: ConnectivityManager,
- private val telephonyManager: TelephonyManager,
- systemUiCarrierConfig: SystemUiCarrierConfig,
+ @Assisted private val telephonyManager: TelephonyManager,
+ @Assisted systemUiCarrierConfig: SystemUiCarrierConfig,
broadcastDispatcher: BroadcastDispatcher,
private val mobileMappingsProxy: MobileMappingsProxy,
- private val bgDispatcher: CoroutineDispatcher,
+ @Background private val bgDispatcher: CoroutineDispatcher,
logger: MobileInputLogger,
- override val tableLogBuffer: TableLogBuffer,
+ @Assisted override val tableLogBuffer: TableLogBuffer,
flags: FeatureFlagsClassic,
- scope: CoroutineScope,
-) : MobileConnectionRepository, MobileConnectionRepositoryKairos {
+) : MobileConnectionRepositoryKairos, KairosBuilder by kairosBuilder() {
+
init {
if (telephonyManager.subscriptionId != subId) {
throw IllegalStateException(
@@ -135,240 +139,225 @@
* it tracked. We use the [scan] operator here to track the most recent callback of _each type_
* here. See [TelephonyCallbackState] to see how the callbacks are stored.
*/
- private val callbackEvents: StateFlow<TelephonyCallbackState> = run {
- val initial = TelephonyCallbackState()
- callbackFlow {
- val callback =
- object :
- TelephonyCallback(),
- TelephonyCallback.CarrierNetworkListener,
- TelephonyCallback.CarrierRoamingNtnListener,
- TelephonyCallback.DataActivityListener,
- TelephonyCallback.DataConnectionStateListener,
- TelephonyCallback.DataEnabledListener,
- TelephonyCallback.DisplayInfoListener,
- TelephonyCallback.ServiceStateListener,
- TelephonyCallback.SignalStrengthsListener {
+ private val callbackEvents: Events<TelephonyCallbackState> = buildEvents {
+ coalescingEvents(
+ initialValue = TelephonyCallbackState(),
+ coalesce = TelephonyCallbackState::applyEvent,
+ ) {
+ val callback =
+ object :
+ TelephonyCallback(),
+ TelephonyCallback.CarrierNetworkListener,
+ TelephonyCallback.CarrierRoamingNtnListener,
+ TelephonyCallback.DataActivityListener,
+ TelephonyCallback.DataConnectionStateListener,
+ TelephonyCallback.DataEnabledListener,
+ TelephonyCallback.DisplayInfoListener,
+ TelephonyCallback.ServiceStateListener,
+ TelephonyCallback.SignalStrengthsListener,
+ TelephonyCallback.EmergencyCallbackModeListener {
- override fun onCarrierNetworkChange(active: Boolean) {
- logger.logOnCarrierNetworkChange(active, subId)
- trySend(CallbackEvent.OnCarrierNetworkChange(active))
- }
-
- override fun onCarrierRoamingNtnModeChanged(active: Boolean) {
- logger.logOnCarrierRoamingNtnModeChanged(active)
- trySend(CallbackEvent.OnCarrierRoamingNtnModeChanged(active))
- }
-
- override fun onDataActivity(direction: Int) {
- logger.logOnDataActivity(direction, subId)
- trySend(CallbackEvent.OnDataActivity(direction))
- }
-
- override fun onDataEnabledChanged(enabled: Boolean, reason: Int) {
- logger.logOnDataEnabledChanged(enabled, subId)
- trySend(CallbackEvent.OnDataEnabledChanged(enabled))
- }
-
- override fun onDataConnectionStateChanged(
- dataState: Int,
- networkType: Int,
- ) {
- logger.logOnDataConnectionStateChanged(dataState, networkType, subId)
- trySend(CallbackEvent.OnDataConnectionStateChanged(dataState))
- }
-
- override fun onDisplayInfoChanged(
- telephonyDisplayInfo: TelephonyDisplayInfo
- ) {
- logger.logOnDisplayInfoChanged(telephonyDisplayInfo, subId)
- trySend(CallbackEvent.OnDisplayInfoChanged(telephonyDisplayInfo))
- }
-
- override fun onServiceStateChanged(serviceState: ServiceState) {
- logger.logOnServiceStateChanged(serviceState, subId)
- trySend(CallbackEvent.OnServiceStateChanged(serviceState))
- }
-
- override fun onSignalStrengthsChanged(signalStrength: SignalStrength) {
- logger.logOnSignalStrengthsChanged(signalStrength, subId)
- trySend(CallbackEvent.OnSignalStrengthChanged(signalStrength))
- }
-
- override fun onCarrierRoamingNtnSignalStrengthChanged(
- signalStrength: NtnSignalStrength
- ) {
- logger.logNtnSignalStrengthChanged(signalStrength)
- trySend(
- CallbackEvent.OnCarrierRoamingNtnSignalStrengthChanged(
- signalStrength
- )
- )
- }
+ override fun onCarrierNetworkChange(active: Boolean) {
+ logger.logOnCarrierNetworkChange(active, subId)
+ emit(CallbackEvent.OnCarrierNetworkChange(active))
}
+
+ override fun onCarrierRoamingNtnModeChanged(active: Boolean) {
+ logger.logOnCarrierRoamingNtnModeChanged(active)
+ emit(CallbackEvent.OnCarrierRoamingNtnModeChanged(active))
+ }
+
+ override fun onDataActivity(direction: Int) {
+ logger.logOnDataActivity(direction, subId)
+ emit(CallbackEvent.OnDataActivity(direction))
+ }
+
+ override fun onDataEnabledChanged(enabled: Boolean, reason: Int) {
+ logger.logOnDataEnabledChanged(enabled, subId)
+ emit(CallbackEvent.OnDataEnabledChanged(enabled))
+ }
+
+ override fun onDataConnectionStateChanged(dataState: Int, networkType: Int) {
+ logger.logOnDataConnectionStateChanged(dataState, networkType, subId)
+ emit(CallbackEvent.OnDataConnectionStateChanged(dataState))
+ }
+
+ override fun onDisplayInfoChanged(telephonyDisplayInfo: TelephonyDisplayInfo) {
+ logger.logOnDisplayInfoChanged(telephonyDisplayInfo, subId)
+ emit(CallbackEvent.OnDisplayInfoChanged(telephonyDisplayInfo))
+ }
+
+ override fun onServiceStateChanged(serviceState: ServiceState) {
+ logger.logOnServiceStateChanged(serviceState, subId)
+ emit(CallbackEvent.OnServiceStateChanged(serviceState))
+ }
+
+ override fun onSignalStrengthsChanged(signalStrength: SignalStrength) {
+ logger.logOnSignalStrengthsChanged(signalStrength, subId)
+ emit(CallbackEvent.OnSignalStrengthChanged(signalStrength))
+ }
+
+ override fun onCallbackModeStarted(
+ type: Int,
+ timerDuration: Duration,
+ subId: Int,
+ ) {
+ // logger.logOnCallBackModeStarted(type, subId)
+ emit(CallbackEvent.OnCallBackModeStarted(type))
+ }
+
+ override fun onCallbackModeRestarted(
+ type: Int,
+ timerDuration: Duration,
+ subId: Int,
+ ) {
+ // no-op
+ }
+
+ override fun onCallbackModeStopped(type: Int, reason: Int, subId: Int) {
+ // logger.logOnCallBackModeStopped(type, reason, subId)
+ emit(CallbackEvent.OnCallBackModeStopped(type))
+ }
+ }
+ withContext(bgDispatcher) {
telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
- awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
}
- .flowOn(bgDispatcher)
- .scan(initial = initial) { state, event -> state.applyEvent(event) }
- .stateIn(scope = scope, started = SharingStarted.WhileSubscribed(), initial)
+ awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
+ }
}
- override val isEmergencyOnly =
- callbackEvents
- .mapNotNull { it.onServiceStateChanged }
- .map { it.serviceState.isEmergencyOnly }
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+ private val serviceState: State<ServiceState?> = buildState {
+ callbackEvents.mapNotNull { it.onServiceStateChanged?.serviceState }.holdState(null)
+ }
- override val isRoaming =
+ override val isEmergencyOnly: State<Boolean> = serviceState.map { it?.isEmergencyOnly == true }
+
+ private val displayInfo: State<TelephonyDisplayInfo?> = buildState {
+ callbackEvents.mapNotNull { it.onDisplayInfoChanged?.telephonyDisplayInfo }.holdState(null)
+ }
+
+ override val isRoaming: State<Boolean> =
if (flags.isEnabled(ROAMING_INDICATOR_VIA_DISPLAY_INFO)) {
- callbackEvents
- .mapNotNull { it.onDisplayInfoChanged }
- .map { it.telephonyDisplayInfo.isRoaming }
+ displayInfo.map { it?.isRoaming == true }
+ } else {
+ serviceState.map { it?.roaming == true }
+ }
+
+ override val operatorAlphaShort: State<String?> = serviceState.map { it?.operatorAlphaShort }
+
+ override val isInService: State<Boolean> =
+ serviceState.map { it?.let(Utils::isInService) == true }
+
+ private val carrierRoamingNtnActive: State<Boolean> = buildState {
+ callbackEvents.mapNotNull { it.onCarrierRoamingNtnModeChanged?.active }.holdState(false)
+ }
+
+ override val isNonTerrestrial: State<Boolean>
+ get() = carrierRoamingNtnActive
+
+ private val signalStrength: State<SignalStrength?> = buildState {
+ callbackEvents.mapNotNull { it.onSignalStrengthChanged?.signalStrength }.holdState(null)
+ }
+
+ override val isGsm: State<Boolean> = signalStrength.map { it?.isGsm == true }
+
+ override val cdmaLevel: State<Int> =
+ signalStrength.map {
+ it?.getCellSignalStrengths(CellSignalStrengthCdma::class.java)?.firstOrNull()?.level
+ ?: SIGNAL_STRENGTH_NONE_OR_UNKNOWN
+ }
+
+ override val primaryLevel: State<Int> =
+ signalStrength.map { it?.level ?: SIGNAL_STRENGTH_NONE_OR_UNKNOWN }
+
+ override val satelliteLevel: State<Int> = buildState {
+ callbackEvents
+ .mapNotNull { it.onCarrierRoamingNtnSignalStrengthChanged?.signalStrength?.level }
+ .holdState(0)
+ }
+
+ override val dataConnectionState: State<DataConnectionState> = buildState {
+ callbackEvents
+ .mapNotNull { it.onDataConnectionStateChanged?.dataState?.toDataConnectionType() }
+ .holdState(Disconnected)
+ }
+
+ override val dataActivityDirection: State<DataActivityModel> = buildState {
+ callbackEvents
+ .mapNotNull { it.onDataActivity?.direction?.toMobileDataActivityModel() }
+ .holdState(DataActivityModel(hasActivityIn = false, hasActivityOut = false))
+ }
+
+ override val carrierNetworkChangeActive: State<Boolean> = buildState {
+ callbackEvents.mapNotNull { it.onCarrierNetworkChange?.active }.holdState(false)
+ }
+
+ private val telephonyDisplayInfo: State<TelephonyDisplayInfo?> = buildState {
+ callbackEvents.mapNotNull { it.onDisplayInfoChanged?.telephonyDisplayInfo }.holdState(null)
+ }
+
+ override val resolvedNetworkType: State<ResolvedNetworkType> =
+ telephonyDisplayInfo.map { displayInfo ->
+ displayInfo
+ ?.overrideNetworkType
+ ?.takeIf { it != OVERRIDE_NETWORK_TYPE_NONE }
+ ?.let { OverrideNetworkType(mobileMappingsProxy.toIconKeyOverride(it)) }
+ ?: displayInfo
+ ?.networkType
+ ?.takeIf { it != NETWORK_TYPE_UNKNOWN }
+ ?.let { DefaultNetworkType(mobileMappingsProxy.toIconKey(it)) }
+ ?: UnknownNetworkType
+ }
+
+ override val inflateSignalStrength: State<Boolean> = buildState {
+ systemUiCarrierConfig.shouldInflateSignalStrength.toState()
+ }
+
+ override val allowNetworkSliceIndicator: State<Boolean> = buildState {
+ systemUiCarrierConfig.allowNetworkSliceIndicator.toState()
+ }
+
+ override val numberOfLevels: State<Int> =
+ inflateSignalStrength.map { shouldInflate ->
+ if (shouldInflate) {
+ DEFAULT_NUM_LEVELS + 1
} else {
- callbackEvents
- .mapNotNull { it.onServiceStateChanged }
- .map { it.serviceState.roaming }
+ DEFAULT_NUM_LEVELS
}
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+ }
- override val operatorAlphaShort =
- callbackEvents
- .mapNotNull { it.onServiceStateChanged }
- .map { it.serviceState.operatorAlphaShort }
- .stateIn(scope, SharingStarted.WhileSubscribed(), null)
-
- override val isInService =
- callbackEvents
- .mapNotNull { it.onServiceStateChanged }
- .map { Utils.isInService(it.serviceState) }
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
-
- override val isNonTerrestrial =
- callbackEvents
- .mapNotNull { it.onCarrierRoamingNtnModeChanged }
- .map { it.active }
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
-
- override val isGsm =
- callbackEvents
- .mapNotNull { it.onSignalStrengthChanged }
- .map { it.signalStrength.isGsm }
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
-
- override val cdmaLevel =
- callbackEvents
- .mapNotNull { it.onSignalStrengthChanged }
- .map {
- it.signalStrength.getCellSignalStrengths(CellSignalStrengthCdma::class.java).let {
- strengths ->
- if (strengths.isNotEmpty()) {
- strengths[0].level
- } else {
- SIGNAL_STRENGTH_NONE_OR_UNKNOWN
- }
- }
- }
- .stateIn(scope, SharingStarted.WhileSubscribed(), SIGNAL_STRENGTH_NONE_OR_UNKNOWN)
-
- override val primaryLevel =
- callbackEvents
- .mapNotNull { it.onSignalStrengthChanged }
- .map { it.signalStrength.level }
- .stateIn(scope, SharingStarted.WhileSubscribed(), SIGNAL_STRENGTH_NONE_OR_UNKNOWN)
-
- override val satelliteLevel: StateFlow<Int> =
- callbackEvents
- .mapNotNull { it.onCarrierRoamingNtnSignalStrengthChanged }
- .map { it.signalStrength.level }
- .stateIn(scope, SharingStarted.WhileSubscribed(), 0)
-
- override val dataConnectionState =
- callbackEvents
- .mapNotNull { it.onDataConnectionStateChanged }
- .map { it.dataState.toDataConnectionType() }
- .stateIn(scope, SharingStarted.WhileSubscribed(), Disconnected)
-
- override val dataActivityDirection =
- callbackEvents
- .mapNotNull { it.onDataActivity }
- .map { it.direction.toMobileDataActivityModel() }
- .stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- DataActivityModel(hasActivityIn = false, hasActivityOut = false),
- )
-
- override val carrierNetworkChangeActive =
- callbackEvents
- .mapNotNull { it.onCarrierNetworkChange }
- .map { it.active }
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
-
- override val resolvedNetworkType =
- callbackEvents
- .mapNotNull { it.onDisplayInfoChanged }
- .map {
- if (it.telephonyDisplayInfo.overrideNetworkType != OVERRIDE_NETWORK_TYPE_NONE) {
- OverrideNetworkType(
- mobileMappingsProxy.toIconKeyOverride(
- it.telephonyDisplayInfo.overrideNetworkType
- )
- )
- } else if (it.telephonyDisplayInfo.networkType != NETWORK_TYPE_UNKNOWN) {
- DefaultNetworkType(
- mobileMappingsProxy.toIconKey(it.telephonyDisplayInfo.networkType)
- )
- } else {
- UnknownNetworkType
- }
- }
- .stateIn(scope, SharingStarted.WhileSubscribed(), UnknownNetworkType)
-
- override val inflateSignalStrength = systemUiCarrierConfig.shouldInflateSignalStrength
- override val allowNetworkSliceIndicator = systemUiCarrierConfig.allowNetworkSliceIndicator
-
- override val numberOfLevels =
- inflateSignalStrength
- .map { shouldInflate ->
- if (shouldInflate) {
- DEFAULT_NUM_LEVELS + 1
- } else {
- DEFAULT_NUM_LEVELS
- }
- }
- .stateIn(scope, SharingStarted.WhileSubscribed(), DEFAULT_NUM_LEVELS)
-
- override val carrierName =
- subscriptionModel
- .map {
- it?.let { model -> NetworkNameModel.SubscriptionDerived(model.carrierName) }
- ?: defaultNetworkName
- }
- .stateIn(scope, SharingStarted.WhileSubscribed(), defaultNetworkName)
+ override val carrierName: State<NetworkNameModel> =
+ subscriptionModel.map {
+ it?.let { model -> NetworkNameModel.SubscriptionDerived(model.carrierName) }
+ ?: defaultNetworkName
+ }
/**
* 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> = callbackEvents.map { Unit }
+ private val telephonyPollingEvent: Events<Unit> = callbackEvents.map {}
- override val cdmaRoaming: StateFlow<Boolean> =
+ private val cdmaEnhancedRoamingIndicatorDisplayNumber: Transactional<Int?> = transactionally {
+ try {
+ telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber
+ } catch (e: UnsupportedOperationException) {
+ // Handles the same as a function call failure
+ null
+ }
+ }
+
+ override val cdmaRoaming: State<Boolean> = buildState {
telephonyPollingEvent
- .mapLatest {
- try {
- val cdmaEri = telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber
- cdmaEri == ERI_ON || cdmaEri == ERI_FLASH
- } catch (e: UnsupportedOperationException) {
- // Handles the same as a function call failure
- false
- }
+ .map {
+ val cdmaEri = cdmaEnhancedRoamingIndicatorDisplayNumber.sample()
+ cdmaEri == ERI_ON || cdmaEri == ERI_FLASH
}
- .flowOn(bgDispatcher)
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+ .holdState(false)
+ }
- override val carrierId =
+ override val carrierId: State<Int> = buildState {
broadcastDispatcher
.broadcastFlow(
filter =
@@ -379,11 +368,8 @@
intent.getIntExtra(EXTRA_SUBSCRIPTION_ID, INVALID_SUBSCRIPTION_ID) == subId
}
.map { it.carrierId() }
- .onStart {
- // Make sure we get the initial carrierId
- emit(telephonyManager.simCarrierId)
- }
- .stateIn(scope, SharingStarted.WhileSubscribed(), telephonyManager.simCarrierId)
+ .toState(telephonyManager.simCarrierId)
+ }
/**
* BroadcastDispatcher does not handle sticky broadcasts, so we can't use it here. Note that we
@@ -393,8 +379,8 @@
* See b/322432056 for context.
*/
@SuppressLint("RegisterReceiverViaContext")
- override val networkName: StateFlow<NetworkNameModel> =
- conflatedCallbackFlow {
+ override val networkName: State<NetworkNameModel> = buildState {
+ conflatedEvents {
val receiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
@@ -405,7 +391,7 @@
) == subId
) {
logger.logServiceProvidersUpdatedBroadcast(intent)
- trySend(
+ emit(
intent.toNetworkNameModel(networkNameSeparator)
?: defaultNetworkName
)
@@ -420,22 +406,28 @@
awaitClose { context.unregisterReceiver(receiver) }
}
- .flowOn(bgDispatcher)
- .stateIn(scope, SharingStarted.Eagerly, defaultNetworkName)
-
- override val dataEnabled = run {
- val initial = telephonyManager.isDataConnectionAllowed
- callbackEvents
- .mapNotNull { it.onDataEnabledChanged }
- .map { it.enabled }
- .stateIn(scope, SharingStarted.WhileSubscribed(), initial)
+ .holdState(defaultNetworkName)
}
- override suspend fun isInEcmMode(): Boolean =
- withContext(bgDispatcher) { telephonyManager.emergencyCallbackMode }
+ override val dataEnabled: State<Boolean> = buildState {
+ callbackEvents
+ .mapNotNull { it.onDataEnabledChanged?.enabled }
+ .holdState(telephonyManager.isDataConnectionAllowed)
+ }
+
+ override val isInEcmMode: State<Boolean> = buildState {
+ callbackEvents
+ .mapNotNull {
+ (it.addedCallbackModes to it.removedCallbackModes).takeIf { (added, removed) ->
+ added.isNotEmpty() || removed.isNotEmpty()
+ }
+ }
+ .foldState(emptySet<Int>()) { (added, removed), acc -> acc - removed + added }
+ .mapTransactionally { it.isNotEmpty() }
+ }
/** Typical mobile connections aren't available during airplane mode. */
- override val isAllowedDuringAirplaneMode = MutableStateFlow(false).asStateFlow()
+ override val isAllowedDuringAirplaneMode: State<Boolean> = stateOf(false)
/**
* Currently, a network with NET_CAPABILITY_PRIORITIZE_LATENCY is the only type of network that
@@ -444,27 +436,27 @@
* self_certified_network_capabilities.xml config file
*/
@SuppressLint("WrongConstant")
- private val networkSliceRequest =
+ private val networkSliceRequest: NetworkRequest =
NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_PRIORITIZE_LATENCY)
.setSubscriptionIds(setOf(subId))
.build()
@SuppressLint("MissingPermission")
- override val hasPrioritizedNetworkCapabilities: StateFlow<Boolean> =
- conflatedCallbackFlow {
+ override val hasPrioritizedNetworkCapabilities: State<Boolean> = buildState {
+ conflatedEvents {
// Our network callback listens only for this.subId && net_cap_prioritize_latency
// therefore our state is a simple mapping of whether or not that network exists
val callback =
object : NetworkCallback() {
override fun onAvailable(network: Network) {
logger.logPrioritizedNetworkAvailable(network.netId)
- trySend(true)
+ emit(true)
}
override fun onLost(network: Network) {
logger.logPrioritizedNetworkLost(network.netId)
- trySend(false)
+ emit(false)
}
}
@@ -472,48 +464,20 @@
awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
}
- .flowOn(bgDispatcher)
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+ .holdState(false)
+ }
- class Factory
- @Inject
- constructor(
- private val context: Context,
- private val broadcastDispatcher: BroadcastDispatcher,
- private val connectivityManager: ConnectivityManager,
- private val telephonyManager: TelephonyManager,
- private val logger: MobileInputLogger,
- private val carrierConfigRepository: CarrierConfigRepository,
- private val mobileMappingsProxy: MobileMappingsProxy,
- private val flags: FeatureFlagsClassic,
- @Background private val bgDispatcher: CoroutineDispatcher,
- @Background private val scope: CoroutineScope,
- ) {
- fun build(
+ @AssistedFactory
+ fun interface Factory {
+ fun create(
subId: Int,
mobileLogger: TableLogBuffer,
- subscriptionModel: Flow<SubscriptionModel?>,
+ subscriptionModel: State<SubscriptionModel?>,
defaultNetworkName: NetworkNameModel,
networkNameSeparator: String,
- ): MobileConnectionRepositoryKairos {
- return MobileConnectionRepositoryKairosImpl(
- subId,
- context,
- subscriptionModel,
- defaultNetworkName,
- networkNameSeparator,
- connectivityManager,
- telephonyManager.createForSubscriptionId(subId),
- carrierConfigRepository.getOrCreateConfigForSubId(subId),
- broadcastDispatcher,
- mobileMappingsProxy,
- bgDispatcher,
- logger,
- mobileLogger,
- flags,
- scope,
- )
- }
+ systemUiCarrierConfig: SystemUiCarrierConfig,
+ telephonyManager: TelephonyManager,
+ ): MobileConnectionRepositoryKairosImpl
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryKairosImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryKairosImpl.kt
index 51771dd..e468159 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryKairosImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryKairosImpl.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -27,21 +27,52 @@
import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
import android.telephony.TelephonyCallback
import android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener
+import android.telephony.TelephonyCallback.EmergencyCallbackModeListener
import android.telephony.TelephonyManager
import android.util.IndentingPrintWriter
-import androidx.annotation.VisibleForTesting
import com.android.internal.telephony.PhoneConstants
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.keyguard.KeyguardUpdateMonitorCallback
import com.android.settingslib.SignalIcon.MobileIconGroup
import com.android.settingslib.mobile.MobileMappings.Config
import com.android.systemui.Dumpable
+import com.android.systemui.Flags
+import com.android.systemui.KairosActivatable
+import com.android.systemui.KairosBuilder
+import com.android.systemui.activated
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
+import com.android.systemui.kairos.BuildSpec
+import com.android.systemui.kairos.Events
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.Incremental
+import com.android.systemui.kairos.State
+import com.android.systemui.kairos.StateSelector
+import com.android.systemui.kairos.asIncremental
+import com.android.systemui.kairos.asyncEvent
+import com.android.systemui.kairos.buildSpec
+import com.android.systemui.kairos.changes
+import com.android.systemui.kairos.combine
+import com.android.systemui.kairos.effect
+import com.android.systemui.kairos.filterNotNull
+import com.android.systemui.kairos.flatMap
+import com.android.systemui.kairos.map
+import com.android.systemui.kairos.mapNotNull
+import com.android.systemui.kairos.mapValues
+import com.android.systemui.kairos.mergeLeft
+import com.android.systemui.kairos.onEach
+import com.android.systemui.kairos.rebuildOn
+import com.android.systemui.kairos.selector
+import com.android.systemui.kairos.stateOf
+import com.android.systemui.kairos.switchEvents
+import com.android.systemui.kairos.transitions
+import com.android.systemui.kairos.util.WithPrev
+import com.android.systemui.kairosBuilder
import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.TableLogBufferFactory
import com.android.systemui.log.table.logDiffsForTable
import com.android.systemui.res.R
import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository
@@ -49,39 +80,32 @@
import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger
import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.CarrierConfigRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepositoryKairos
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepositoryKairos
import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
import com.android.systemui.statusbar.pipeline.mobile.util.SubscriptionManagerProxy
+import com.android.systemui.statusbar.pipeline.shared.data.model.DefaultConnectionModel
import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel
-import com.android.systemui.util.kotlin.pairwise
import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import dagger.Binds
+import dagger.Lazy
+import dagger.Provides
+import dagger.multibindings.ElementsIntoSet
import java.io.PrintWriter
-import java.lang.ref.WeakReference
-import java.util.concurrent.ConcurrentHashMap
+import java.time.Duration
import javax.inject.Inject
+import javax.inject.Provider
import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.mapLatest
-import kotlinx.coroutines.flow.mapNotNull
-import kotlinx.coroutines.flow.merge
-import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
-@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+@ExperimentalKairosApi
@SysUISingleton
class MobileConnectionsRepositoryKairosImpl
@Inject
@@ -96,39 +120,26 @@
broadcastDispatcher: BroadcastDispatcher,
private val context: Context,
@Background private val bgDispatcher: CoroutineDispatcher,
- @Background private val scope: CoroutineScope,
@Main private val mainDispatcher: CoroutineDispatcher,
airplaneModeRepository: AirplaneModeRepository,
// Some "wifi networks" should be rendered as a mobile connection, which is why the wifi
// repository is an input to the mobile repository.
- // See [CarrierMergedConnectionRepository] for details.
+ // See [CarrierMergedConnectionRepositoryKairos] for details.
wifiRepository: WifiRepository,
- private val fullMobileRepoFactory: FullMobileConnectionRepository.Factory,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
- private val dumpManager: DumpManager,
-) : MobileConnectionsRepository, MobileConnectionsRepositoryKairos, Dumpable {
-
- // TODO(b/333912012): for now, we are never invalidating the cache. We can do better though
- private var subIdRepositoryCache =
- ConcurrentHashMap<Int, WeakReference<FullMobileConnectionRepository>>()
-
- private val defaultNetworkName =
- NetworkNameModel.Default(
- context.getString(com.android.internal.R.string.lockscreen_carrier_default)
- )
-
- private val networkNameSeparator: String =
- context.getString(R.string.status_bar_network_name_separator)
+ dumpManager: DumpManager,
+ private val mobileRepoFactory: Lazy<ConnectionRepoFactory>,
+) : MobileConnectionsRepositoryKairos, Dumpable, KairosBuilder by kairosBuilder() {
init {
- dumpManager.registerNormalDumpable("MobileConnectionsRepository", this)
+ dumpManager.registerNormalDumpable("MobileConnectionsRepositoryKairos", this)
}
- private val carrierMergedSubId: StateFlow<Int?> =
+ private val carrierMergedSubId: State<Int?> = buildState {
combine(
- wifiRepository.wifiNetwork,
- connectivityRepository.defaultConnections,
- airplaneModeRepository.isAirplaneMode,
+ wifiRepository.wifiNetwork.toState(),
+ connectivityRepository.defaultConnections.toState(),
+ airplaneModeRepository.isAirplaneMode.toState(),
) { wifiNetwork, defaultConnections, isAirplaneMode ->
// The carrier merged connection should only be used if it's also the default
// connection or mobile connections aren't available because of airplane mode.
@@ -143,16 +154,12 @@
null
}
}
- .distinctUntilChanged()
- .logDiffsForTable(
- tableLogger,
- LOGGING_PREFIX,
- columnName = "carrierMergedSubId",
- initialValue = null,
- )
- .stateIn(scope, started = SharingStarted.WhileSubscribed(), null)
+ .also {
+ logDiffsForTable(it, tableLogger, LOGGING_PREFIX, columnName = "carrierMergedSubId")
+ }
+ }
- private val mobileSubscriptionsChangeEvent: Flow<Unit> =
+ private val mobileSubscriptionsChangeEvent: Events<Unit> = buildEvents {
conflatedCallbackFlow {
val callback =
object : SubscriptionManager.OnSubscriptionsChangedListener() {
@@ -161,18 +168,15 @@
trySend(Unit)
}
}
-
- subscriptionManager.addOnSubscriptionsChangedListener(
- bgDispatcher.asExecutor(),
- callback,
- )
-
+ subscriptionManager.addOnSubscriptionsChangedListener(Runnable::run, callback)
awaitClose { subscriptionManager.removeOnSubscriptionsChangedListener(callback) }
}
.flowOn(bgDispatcher)
+ .toEvents()
+ }
/** Turn ACTION_SERVICE_STATE (for subId = -1) into an event */
- private val serviceStateChangedEvent: Flow<Unit> =
+ private val serviceStateChangedEvent: Events<Unit> = buildEvents {
broadcastDispatcher
.broadcastFlow(IntentFilter(Intent.ACTION_SERVICE_STATE)) { intent, _ ->
val subId =
@@ -186,219 +190,235 @@
Unit
}
}
- // Emit on start so that we always check the state at least once
- .onStart { emit(Unit) }
+ .toEvents()
+ }
/** Eager flow to determine the device-based emergency calls only state */
- override val isDeviceEmergencyCallCapable: StateFlow<Boolean> =
- serviceStateChangedEvent
- .mapLatest {
- val modems = telephonyManager.activeModemCount
-
- // Assume false for automotive devices which don't have the calling feature.
- // TODO: b/398045526 to revisit the below.
- val isAutomotive: Boolean =
- context.packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
- val hasFeatureCalling: Boolean =
- context.packageManager.hasSystemFeature(
- PackageManager.FEATURE_TELEPHONY_CALLING
- )
- if (isAutomotive && !hasFeatureCalling) {
- return@mapLatest false
- }
-
- // Check the service state for every modem. If any state reports emergency calling
- // capable, then consider the device to have emergency call capabilities
- (0..<modems)
- .map { telephonyManager.getServiceStateForSlot(it) }
- .any { it?.isEmergencyOnly == true }
+ override val isDeviceEmergencyCallCapable: State<Boolean> = buildState {
+ rebuildOn(serviceStateChangedEvent) { asyncEvent { doAnyModemsSupportEmergencyCalls() } }
+ .switchEvents()
+ .holdState(false)
+ .also {
+ logDiffsForTable(
+ it,
+ tableLogger,
+ LOGGING_PREFIX,
+ columnName = "deviceEmergencyOnly",
+ )
}
- .flowOn(bgDispatcher)
- .distinctUntilChanged()
- .logDiffsForTable(
- tableLogger,
- columnPrefix = LOGGING_PREFIX,
- columnName = "deviceEmergencyOnly",
- initialValue = false,
- )
- .stateIn(scope, SharingStarted.Eagerly, false)
+ }
+
+ private suspend fun doAnyModemsSupportEmergencyCalls(): Boolean =
+ withContext(bgDispatcher) {
+ val modems = telephonyManager.activeModemCount
+
+ // Assume false for automotive devices which don't have the calling feature.
+ // TODO: b/398045526 to revisit the below.
+ val isAutomotive: Boolean =
+ context.packageManager.hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)
+ val hasFeatureCalling: Boolean =
+ context.packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY_CALLING)
+ if (isAutomotive && !hasFeatureCalling) {
+ return@withContext false
+ }
+
+ // Check the service state for every modem. If any state reports emergency calling
+ // capable, then consider the device to have emergency call capabilities
+ (0..<modems)
+ .map { telephonyManager.getServiceStateForSlot(it) }
+ .any { it?.isEmergencyOnly == true }
+ }
/**
* State flow that emits the set of mobile data subscriptions, each represented by its own
* [SubscriptionModel].
*/
- override val subscriptions: StateFlow<List<SubscriptionModel>> =
- merge(mobileSubscriptionsChangeEvent, carrierMergedSubId)
- .mapLatest { fetchSubscriptionsList().map { it.toSubscriptionModel() } }
- .onEach { infos -> updateRepos(infos) }
- .distinctUntilChanged()
- .logDiffsForTable(
- tableLogger,
- LOGGING_PREFIX,
- columnName = "subscriptions",
- initialValue = listOf(),
- )
- .stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf())
+ override val subscriptions: State<List<SubscriptionModel>> = buildState {
+ rebuildOn(mergeLeft(mobileSubscriptionsChangeEvent, carrierMergedSubId.changes)) {
+ asyncEvent { fetchSubscriptionModels() }
+ }
+ .switchEvents()
+ .holdState(emptyList())
+ .also {
+ logDiffsForTable(it, tableLogger, LOGGING_PREFIX, columnName = "subscriptions")
+ }
+ }
- override val activeMobileDataSubscriptionId: StateFlow<Int?> =
- conflatedCallbackFlow {
+ val subscriptionsById: State<Map<Int, SubscriptionModel>> =
+ subscriptions.map { subs -> subs.associateBy { it.subscriptionId } }
+
+ override val mobileConnectionsBySubId: Incremental<Int, MobileConnectionRepositoryKairos> =
+ buildIncremental {
+ subscriptionsById
+ .asIncremental()
+ .mapValues { (subId, sub) -> mobileRepoFactory.get().create(subId) }
+ .applyLatestSpecForKey()
+ }
+
+ private val telephonyManagerState: State<Pair<Int?, Set<Int>>> = buildState {
+ callbackFlow {
val callback =
- object : TelephonyCallback(), ActiveDataSubscriptionIdListener {
+ object :
+ TelephonyCallback(),
+ ActiveDataSubscriptionIdListener,
+ EmergencyCallbackModeListener {
override fun onActiveDataSubscriptionIdChanged(subId: Int) {
if (subId != INVALID_SUBSCRIPTION_ID) {
- trySend(subId)
+ trySend { (_, set): Pair<Int?, Set<Int>> -> subId to set }
} else {
- trySend(null)
+ trySend { (_, set): Pair<Int?, Set<Int>> -> null to set }
}
}
- }
- telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
+ override fun onCallbackModeStarted(
+ type: Int,
+ timerDuration: Duration,
+ subId: Int,
+ ) {
+ trySend { (id, set): Pair<Int?, Set<Int>> -> id to (set + type) }
+ }
+
+ override fun onCallbackModeRestarted(
+ type: Int,
+ timerDuration: Duration,
+ subId: Int,
+ ) {
+ // no-op
+ }
+
+ override fun onCallbackModeStopped(type: Int, reason: Int, subId: Int) {
+ trySend { (id, set): Pair<Int?, Set<Int>> -> id to (set - type) }
+ }
+ }
+ telephonyManager.registerTelephonyCallback(Runnable::run, callback)
awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
}
.flowOn(bgDispatcher)
- .distinctUntilChanged()
- .logDiffsForTable(
- tableLogger,
- LOGGING_PREFIX,
- columnName = "activeSubId",
- initialValue = null,
- )
- .stateIn(scope, started = SharingStarted.WhileSubscribed(), null)
+ .scanToState(null to emptySet())
+ }
- override val activeMobileDataRepository =
- activeMobileDataSubscriptionId
- .map { activeSubId ->
- if (activeSubId == null) {
- null
- } else {
- getOrCreateRepoForSubId(activeSubId)
+ override val activeMobileDataSubscriptionId: State<Int?> =
+ telephonyManagerState
+ .map { it.first }
+ .also {
+ onActivated {
+ logDiffsForTable(it, tableLogger, LOGGING_PREFIX, columnName = "activeSubId")
}
}
- .stateIn(scope, SharingStarted.WhileSubscribed(), null)
- override val defaultDataSubId: StateFlow<Int?> =
+ override val activeMobileDataRepository: State<MobileConnectionRepositoryKairos?> =
+ combine(activeMobileDataSubscriptionId, mobileConnectionsBySubId) { id, cache -> cache[id] }
+
+ override val defaultDataSubId: State<Int?> = buildState {
broadcastDispatcher
.broadcastFlow(
IntentFilter(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)
) { intent, _ ->
- val subId =
- intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID)
- if (subId == INVALID_SUBSCRIPTION_ID) {
- null
- } else {
- subId
- }
+ intent
+ .getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID)
+ .takeIf { it != INVALID_SUBSCRIPTION_ID }
}
- .distinctUntilChanged()
- .logDiffsForTable(
- tableLogger,
- LOGGING_PREFIX,
- columnName = "defaultSubId",
- initialValue = null,
- )
.onStart {
- val subId = subscriptionManagerProxy.getDefaultDataSubscriptionId()
- emit(if (subId == INVALID_SUBSCRIPTION_ID) null else subId)
+ emit(
+ subscriptionManagerProxy.getDefaultDataSubscriptionId().takeIf {
+ it != INVALID_SUBSCRIPTION_ID
+ }
+ )
}
- .stateIn(scope, SharingStarted.WhileSubscribed(), null)
+ .toState(initialValue = null)
+ .also { logDiffsForTable(it, tableLogger, LOGGING_PREFIX, columnName = "defaultSubId") }
+ }
- private val carrierConfigChangedEvent =
- broadcastDispatcher
- .broadcastFlow(IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED))
+ private val carrierConfigChangedEvent: Events<Unit> =
+ buildEvents {
+ broadcastDispatcher
+ .broadcastFlow(IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED))
+ .toEvents()
+ }
.onEach { logger.logActionCarrierConfigChanged() }
- override val defaultDataSubRatConfig: StateFlow<Config> =
- merge(defaultDataSubId, carrierConfigChangedEvent)
- .onStart { emit(Unit) }
- .mapLatest { Config.readConfig(context) }
- .distinctUntilChanged()
- .onEach { logger.logDefaultDataSubRatConfig(it) }
- .stateIn(
- scope,
- SharingStarted.WhileSubscribed(),
- initialValue = Config.readConfig(context),
- )
+ override val defaultDataSubRatConfig: State<Config> = buildState {
+ rebuildOn(mergeLeft(defaultDataSubId.changes, carrierConfigChangedEvent)) {
+ Config.readConfig(context).also { effect { logger.logDefaultDataSubRatConfig(it) } }
+ }
+ }
- override val defaultMobileIconMapping: Flow<Map<String, MobileIconGroup>> =
+ override val defaultMobileIconMapping: State<Map<String, MobileIconGroup>> = buildState {
defaultDataSubRatConfig
.map { mobileMappingsProxy.mapIconSets(it) }
- .distinctUntilChanged()
- .onEach { logger.logDefaultMobileIconMapping(it) }
+ .apply { observe { logger.logDefaultMobileIconMapping(it) } }
+ }
- override val defaultMobileIconGroup: Flow<MobileIconGroup> =
+ override val defaultMobileIconGroup: State<MobileIconGroup> = buildState {
defaultDataSubRatConfig
.map { mobileMappingsProxy.getDefaultIcons(it) }
- .distinctUntilChanged()
- .onEach { logger.logDefaultMobileIconGroup(it) }
+ .apply { observe { logger.logDefaultMobileIconGroup(it) } }
+ }
- override val isAnySimSecure: Flow<Boolean> =
+ override val isAnySimSecure: State<Boolean> = buildState {
conflatedCallbackFlow {
val callback =
object : KeyguardUpdateMonitorCallback() {
override fun onSimStateChanged(subId: Int, slotId: Int, simState: Int) {
logger.logOnSimStateChanged()
- trySend(getIsAnySimSecure())
+ trySend(keyguardUpdateMonitor.isSimPinSecure)
}
}
keyguardUpdateMonitor.registerCallback(callback)
- trySend(false)
awaitClose { keyguardUpdateMonitor.removeCallback(callback) }
}
.flowOn(mainDispatcher)
- .logDiffsForTable(
- tableLogger,
- LOGGING_PREFIX,
- columnName = "isAnySimSecure",
- initialValue = false,
- )
- .distinctUntilChanged()
+ .toState(false)
+ .also {
+ logDiffsForTable(it, tableLogger, LOGGING_PREFIX, columnName = "isAnySimSecure")
+ }
+ }
- override fun getIsAnySimSecure() = keyguardUpdateMonitor.isSimPinSecure
+ private val defaultConnections: State<DefaultConnectionModel> = buildState {
+ connectivityRepository.defaultConnections.toState()
+ }
- override fun getRepoForSubId(subId: Int): FullMobileConnectionRepository =
- getOrCreateRepoForSubId(subId)
-
- private fun getOrCreateRepoForSubId(subId: Int) =
- subIdRepositoryCache[subId]?.get()
- ?: createRepositoryForSubId(subId).also {
- subIdRepositoryCache[subId] = WeakReference(it)
+ override val mobileIsDefault: State<Boolean> =
+ defaultConnections
+ .map { it.mobile.isDefault }
+ .also {
+ onActivated {
+ logDiffsForTable(
+ it,
+ tableLogger,
+ columnPrefix = LOGGING_PREFIX,
+ columnName = "mobileIsDefault",
+ )
+ }
}
- override val mobileIsDefault: StateFlow<Boolean> =
- connectivityRepository.defaultConnections
- .map { it.mobile.isDefault }
- .distinctUntilChanged()
- .logDiffsForTable(
- tableLogger,
- columnPrefix = LOGGING_PREFIX,
- columnName = "mobileIsDefault",
- initialValue = false,
- )
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
-
- override val hasCarrierMergedConnection: StateFlow<Boolean> =
+ override val hasCarrierMergedConnection: State<Boolean> =
carrierMergedSubId
.map { it != null }
- .distinctUntilChanged()
- .logDiffsForTable(
- tableLogger,
- columnPrefix = LOGGING_PREFIX,
- columnName = "hasCarrierMergedConnection",
- initialValue = false,
- )
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+ .also {
+ onActivated {
+ logDiffsForTable(
+ it,
+ tableLogger,
+ columnPrefix = LOGGING_PREFIX,
+ columnName = "hasCarrierMergedConnection",
+ )
+ }
+ }
- override val defaultConnectionIsValidated: StateFlow<Boolean> =
- connectivityRepository.defaultConnections
+ override val defaultConnectionIsValidated: State<Boolean> =
+ defaultConnections
.map { it.isValidated }
- .distinctUntilChanged()
- .logDiffsForTable(
- tableLogger,
- columnName = "defaultConnectionIsValidated",
- initialValue = false,
- )
- .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+ .also {
+ onActivated {
+ logDiffsForTable(
+ it,
+ tableLogger,
+ columnPrefix = LOGGING_PREFIX,
+ columnName = "defaultConnectionIsValidated",
+ )
+ }
+ }
/**
* Flow that tracks the active mobile data subscriptions. Emits `true` whenever the active data
@@ -409,79 +429,45 @@
* TODO(b/265164432): we should probably expose all change events, not just same group
*/
@SuppressLint("MissingPermission")
- override val activeSubChangedInGroupEvent =
- activeMobileDataSubscriptionId
- .pairwise()
- .mapNotNull { (prevVal: Int?, newVal: Int?) ->
- if (prevVal == null || newVal == null) return@mapNotNull null
-
- val prevSub = subscriptionManager.getActiveSubscriptionInfo(prevVal)?.groupUuid
- val nextSub = subscriptionManager.getActiveSubscriptionInfo(newVal)?.groupUuid
-
- if (prevSub != null && prevSub == nextSub) Unit else null
+ override val activeSubChangedInGroupEvent: Events<Unit> = buildEvents {
+ activeMobileDataSubscriptionId.transitions
+ .mapNotNull { (prevVal, newVal) ->
+ prevVal?.let { newVal?.let { WithPrev(prevVal, newVal) } }
}
- .flowOn(bgDispatcher)
+ .mapAsyncLatest { (prevVal, newVal) ->
+ if (isActiveSubChangeInGroup(prevVal, newVal)) Unit else null
+ }
+ .filterNotNull()
+ }
- override suspend fun isInEcmMode(): Boolean {
- if (telephonyManager.emergencyCallbackMode) {
- return true
+ private suspend fun isActiveSubChangeInGroup(prevId: Int, newId: Int): Boolean =
+ withContext(bgDispatcher) {
+ val prevSub = subscriptionManager.getActiveSubscriptionInfo(prevId)?.groupUuid
+ val nextSub = subscriptionManager.getActiveSubscriptionInfo(newId)?.groupUuid
+ prevSub != null && prevSub == nextSub
}
- return with(subscriptions.value) {
- any { getOrCreateRepoForSubId(it.subscriptionId).isInEcmMode() }
- }
- }
- private fun isValidSubId(subId: Int): Boolean = checkSub(subId, subscriptions.value)
+ private val isInEcmModeTopLevel: State<Boolean> =
+ telephonyManagerState.map { it.second.isNotEmpty() }
- @VisibleForTesting fun getSubIdRepoCache() = subIdRepositoryCache
-
- private fun subscriptionModelForSubId(subId: Int): Flow<SubscriptionModel?> {
- return subscriptions.map { list ->
- list.firstOrNull { model -> model.subscriptionId == subId }
- }
- }
-
- private fun createRepositoryForSubId(subId: Int): FullMobileConnectionRepository {
- return fullMobileRepoFactory.build(
- subId,
- isCarrierMerged(subId),
- subscriptionModelForSubId(subId),
- defaultNetworkName,
- networkNameSeparator,
- )
- }
-
- private fun updateRepos(newInfos: List<SubscriptionModel>) {
- subIdRepositoryCache.forEach { (subId, repo) ->
- repo.get()?.setIsCarrierMerged(isCarrierMerged(subId))
- }
- }
-
- private fun isCarrierMerged(subId: Int): Boolean {
- return subId == carrierMergedSubId.value
- }
-
- /**
- * True if the checked subId is in the list of current subs or the active mobile data subId
- *
- * @param checkedSubs the list to validate [subId] against. To invalidate the cache, pass in the
- * new subscription list. Otherwise use [subscriptions.value] to validate a subId against the
- * current known subscriptions
- */
- private fun checkSub(subId: Int, checkedSubs: List<SubscriptionModel>): Boolean {
- if (activeMobileDataSubscriptionId.value == subId) return true
-
- checkedSubs.forEach {
- if (it.subscriptionId == subId) {
- return true
+ override val isInEcmMode: State<Boolean> =
+ isInEcmModeTopLevel.flatMap { isInEcm ->
+ if (isInEcm) {
+ stateOf(true)
+ } else {
+ mobileConnectionsBySubId.flatMap {
+ it.mapValues { it.value.isInEcmMode }.combine().map { it.values.any { it } }
+ }
}
}
- return false
- }
+ /** Determines which subId is currently carrier-merged. */
+ val carrierMergedSelector: StateSelector<Int?> = carrierMergedSubId.selector()
- private suspend fun fetchSubscriptionsList(): List<SubscriptionInfo> =
- withContext(bgDispatcher) { subscriptionManager.completeActiveSubscriptionInfoList }
+ private suspend fun fetchSubscriptionModels(): List<SubscriptionModel> =
+ withContext(bgDispatcher) {
+ subscriptionManager.completeActiveSubscriptionInfoList.map { it.toSubscriptionModel() }
+ }
private fun SubscriptionInfo.toSubscriptionModel(): SubscriptionModel =
SubscriptionModel(
@@ -493,23 +479,106 @@
profileClass = profileClass,
)
+ private var dumpCache: DumpCache? = null
+
+ private data class DumpCache(val repos: Map<Int, FullMobileConnectionRepositoryKairos>)
+
override fun dump(pw: PrintWriter, args: Array<String>) {
+ val cache = dumpCache ?: return
val ipw = IndentingPrintWriter(pw, " ")
ipw.println("Connection cache:")
ipw.increaseIndent()
- subIdRepositoryCache.entries.forEach { (subId, repo) ->
- ipw.println("$subId: ${repo.get()}")
- }
+ cache.repos.forEach { (subId, repo) -> ipw.println("$subId: $repo") }
ipw.decreaseIndent()
- ipw.println("Connections (${subIdRepositoryCache.size} total):")
+ ipw.println("Connections (${cache.repos.size} total):")
ipw.increaseIndent()
- subIdRepositoryCache.values.forEach { it.get()?.dump(ipw) }
+ cache.repos.values.forEach { it.dump(ipw) }
ipw.decreaseIndent()
}
+ fun interface ConnectionRepoFactory {
+ fun create(subId: Int): BuildSpec<MobileConnectionRepositoryKairos>
+ }
+
+ @dagger.Module
+ object Module {
+ @Provides
+ @ElementsIntoSet
+ fun kairosActivatable(
+ impl: Provider<MobileConnectionsRepositoryKairosImpl>
+ ): Set<@JvmSuppressWildcards KairosActivatable> =
+ if (Flags.statusBarMobileIconKairos()) setOf(impl.get()) else emptySet()
+ }
+
companion object {
private const val LOGGING_PREFIX = "Repo"
}
}
+
+@ExperimentalKairosApi
+class MobileConnectionRepositoryKairosFactoryImpl
+@Inject
+constructor(
+ context: Context,
+ private val connectionsRepo: MobileConnectionsRepositoryKairosImpl,
+ private val logFactory: TableLogBufferFactory,
+ private val carrierConfigRepo: CarrierConfigRepository,
+ private val telephonyManager: TelephonyManager,
+ private val mobileRepoFactory: MobileConnectionRepositoryKairosImpl.Factory,
+ private val mergedRepoFactory: CarrierMergedConnectionRepositoryKairos.Factory,
+) : MobileConnectionsRepositoryKairosImpl.ConnectionRepoFactory {
+
+ private val networkNameSeparator: String =
+ context.getString(R.string.status_bar_network_name_separator)
+
+ private val defaultNetworkName =
+ NetworkNameModel.Default(
+ context.getString(com.android.internal.R.string.lockscreen_carrier_default)
+ )
+
+ override fun create(subId: Int): BuildSpec<MobileConnectionRepositoryKairos> = buildSpec {
+ activated {
+ val mobileLogger =
+ logFactory.getOrCreate(tableBufferLogName(subId), MOBILE_CONNECTION_BUFFER_SIZE)
+ val mobileRepo = activated {
+ mobileRepoFactory.create(
+ subId,
+ mobileLogger,
+ connectionsRepo.subscriptionsById.map { subs -> subs[subId] },
+ defaultNetworkName,
+ networkNameSeparator,
+ carrierConfigRepo.getOrCreateConfigForSubId(subId),
+ telephonyManager.createForSubscriptionId(subId),
+ )
+ }
+ FullMobileConnectionRepositoryKairos(
+ subId = subId,
+ tableLogBuffer = mobileLogger,
+ mobileRepo = mobileRepo,
+ carrierMergedRepoSpec =
+ buildSpec {
+ activated { mergedRepoFactory.build(subId, mobileLogger, mobileRepo) }
+ },
+ isCarrierMerged = connectionsRepo.carrierMergedSelector[subId],
+ )
+ }
+ }
+
+ companion object {
+ /** The buffer size to use for logging. */
+ private const val MOBILE_CONNECTION_BUFFER_SIZE = 100
+
+ /** Returns a log buffer name for a mobile connection with the given [subId]. */
+ fun tableBufferLogName(subId: Int): String = "MobileConnectionLog[$subId]"
+ }
+
+ @dagger.Module
+ interface Module {
+ @Binds
+ fun bindImpl(
+ impl: MobileConnectionRepositoryKairosFactoryImpl
+ ): MobileConnectionsRepositoryKairosImpl.ConnectionRepoFactory
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/Producer.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/Producer.kt
new file mode 100644
index 0000000..a6209fa
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/Producer.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.kotlin
+
+/** Like a [javax.inject.Provider], but [get] is a `suspend fun`. */
+fun interface Producer<out T> {
+ suspend fun get(): T
+}
diff --git a/packages/SystemUI/tests/utils/src/android/net/ConnectivityManagerKosmos.kt b/packages/SystemUI/tests/utils/src/android/net/ConnectivityManagerKosmos.kt
new file mode 100644
index 0000000..516053d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/android/net/ConnectivityManagerKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mockFixture
+
+var Kosmos.connectivityManager: ConnectivityManager by mockFixture()
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kairos/CollectLastValue.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kairos/CollectLastValue.kt
new file mode 100644
index 0000000..927209f
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kairos/CollectLastValue.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.kairos
+
+import com.android.systemui.coroutines.collectLastValue
+import kotlin.properties.ReadOnlyProperty
+import kotlin.reflect.KProperty
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+
+/**
+ * Collect [state] in a new [Job] and return a getter for the collection of values collected.
+ *
+ * ```
+ * fun myTest() = runTest {
+ * // ...
+ * val values by collectValues(underTest.flow)
+ * assertThat(values).isEqualTo(listOf(expected1, expected2, ...))
+ * }
+ * ```
+ */
+@ExperimentalKairosApi
+fun <T> TestScope.collectLastValue(state: State<T>, kairosNetwork: KairosNetwork): KairosValue<T?> {
+ var value: T? = null
+ backgroundScope.launch { kairosNetwork.activateSpec { state.observe { value = it } } }
+ return KairosValueImpl {
+ runCurrent()
+ value
+ }
+}
+
+/**
+ * Collect [flow] in a new [Job] and return a getter for the collection of values collected.
+ *
+ * ```
+ * fun myTest() = runTest {
+ * // ...
+ * val values by collectValues(underTest.flow)
+ * assertThat(values).isEqualTo(listOf(expected1, expected2, ...))
+ * }
+ * ```
+ */
+@ExperimentalKairosApi
+fun <T> TestScope.collectLastValue(flow: Events<T>, kairosNetwork: KairosNetwork): KairosValue<T?> {
+ var value: T? = null
+ backgroundScope.launch { kairosNetwork.activateSpec { flow.observe { value = it } } }
+ return KairosValueImpl {
+ runCurrent()
+ value
+ }
+}
+
+/**
+ * Collect [flow] in a new [Job] and return a getter for the collection of values collected.
+ *
+ * ```
+ * fun myTest() = runTest {
+ * // ...
+ * val values by collectValues(underTest.flow)
+ * assertThat(values).isEqualTo(listOf(expected1, expected2, ...))
+ * }
+ * ```
+ */
+@ExperimentalKairosApi
+fun <T> TestScope.collectValues(
+ flow: Events<T>,
+ kairosNetwork: KairosNetwork,
+): KairosValue<List<T>> {
+ val values = mutableListOf<T>()
+ backgroundScope.launch { kairosNetwork.activateSpec { flow.observe { values.add(it) } } }
+ return KairosValueImpl {
+ runCurrent()
+ values.toList()
+ }
+}
+
+/** @see collectLastValue */
+interface KairosValue<T> : ReadOnlyProperty<Any?, T> {
+ val value: T
+}
+
+private class KairosValueImpl<T>(private val block: () -> T) : KairosValue<T> {
+ override val value: T
+ get() = block()
+
+ override fun getValue(thisRef: Any?, property: KProperty<*>): T = value
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kairos/KairosKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kairos/KairosKosmos.kt
new file mode 100644
index 0000000..d7f2041
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kairos/KairosKosmos.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.kairos
+
+import com.android.systemui.KairosActivatable
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+
+@ExperimentalKairosApi
+val Kosmos.kairos: KairosNetwork by Fixture { applicationCoroutineScope.launchKairosNetwork() }
+
+@ExperimentalKairosApi
+fun Kosmos.activateKairosActivatable(activatable: KairosActivatable) {
+ applicationCoroutineScope.launch { kairos.activateSpec { activatable.run { activate() } } }
+}
+
+@ExperimentalKairosApi
+fun <T : KairosActivatable> ActivatedKairosFixture(block: Kosmos.() -> T) = Fixture {
+ block().also { activateKairosActivatable(it) }
+}
+
+@ExperimentalKairosApi
+fun Kosmos.runKairosTest(timeout: Duration = 5.seconds, block: suspend KairosTestScope.() -> Unit) =
+ testScope.runTest(timeout) { KairosTestScopeImpl(this@runKairosTest, this, kairos).block() }
+
+@ExperimentalKairosApi
+interface KairosTestScope : Kosmos {
+ fun <T> State<T>.collectLastValue(): KairosValue<T?>
+
+ suspend fun <T> State<T>.sample(): T
+
+ fun <T : KairosActivatable> T.activated(): T
+}
+
+@ExperimentalKairosApi
+private class KairosTestScopeImpl(
+ kosmos: Kosmos,
+ val testScope: TestScope,
+ val kairos: KairosNetwork,
+) : KairosTestScope, Kosmos by kosmos {
+ override fun <T> State<T>.collectLastValue(): KairosValue<T?> =
+ testScope.collectLastValue(this@collectLastValue, kairos)
+
+ override suspend fun <T> State<T>.sample(): T = kairos.transact { sample() }
+
+ override fun <T : KairosActivatable> T.activated(): T =
+ this.also { activateKairosActivatable(it) }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepositoryKairos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepositoryKairos.kt
new file mode 100644
index 0000000..8cf3ee8
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepositoryKairos.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository
+
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosNetwork
+import com.android.systemui.kairos.MutableState
+import com.android.systemui.kairos.State
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState
+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.FakeMobileConnectionRepository.Companion.DEFAULT_NETWORK_NAME
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS
+import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
+
+@ExperimentalKairosApi
+class FakeMobileConnectionRepositoryKairos(
+ override val subId: Int,
+ kairos: KairosNetwork,
+ override val tableLogBuffer: TableLogBuffer,
+) : MobileConnectionRepositoryKairos {
+ override val carrierId = MutableState(kairos, 0)
+ override val inflateSignalStrength = MutableState(kairos, false)
+ override val allowNetworkSliceIndicator = MutableState(kairos, true)
+ override val isEmergencyOnly = MutableState(kairos, false)
+ override val isRoaming = MutableState(kairos, false)
+ override val operatorAlphaShort = MutableState<String?>(kairos, null)
+ override val isInService = MutableState(kairos, false)
+ override val isNonTerrestrial = MutableState(kairos, false)
+ override val isGsm = MutableState(kairos, false)
+ override val cdmaLevel = MutableState(kairos, 0)
+ override val primaryLevel = MutableState(kairos, 0)
+ override val satelliteLevel = MutableState(kairos, 0)
+ override val dataConnectionState = MutableState(kairos, DataConnectionState.Disconnected)
+ override val dataActivityDirection =
+ MutableState(kairos, DataActivityModel(hasActivityIn = false, hasActivityOut = false))
+ override val carrierNetworkChangeActive = MutableState(kairos, false)
+ override val resolvedNetworkType =
+ MutableState<ResolvedNetworkType>(kairos, ResolvedNetworkType.UnknownNetworkType)
+ override val numberOfLevels = MutableState(kairos, DEFAULT_NUM_LEVELS)
+ override val dataEnabled = MutableState(kairos, true)
+ override val cdmaRoaming = MutableState(kairos, false)
+ override val networkName =
+ MutableState<NetworkNameModel>(kairos, NetworkNameModel.Default(DEFAULT_NETWORK_NAME))
+ override val carrierName =
+ MutableState<NetworkNameModel>(kairos, NetworkNameModel.Default(DEFAULT_NETWORK_NAME))
+ override val isAllowedDuringAirplaneMode = MutableState(kairos, false)
+ override val hasPrioritizedNetworkCapabilities = MutableState(kairos, false)
+ override val isInEcmMode: State<Boolean> = MutableState(kairos, false)
+
+ /**
+ * Set [primaryLevel] and [cdmaLevel]. Convenient when you don't care about the connection type
+ */
+ fun setAllLevels(level: Int) {
+ cdmaLevel.setValue(level)
+ primaryLevel.setValue(level)
+ }
+
+ /** Set the correct [resolvedNetworkType] for the given group via its lookup key */
+ fun setNetworkTypeKey(key: String) {
+ resolvedNetworkType.setValue(ResolvedNetworkType.DefaultNetworkType(key))
+ }
+
+ /**
+ * Set both [isRoaming] and [cdmaRoaming] properties, in the event that you don't care about the
+ * connection type
+ */
+ fun setAllRoaming(roaming: Boolean) {
+ isRoaming.setValue(roaming)
+ cdmaRoaming.setValue(roaming)
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepositoryKairos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepositoryKairos.kt
new file mode 100644
index 0000000..624b2cc
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepositoryKairos.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository
+
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
+import android.telephony.TelephonyDisplayInfo
+import android.telephony.TelephonyManager
+import com.android.settingslib.SignalIcon
+import com.android.settingslib.mobile.MobileMappings
+import com.android.settingslib.mobile.TelephonyIcons
+import com.android.systemui.KairosBuilder
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosNetwork
+import com.android.systemui.kairos.MutableEvents
+import com.android.systemui.kairos.MutableState
+import com.android.systemui.kairos.State
+import com.android.systemui.kairos.asIncremental
+import com.android.systemui.kairos.buildSpec
+import com.android.systemui.kairos.combine
+import com.android.systemui.kairos.map
+import com.android.systemui.kairos.mapValues
+import com.android.systemui.kairosBuilder
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
+import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
+
+// TODO(b/261632894): remove this in favor of the real impl or DemoMobileConnectionsRepositoryKairos
+@ExperimentalKairosApi
+class FakeMobileConnectionsRepositoryKairos(
+ kairos: KairosNetwork,
+ val tableLogBuffer: TableLogBuffer,
+ mobileMappings: MobileMappingsProxy = FakeMobileMappingsProxy(),
+) : MobileConnectionsRepositoryKairos, KairosBuilder by kairosBuilder() {
+
+ val GSM_KEY = mobileMappings.toIconKey(GSM)
+ val LTE_KEY = mobileMappings.toIconKey(LTE)
+ val UMTS_KEY = mobileMappings.toIconKey(UMTS)
+ val LTE_ADVANCED_KEY = mobileMappings.toIconKeyOverride(LTE_ADVANCED_PRO)
+
+ /**
+ * To avoid a reliance on [MobileMappings], we'll build a simpler map from network type to
+ * mobile icon. See TelephonyManager.NETWORK_TYPES for a list of types and [TelephonyIcons] for
+ * the exhaustive set of icons
+ */
+ val TEST_MAPPING: Map<String, SignalIcon.MobileIconGroup> =
+ mapOf(
+ GSM_KEY to TelephonyIcons.THREE_G,
+ LTE_KEY to TelephonyIcons.LTE,
+ UMTS_KEY to TelephonyIcons.FOUR_G,
+ LTE_ADVANCED_KEY to TelephonyIcons.NR_5G,
+ )
+
+ override val subscriptions = MutableState(kairos, emptyList<SubscriptionModel>())
+
+ override val mobileConnectionsBySubId = buildIncremental {
+ subscriptions
+ .map { it.associate { sub -> sub.subscriptionId to Unit } }
+ .asIncremental()
+ .mapValues { (subId, _) ->
+ buildSpec {
+ FakeMobileConnectionRepositoryKairos(subId, kairosNetwork, tableLogBuffer)
+ }
+ }
+ .applyLatestSpecForKey()
+ }
+
+ private val _activeMobileDataSubscriptionId = MutableState<Int?>(kairos, null)
+ override val activeMobileDataSubscriptionId: State<Int?> = _activeMobileDataSubscriptionId
+
+ override val activeMobileDataRepository: State<MobileConnectionRepositoryKairos?> =
+ combine(mobileConnectionsBySubId, activeMobileDataSubscriptionId) { conns, activeSub ->
+ conns[activeSub]
+ }
+
+ override val activeSubChangedInGroupEvent = MutableEvents<Unit>(kairos)
+
+ override val defaultDataSubId = MutableState(kairos, INVALID_SUBSCRIPTION_ID)
+
+ override val mobileIsDefault = MutableState(kairos, false)
+
+ override val hasCarrierMergedConnection = MutableState(kairos, false)
+
+ override val defaultConnectionIsValidated = MutableState(kairos, false)
+
+ override val defaultDataSubRatConfig = MutableState(kairos, MobileMappings.Config())
+
+ override val defaultMobileIconMapping = MutableState(kairos, TEST_MAPPING)
+
+ override val defaultMobileIconGroup = MutableState(kairos, DEFAULT_ICON)
+
+ override val isDeviceEmergencyCallCapable = MutableState(kairos, false)
+
+ override val isAnySimSecure = MutableState(kairos, false)
+
+ override val isInEcmMode: State<Boolean> = MutableState(kairos, false)
+
+ fun setActiveMobileDataSubscriptionId(subId: Int) {
+ // Simulate the filtering that the repo does
+ if (subId == INVALID_SUBSCRIPTION_ID) {
+ _activeMobileDataSubscriptionId.setValue(null)
+ } else {
+ _activeMobileDataSubscriptionId.setValue(subId)
+ }
+ }
+
+ companion object {
+ val DEFAULT_ICON = TelephonyIcons.G
+
+ // Use [MobileMappings] to define some simple definitions
+ const val GSM = TelephonyManager.NETWORK_TYPE_GSM
+ const val LTE = TelephonyManager.NETWORK_TYPE_LTE
+ const val UMTS = TelephonyManager.NETWORK_TYPE_UMTS
+ const val LTE_ADVANCED_PRO = TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_LTE_ADVANCED_PRO
+ }
+}
+
+@ExperimentalKairosApi
+val MobileConnectionsRepositoryKairos.fake
+ get() = this as FakeMobileConnectionsRepositoryKairos
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileDataRepositoryKairosKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileDataRepositoryKairosKosmos.kt
new file mode 100644
index 0000000..f57cf99
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileDataRepositoryKairosKosmos.kt
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository
+
+import android.content.applicationContext
+import android.telephony.SubscriptionManager
+import android.telephony.telephonyManager
+import com.android.keyguard.keyguardUpdateMonitor
+import com.android.systemui.broadcast.broadcastDispatcher
+import com.android.systemui.demoModeController
+import com.android.systemui.dump.dumpManager
+import com.android.systemui.kairos.ActivatedKairosFixture
+import com.android.systemui.kairos.ExperimentalKairosApi
+import com.android.systemui.kairos.KairosNetwork
+import com.android.systemui.kairos.MutableEvents
+import com.android.systemui.kairos.buildSpec
+import com.android.systemui.kairos.kairos
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.logcatTableLogBuffer
+import com.android.systemui.log.table.tableLogBufferFactory
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.airplaneModeRepository
+import com.android.systemui.statusbar.pipeline.mobile.data.MobileInputLogger
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoMobileConnectionsRepositoryKairos
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoModeMobileConnectionDataSourceKairos
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.model.FakeNetworkEventModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileConnectionsRepositoryKairosImpl
+import com.android.systemui.statusbar.pipeline.mobile.util.FakeSubscriptionManagerProxy
+import com.android.systemui.statusbar.pipeline.mobile.util.SubscriptionManagerProxy
+import com.android.systemui.statusbar.pipeline.shared.data.repository.connectivityRepository
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.DemoModeWifiDataSource
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.wifiRepository
+import com.android.systemui.util.mockito.mockFixture
+import org.mockito.kotlin.mock
+
+@ExperimentalKairosApi
+var Kosmos.mobileConnectionsRepositoryKairos: MobileConnectionsRepositoryKairos by Fixture {
+ mobileRepositorySwitcherKairos
+}
+
+@ExperimentalKairosApi
+val Kosmos.fakeMobileConnectionsRepositoryKairos by ActivatedKairosFixture {
+ FakeMobileConnectionsRepositoryKairos(kairos, logcatTableLogBuffer(this), mobileMappingsProxy)
+}
+
+@ExperimentalKairosApi
+val Kosmos.demoMobileConnectionsRepositoryKairos by ActivatedKairosFixture {
+ DemoMobileConnectionsRepositoryKairos(
+ mobileDataSource = demoModeMobileConnectionDataSourceKairos,
+ wifiDataSource = wifiDataSource,
+ context = applicationContext,
+ logFactory = tableLogBufferFactory,
+ )
+}
+
+@ExperimentalKairosApi
+val Kosmos.demoModeMobileConnectionDataSourceKairos:
+ DemoModeMobileConnectionDataSourceKairos by Fixture {
+ FakeDemoModeMobileConnectionDataSourceKairos(kairos)
+}
+
+val Kosmos.wifiDataSource: DemoModeWifiDataSource by mockFixture()
+
+@ExperimentalKairosApi
+class FakeDemoModeMobileConnectionDataSourceKairos(kairos: KairosNetwork) :
+ DemoModeMobileConnectionDataSourceKairos {
+ override val mobileEvents = MutableEvents<FakeNetworkEventModel?>(kairos)
+}
+
+@ExperimentalKairosApi
+val DemoModeMobileConnectionDataSourceKairos.fake
+ get() = this as FakeDemoModeMobileConnectionDataSourceKairos
+
+@ExperimentalKairosApi
+val Kosmos.mobileRepositorySwitcherKairos:
+ MobileRepositorySwitcherKairos by ActivatedKairosFixture {
+ MobileRepositorySwitcherKairos(
+ realRepository = mobileConnectionsRepositoryKairosImpl,
+ demoRepositoryFactory = demoMobileConnectionsRepositoryKairosFactory,
+ demoModeController = demoModeController,
+ )
+}
+
+@ExperimentalKairosApi
+val Kosmos.demoMobileConnectionsRepositoryKairosFactory:
+ DemoMobileConnectionsRepositoryKairos.Factory by Fixture {
+ DemoMobileConnectionsRepositoryKairos.Factory {
+ DemoMobileConnectionsRepositoryKairos(
+ mobileDataSource = demoModeMobileConnectionDataSourceKairos,
+ wifiDataSource = wifiDataSource,
+ context = applicationContext,
+ logFactory = tableLogBufferFactory,
+ )
+ }
+}
+
+@ExperimentalKairosApi
+val Kosmos.mobileConnectionsRepositoryKairosImpl:
+ MobileConnectionsRepositoryKairosImpl by ActivatedKairosFixture {
+ MobileConnectionsRepositoryKairosImpl(
+ connectivityRepository = connectivityRepository,
+ subscriptionManager = subscriptionManager,
+ subscriptionManagerProxy = subscriptionManagerProxy,
+ telephonyManager = telephonyManager,
+ logger = mobileInputLogger,
+ tableLogger = summaryLogger,
+ mobileMappingsProxy = mobileMappingsProxy,
+ broadcastDispatcher = broadcastDispatcher,
+ context = applicationContext,
+ bgDispatcher = testDispatcher,
+ mainDispatcher = testDispatcher,
+ airplaneModeRepository = airplaneModeRepository,
+ wifiRepository = wifiRepository,
+ keyguardUpdateMonitor = keyguardUpdateMonitor,
+ dumpManager = dumpManager,
+ mobileRepoFactory = { mobileConnectionRepositoryKairosFactory },
+ )
+}
+
+val Kosmos.subscriptionManager: SubscriptionManager by mockFixture()
+val Kosmos.mobileInputLogger: MobileInputLogger by mockFixture()
+val Kosmos.summaryLogger: TableLogBuffer by Fixture { logcatTableLogBuffer(this, "summaryLogger") }
+
+@ExperimentalKairosApi
+val Kosmos.mobileConnectionRepositoryKairosFactory by Fixture {
+ MobileConnectionsRepositoryKairosImpl.ConnectionRepoFactory { subId ->
+ buildSpec { FakeMobileConnectionRepositoryKairos(subId, kairos, mock()) }
+ }
+}
+
+val Kosmos.subscriptionManagerProxy: SubscriptionManagerProxy by Fixture {
+ FakeSubscriptionManagerProxy()
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeSubscriptionManagerProxy.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeSubscriptionManagerProxy.kt
index 5b70907..a391c44 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeSubscriptionManagerProxy.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeSubscriptionManagerProxy.kt
@@ -41,3 +41,6 @@
SubscriptionInfo.Builder().setId(subId).setEmbedded(isEmbedded).build()
}
}
+
+val SubscriptionManagerProxy.fake
+ get() = this as FakeSubscriptionManagerProxy
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryKosmos.kt
index 00bfa99..bb254a1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryKosmos.kt
@@ -18,5 +18,5 @@
import com.android.systemui.kosmos.Kosmos
-val Kosmos.connectivityRepository: ConnectivityRepository by
+var Kosmos.connectivityRepository: ConnectivityRepository by
Kosmos.Fixture { FakeConnectivityRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryKosmos.kt
index e44061a..f560c50 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryKosmos.kt
@@ -19,4 +19,4 @@
import com.android.systemui.kosmos.Kosmos
val Kosmos.fakeWifiRepository: FakeWifiRepository by Kosmos.Fixture { FakeWifiRepository() }
-val Kosmos.wifiRepository: WifiRepository by Kosmos.Fixture { fakeWifiRepository }
+var Kosmos.wifiRepository: WifiRepository by Kosmos.Fixture { fakeWifiRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/MockitoKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/MockitoKosmos.kt
new file mode 100644
index 0000000..1638cb7
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/MockitoKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.mockito
+
+import com.android.systemui.kosmos.Kosmos.Fixture
+import org.mockito.kotlin.mock
+
+inline fun <reified T> mockFixture(): Fixture<T> = Fixture { mock() }