diff options
35 files changed, 5082 insertions, 5269 deletions
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index b53cb27dd73b..5b48566d92f9 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -727,6 +727,7 @@ android_library { "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 f82de7a783c1..80f4b2ce7b10 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 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository 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) - } - wifiDataSource = - mock<DemoModeWifiDataSource>().also { - whenever(it.wifiEvents).thenReturn(MutableStateFlow(null)) + private val kosmos = + testKosmos().apply { + useUnconfinedTestDispatcher() + demoModeController.stub { + // Never start in demo mode + on { isInDemoMode } doReturn false } - wifiRepository = FakeWifiRepository() - - connectivityRepository = FakeConnectivityRepository() - - realRepo = - MobileConnectionsRepositoryImpl( - connectivityRepository, - subscriptionManager, - subscriptionManagerProxy, - telephonyManager, - logger, - summaryLogger, - mobileMappings, - fakeBroadcastDispatcher, - context, - /* bgDispatcher = */ IMMEDIATE, - scope, - /* mainDispatcher = */ IMMEDIATE, - FakeAirplaneModeRepository(), - wifiRepository, - mock(), - mock(), - mock(), - ) + wifiDataSource.stub { on { wifiEvents } doReturn MutableStateFlow(null) } + } - demoRepo = - DemoMobileConnectionsRepository( - mobileDataSource = mobileDataSource, - wifiDataSource = wifiDataSource, - scope = scope, - context = context, - logFactory = kosmos.tableLogBufferFactory, - ) + private val Kosmos.underTest + get() = mobileRepositorySwitcherKairos - underTest = - MobileRepositorySwitcherKairos( - scope = scope, - realRepository = realRepo, - demoMobileConnectionsRepository = demoRepo, - demoModeController = demoModeController, - ) - } + private val Kosmos.realRepo + get() = mobileConnectionsRepositoryKairosImpl - @After - fun tearDown() { - scope.cancel() - } + private fun runTest(block: suspend KairosTestScope.() -> Unit) = + kosmos.run { runKairosTest { block() } } @Test - fun activeRepoMatchesDemoModeSetting() = - runBlocking(IMMEDIATE) { - whenever(demoModeController.isInDemoMode).thenReturn(false) + fun activeRepoMatchesDemoModeSetting() = runTest { + demoModeController.stub { on { isInDemoMode } doReturn false } - var latest: MobileConnectionsRepository? = null - val job = underTest.activeRepo.onEach { latest = it }.launchIn(this) + val latest by underTest.activeRepo.collectLastValue() - assertThat(latest).isEqualTo(realRepo) + assertThat(latest).isEqualTo(realRepo) - startDemoMode() + startDemoMode() - assertThat(latest).isEqualTo(demoRepo) + assertThat(latest).isInstanceOf(DemoMobileConnectionsRepositoryKairos::class.java) - finishDemoMode() + finishDemoMode() - assertThat(latest).isEqualTo(realRepo) - - job.cancel() - } + assertThat(latest).isEqualTo(realRepo) + } @Test - fun subscriptionListUpdatesWhenDemoModeChanges() = - runBlocking(IMMEDIATE) { - whenever(demoModeController.isInDemoMode).thenReturn(false) - - whenever(subscriptionManager.completeActiveSubscriptionInfoList) - .thenReturn(listOf(SUB_1, SUB_2)) + fun subscriptionListUpdatesWhenDemoModeChanges() = runTest { + demoModeController.stub { on { isInDemoMode } doReturn false } - var latest: List<SubscriptionModel>? = null - val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) + subscriptionManager.stub { + on { completeActiveSubscriptionInfoList } doReturn listOf(SUB_1, SUB_2) + } - // The real subscriptions has 2 subs - whenever(subscriptionManager.completeActiveSubscriptionInfoList) - .thenReturn(listOf(SUB_1, SUB_2)) - getSubscriptionCallback().onSubscriptionsChanged() + val latest by underTest.subscriptions.collectLastValue() - assertThat(latest).isEqualTo(listOf(MODEL_1, MODEL_2)) + // The real subscriptions has 2 subs + getSubscriptionCallback().onSubscriptionsChanged() - // Demo mode turns on, and we should see only the demo subscriptions - startDemoMode() - fakeNetworkEventsFlow.value = 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!![0].subscriptionId).isEqualTo(3) + // Demo mode turns on, and we should see only the demo subscriptions + startDemoMode() + demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(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!!.first().subscriptionId).isEqualTo(3) - assertThat(latest).isEqualTo(listOf(MODEL_1, MODEL_2)) + finishDemoMode() - job.cancel() - } + assertThat(latest).isEqualTo(listOf(MODEL_1, MODEL_2)) + } - 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 @@ class MobileRepositorySwitcherKairosTest : SysuiTestCase() { 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 d2cd22734d8e..99cc93d6dc30 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 androidx.test.filters.SmallTest 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 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() - } + private val kosmos = + testKosmos().apply { + useUnconfinedTestDispatcher() + wifiDataSource.stub { on { wifiEvents } doReturn fakeWifiEventFlow } + } - @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 95cdee0648a3..503d561a2234 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 android.telephony.TelephonyManager.DATA_ACTIVITY_NONE 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 - - @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) - } - - underTest = - DemoMobileConnectionsRepositoryKairos( - mobileDataSource = mobileDataSource, - wifiDataSource = wifiDataSource, - scope = testScope.backgroundScope, - context = context, - logFactory = kosmos.tableLogBufferFactory, - ) + private val kosmos = + testKosmos().apply { + useUnconfinedTestDispatcher() + wifiDataSource.stub { on { wifiEvents } doReturn fakeWifiEventFlow } + } - underTest.startProcessingCommands() - } + private fun runTest(block: suspend KairosTestScope.() -> Unit) = + kosmos.run { runKairosTest { block() } } @Test - fun isDefault_defaultsToTrue() = - testScope.runTest { - val isDefault = underTest.mobileIsDefault.value - assertThat(isDefault).isTrue() - } + fun isDefault_defaultsToTrue() = runTest { + underTest + val isDefault = kairos.transact { underTest.mobileIsDefault.sample() } + assertThat(isDefault).isTrue() + } @Test - fun validated_defaultsToTrue() = - testScope.runTest { - val isValidated = underTest.defaultConnectionIsValidated.value - assertThat(isValidated).isTrue() - } + fun validated_defaultsToTrue() = runTest { + underTest + val isValidated = kairos.transact { underTest.defaultConnectionIsValidated.sample() } + assertThat(isValidated).isTrue() + } @Test - fun networkEvent_createNewSubscription() = - testScope.runTest { - var latest: List<SubscriptionModel>? = null - val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) + fun networkEvent_createNewSubscription() = runTest { + val latest by underTest.subscriptions.collectLastValue() - assertThat(latest).isEmpty() + assertThat(latest).isEmpty() - fakeNetworkEventFlow.value = validMobileEvent(subId = 1) + demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(validMobileEvent(subId = 1)) - assertThat(latest).hasSize(1) - assertThat(latest!![0].subscriptionId).isEqualTo(1) - - job.cancel() - } + assertThat(latest).hasSize(1) + assertThat(latest!!.first().subscriptionId).isEqualTo(1) + } @Test - fun wifiCarrierMergedEvent_createNewSubscription() = - testScope.runTest { - var latest: List<SubscriptionModel>? = null - val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) + fun wifiCarrierMergedEvent_createNewSubscription() = runTest { + val latest by underTest.subscriptions.collectLastValue() - assertThat(latest).isEmpty() + assertThat(latest).isEmpty() - fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5) + fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5) - assertThat(latest).hasSize(1) - assertThat(latest!![0].subscriptionId).isEqualTo(5) - - job.cancel() - } + assertThat(latest).hasSize(1) + assertThat(latest!!.first().subscriptionId).isEqualTo(5) + } @Test - fun networkEvent_reusesSubscriptionWhenSameId() = - testScope.runTest { - var latest: List<SubscriptionModel>? = null - val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) - - assertThat(latest).isEmpty() + fun networkEvent_reusesSubscriptionWhenSameId() = runTest { + val latest by underTest.subscriptions.collectLastValue() - fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 1) + assertThat(latest).isEmpty() - assertThat(latest).hasSize(1) - assertThat(latest!![0].subscriptionId).isEqualTo(1) + demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit( + validMobileEvent(subId = 1, level = 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!!.first().subscriptionId).isEqualTo(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 + demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit( + validMobileEvent(subId = 1, level = 2) + ) - job.cancel() - } + assertThat(latest).hasSize(1) + assertThat(latest!!.first().subscriptionId).isEqualTo(1) + } @Test - fun wifiCarrierMergedEvent_reusesSubscriptionWhenSameId() = - testScope.runTest { - var latest: List<SubscriptionModel>? = null - val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) - - assertThat(latest).isEmpty() + fun wifiCarrierMergedEvent_reusesSubscriptionWhenSameId() = runTest { + val latest by underTest.subscriptions.collectLastValue() - fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5, level = 1) + assertThat(latest).isEmpty() - assertThat(latest).hasSize(1) - assertThat(latest!![0].subscriptionId).isEqualTo(5) + fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5, level = 1) - // 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) - 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) - job.cancel() - } + assertThat(latest).hasSize(1) + assertThat(latest!!.first().subscriptionId).isEqualTo(5) + } @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) + fun multipleSubscriptions() = runTest { + val latest by underTest.subscriptions.collectLastValue() - assertThat(latest).hasSize(2) + demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(validMobileEvent(subId = 1)) + demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(validMobileEvent(subId = 2)) - job.cancel() - } + assertThat(latest).hasSize(2) + } @Test - fun mobileSubscriptionAndCarrierMergedSubscription() = - testScope.runTest { - var latest: List<SubscriptionModel>? = null - val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) + fun mobileSubscriptionAndCarrierMergedSubscription() = runTest { + val latest by underTest.subscriptions.collectLastValue() - fakeNetworkEventFlow.value = validMobileEvent(subId = 1) - fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5) + demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(validMobileEvent(subId = 1)) + fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 5) - assertThat(latest).hasSize(2) - - job.cancel() - } + assertThat(latest).hasSize(2) + } @Test - fun multipleMobileSubscriptionsAndCarrierMergedSubscription() = - testScope.runTest { - var latest: List<SubscriptionModel>? = null - val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) + fun multipleMobileSubscriptionsAndCarrierMergedSubscription() = runTest { + val latest by underTest.subscriptions.collectLastValue() - fakeNetworkEventFlow.value = validMobileEvent(subId = 1) - fakeNetworkEventFlow.value = validMobileEvent(subId = 2) - fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 3) + demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(validMobileEvent(subId = 1)) + demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(validMobileEvent(subId = 2)) + fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 3) - assertThat(latest).hasSize(3) - - job.cancel() - } + assertThat(latest).hasSize(3) + } @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) + fun mobileDisabledEvent_disablesConnection_subIdSpecified_singleConn() = runTest { + val latest by underTest.subscriptions.collectLastValue() - fakeNetworkEventFlow.value = MobileDisabled(subId = 1) + demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit( + validMobileEvent(subId = 1, level = 1) + ) - assertThat(latest).hasSize(0) + demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(MobileDisabled(subId = 1)) - job.cancel() - } + assertThat(latest).hasSize(0) + } @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) + fun mobileDisabledEvent_disablesConnection_subIdNotSpecified_singleConn() = runTest { + val latest by underTest.subscriptions.collectLastValue() - fakeNetworkEventFlow.value = MobileDisabled(subId = null) + demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit( + validMobileEvent(subId = 1, level = 1) + ) - assertThat(latest).hasSize(0) + demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit( + MobileDisabled(subId = null) + ) - job.cancel() - } + assertThat(latest).hasSize(0) + } @Test - fun mobileDisabledEvent_disablesConnection_subIdSpecified_multipleConn() = - testScope.runTest { - var latest: List<SubscriptionModel>? = null - val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) + fun mobileDisabledEvent_disablesConnection_subIdSpecified_multipleConn() = runTest { + val latest by underTest.subscriptions.collectLastValue() - fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 1) - fakeNetworkEventFlow.value = validMobileEvent(subId = 2, level = 1) + demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit( + validMobileEvent(subId = 1, level = 1) + ) - fakeNetworkEventFlow.value = MobileDisabled(subId = 2) + assertThat(latest).hasSize(1) - assertThat(latest).hasSize(1) + demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit( + validMobileEvent(subId = 2, level = 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) + assertThat(latest).hasSize(2) - fakeNetworkEventFlow.value = validMobileEvent(subId = 1, level = 1) - fakeNetworkEventFlow.value = validMobileEvent(subId = 2, level = 1) + demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(MobileDisabled(subId = 2)) - fakeNetworkEventFlow.value = MobileDisabled(subId = null) - - assertThat(latest).hasSize(2) - - job.cancel() - } + assertThat(latest).hasSize(1) + } @Test - fun wifiNetworkUpdatesToDisabled_carrierMergedConnectionRemoved() = - testScope.runTest { - var latest: List<SubscriptionModel>? = null - val job = underTest.subscriptions.onEach { latest = it }.launchIn(this) + 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) + } - fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 1) + @Test + fun wifiNetworkUpdatesToDisabled_carrierMergedConnectionRemoved() = runTest { + val latest by underTest.subscriptions.collectLastValue() - assertThat(latest).hasSize(1) + fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 1) - fakeWifiEventFlow.value = FakeWifiEventModel.WifiDisabled + assertThat(latest).hasSize(1) - assertThat(latest).isEmpty() + fakeWifiEventFlow.value = FakeWifiEventModel.WifiDisabled - job.cancel() - } + assertThat(latest).isEmpty() + } @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) + fun wifiNetworkUpdatesToActive_carrierMergedConnectionRemoved() = runTest { + val latest by underTest.subscriptions.collectLastValue() - fakeWifiEventFlow.value = - FakeWifiEventModel.Wifi(level = 1, activity = 0, ssid = null, validated = true) + fakeWifiEventFlow.value = validCarrierMergedEvent(subId = 1) - assertThat(latest).isEmpty() + assertThat(latest).hasSize(1) - job.cancel() - } + fakeWifiEventFlow.value = + FakeWifiEventModel.Wifi(level = 1, activity = 0, ssid = null, validated = true) - @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() - } + assertThat(latest).isEmpty() + } @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() - } + 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) + } - /** 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() - } + 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() = - 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 + fun demoConnection_singleSubscription() = runTest { + var currentEvent: FakeNetworkEventModel = validMobileEvent(subId = 1) + val connections by underTest.mobileConnectionsBySubId.map { it.values }.collectLastValue() - assertThat(connections).hasSize(1) - val connection1 = connections!![0] + demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(currentEvent) - assertConnection(connection1, currentEvent) + assertThat(connections).hasSize(1) + val connection1 = connections!!.first() - // Exercise the whole api + assertConnection(connection1, currentEvent) - currentEvent = validMobileEvent(subId = 1, level = 2) - fakeNetworkEventFlow.value = currentEvent - assertConnection(connection1, currentEvent) + // Exercise the whole api - job.cancel() - } + currentEvent = validMobileEvent(subId = 1, level = 2) + demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(currentEvent) + assertConnection(connection1, currentEvent) + } @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") - } + 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) - fakeNetworkEventFlow.value = currentEvent2 - assertConnection(connection1!!, currentEvent1) - assertConnection(connection2!!, 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) + // 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) - job.cancel() - } + // 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() = - 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") - } + 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) + assertConnection(connection1!!, currentEvent1) + assertCarrierMergedConnection(connection2!!, currentEvent2) - // and vice versa - currentEvent1 = validMobileEvent(subId = 1, inflateStrength = true) - fakeNetworkEventFlow.value = currentEvent1 - 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) - job.cancel() - } + // and vice versa + currentEvent1 = validMobileEvent(subId = 1, inflateStrength = true) + demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(currentEvent1) + assertConnection(connection1!!, currentEvent1) + assertCarrierMergedConnection(connection2!!, currentEvent2) + } @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 demoIsNotInEcmState() = runTest { + underTest + assertThat(kairos.transact { underTest.isInEcmMode.sample() }).isFalse() } - private fun TestScope.assertConnection( - conn: DemoMobileConnectionRepository, + 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) - - // 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) + 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.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 93d56d5aa1f5..1838d13b793a 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 android.telephony.TelephonyManager 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 lateinit var wifiRepository: FakeWifiRepository - @Mock private lateinit var logger: TableLogBuffer - @Mock private lateinit var telephonyManager: TelephonyManager - - private val testDispatcher = UnconfinedTestDispatcher() - private val testScope = TestScope(testDispatcher) - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - whenever(telephonyManager.subscriptionId).thenReturn(SUB_ID) - whenever(telephonyManager.simOperatorName).thenReturn("") - - wifiRepository = FakeWifiRepository() - - underTest = - CarrierMergedConnectionRepositoryKairos( - SUB_ID, - logger, - telephonyManager, - testScope.backgroundScope.coroutineContext, - testScope.backgroundScope, - wifiRepository, - ) + private val Kosmos.underTest by ActivatedKairosFixture { + CarrierMergedConnectionRepositoryKairos( + subId = SUB_ID, + tableLogBuffer = logcatTableLogBuffer(this), + telephonyManager = telephonyManager, + wifiRepository = wifiRepository, + isInEcmMode = stateOf(false), + ) } - @Test - fun inactiveWifi_isDefault() = - testScope.runTest { - var latestConnState: DataConnectionState? = null - var latestNetType: ResolvedNetworkType? = null - - val dataJob = - underTest.dataConnectionState.onEach { latestConnState = it }.launchIn(this) - val netJob = underTest.resolvedNetworkType.onEach { latestNetType = it }.launchIn(this) - - wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive()) - - assertThat(latestConnState).isEqualTo(DataConnectionState.Disconnected) - assertThat(latestNetType).isNotEqualTo(ResolvedNetworkType.CarrierMergedNetworkType) + private val Kosmos.telephonyManager: TelephonyManager by Fixture { + mock { + on { subscriptionId } doReturn SUB_ID + on { simOperatorName } doReturn "" + } + } - dataJob.cancel() - netJob.cancel() + private fun runTest(block: suspend KairosTestScope.() -> Unit) = + testKosmos().run { + useUnconfinedTestDispatcher() + runKairosTest { block() } } @Test - fun activeWifi_isDefault() = - testScope.runTest { - var latestConnState: DataConnectionState? = null - var latestNetType: ResolvedNetworkType? = null + fun inactiveWifi_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.Inactive()) - wifiRepository.setWifiNetwork(WifiNetworkModel.Active.of(level = 1)) - - 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 carrierMergedWifi_isValidAndFieldsComeFromWifiNetwork() = - testScope.runTest { - var latest: Int? = null - val job = underTest.primaryLevel.onEach { latest = it }.launchIn(this) - - wifiRepository.setIsWifiEnabled(true) - wifiRepository.setIsWifiDefault(true) - - wifiRepository.setWifiNetwork( - WifiNetworkModel.CarrierMerged.of(subscriptionId = SUB_ID, level = 3) - ) + fun activeWifi_isDefault() = runTest { + val latestConnState by underTest.dataConnectionState.collectLastValue() + val latestNetType by underTest.resolvedNetworkType.collectLastValue() - assertThat(latest).isEqualTo(3) + fakeWifiRepository.setWifiNetwork(WifiNetworkModel.Active.of(level = 1)) - job.cancel() - } + assertThat(latestConnState).isEqualTo(DataConnectionState.Disconnected) + assertThat(latestNetType).isNotEqualTo(ResolvedNetworkType.CarrierMergedNetworkType) + } @Test - fun activity_comesFromWifiActivity() = - testScope.runTest { - var latest: DataActivityModel? = null - val job = underTest.dataActivityDirection.onEach { latest = it }.launchIn(this) - - wifiRepository.setIsWifiEnabled(true) - wifiRepository.setIsWifiDefault(true) - wifiRepository.setWifiNetwork( - WifiNetworkModel.CarrierMerged.of(subscriptionId = SUB_ID, level = 3) - ) - wifiRepository.setWifiActivity( - DataActivityModel(hasActivityIn = true, hasActivityOut = false) - ) + fun carrierMergedWifi_isValidAndFieldsComeFromWifiNetwork() = runTest { + val latest by underTest.primaryLevel.collectLastValue() - assertThat(latest!!.hasActivityIn).isTrue() - assertThat(latest!!.hasActivityOut).isFalse() + fakeWifiRepository.setIsWifiEnabled(true) + fakeWifiRepository.setIsWifiDefault(true) - wifiRepository.setWifiActivity( - DataActivityModel(hasActivityIn = false, hasActivityOut = true) - ) + fakeWifiRepository.setWifiNetwork( + WifiNetworkModel.CarrierMerged.of(subscriptionId = SUB_ID, level = 3) + ) - assertThat(latest!!.hasActivityIn).isFalse() - assertThat(latest!!.hasActivityOut).isTrue() + assertThat(latest).isEqualTo(3) + } - job.cancel() - } + @Test + fun activity_comesFromWifiActivity() = runTest { + val latest by underTest.dataActivityDirection.collectLastValue() + + fakeWifiRepository.setIsWifiEnabled(true) + fakeWifiRepository.setIsWifiDefault(true) + fakeWifiRepository.setWifiNetwork( + WifiNetworkModel.CarrierMerged.of(subscriptionId = SUB_ID, level = 3) + ) + fakeWifiRepository.setWifiActivity( + DataActivityModel(hasActivityIn = true, hasActivityOut = false) + ) + + assertThat(latest!!.hasActivityIn).isTrue() + assertThat(latest!!.hasActivityOut).isFalse() + + fakeWifiRepository.setWifiActivity( + DataActivityModel(hasActivityIn = false, hasActivityOut = true) + ) + + assertThat(latest!!.hasActivityIn).isFalse() + assertThat(latest!!.hasActivityOut).isTrue() + } @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) - ) + fun carrierMergedWifi_wrongSubId_isDefault() = runTest { + val latestLevel by underTest.primaryLevel.collectLastValue() + val latestType by underTest.resolvedNetworkType.collectLastValue() - assertThat(latestLevel).isNotEqualTo(3) - assertThat(latestType).isNotEqualTo(ResolvedNetworkType.CarrierMergedNetworkType) + fakeWifiRepository.setWifiNetwork( + WifiNetworkModel.CarrierMerged.of(subscriptionId = SUB_ID + 10, level = 3) + ) - 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) - - wifiRepository.setWifiNetwork( - WifiNetworkModel.CarrierMerged.of(subscriptionId = SUB_ID, level = 3) - ) - wifiRepository.setIsWifiEnabled(false) + fun carrierMergedButNotEnabled_isDefault() = runTest { + val latest by underTest.primaryLevel.collectLastValue() - assertThat(latest).isNotEqualTo(3) + fakeWifiRepository.setWifiNetwork( + WifiNetworkModel.CarrierMerged.of(subscriptionId = SUB_ID, level = 3) + ) + fakeWifiRepository.setIsWifiEnabled(false) - 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) - - wifiRepository.setWifiNetwork( - WifiNetworkModel.CarrierMerged.of(subscriptionId = SUB_ID, level = 3) - ) - wifiRepository.setIsWifiDefault(false) + fun carrierMergedButWifiNotDefault_isDefault() = runTest { + val latest by underTest.primaryLevel.collectLastValue() - assertThat(latest).isNotEqualTo(3) + fakeWifiRepository.setWifiNetwork( + WifiNetworkModel.CarrierMerged.of(subscriptionId = SUB_ID, level = 3) + ) + fakeWifiRepository.setIsWifiDefault(false) - job.cancel() - } + assertThat(latest).isNotEqualTo(3) + } @Test - fun numberOfLevels_comesFromCarrierMerged() = - testScope.runTest { - var latest: Int? = null - val job = underTest.numberOfLevels.onEach { latest = it }.launchIn(this) - - wifiRepository.setWifiNetwork( - WifiNetworkModel.CarrierMerged.of( - subscriptionId = SUB_ID, - level = 1, - numberOfLevels = 6, - ) + fun numberOfLevels_comesFromCarrierMerged() = runTest { + val latest by underTest.numberOfLevels.collectLastValue() + + fakeWifiRepository.setWifiNetwork( + WifiNetworkModel.CarrierMerged.of( + subscriptionId = SUB_ID, + level = 1, + numberOfLevels = 6, ) + ) - assertThat(latest).isEqualTo(6) - - job.cancel() - } + assertThat(latest).isEqualTo(6) + } @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() + fun dataEnabled_matchesWifiEnabled() = runTest { + val latest by underTest.dataEnabled.collectLastValue() - wifiRepository.setIsWifiEnabled(false) - assertThat(latest).isFalse() + fakeWifiRepository.setIsWifiEnabled(true) + assertThat(latest).isTrue() - job.cancel() - } + fakeWifiRepository.setIsWifiEnabled(false) + assertThat(latest).isFalse() + } @Test - fun cdmaRoaming_alwaysFalse() = - testScope.runTest { - var latest: Boolean? = null - val job = underTest.cdmaRoaming.onEach { latest = it }.launchIn(this) - - assertThat(latest).isFalse() - - job.cancel() - } + fun cdmaRoaming_alwaysFalse() = runTest { + val latest by underTest.cdmaRoaming.collectLastValue() + assertThat(latest).isFalse() + } @Test - fun networkName_usesSimOperatorNameAsInitial() = - testScope.runTest { - whenever(telephonyManager.simOperatorName).thenReturn("Test SIM name") - - var latest: NetworkNameModel? = null - val job = underTest.networkName.onEach { latest = it }.launchIn(this) + fun networkName_usesSimOperatorNameAsInitial() = runTest { + 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")) + } @Test - fun networkName_updatesOnNetworkUpdate() = - 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() - whenever(telephonyManager.simOperatorName).thenReturn("New SIM name") - wifiRepository.setWifiNetwork( - WifiNetworkModel.CarrierMerged.of(subscriptionId = SUB_ID, level = 3) - ) + assertThat(latest).isEqualTo(NetworkNameModel.SimDerived("Test SIM name")) - assertThat(latest).isEqualTo(NetworkNameModel.SimDerived("New SIM name")) + telephonyManager.stub { on { simOperatorName } doReturn "New SIM name" } + fakeWifiRepository.setWifiNetwork( + WifiNetworkModel.CarrierMerged.of(subscriptionId = SUB_ID, level = 3) + ) - job.cancel() - } + assertThat(latest).isEqualTo(NetworkNameModel.SimDerived("New SIM name")) + } @Test - fun isAllowedDuringAirplaneMode_alwaysTrue() = - testScope.runTest { - val latest by collectLastValue(underTest.isAllowedDuringAirplaneMode) + fun isAllowedDuringAirplaneMode_alwaysTrue() = runTest { + val latest by underTest.isAllowedDuringAirplaneMode.collectLastValue() - 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 56d76fc54d76..858bb095df93 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 android.telephony.TelephonyManager 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 - - @Before - fun setUp() { - mobileRepo = FakeMobileConnectionRepository(SUB_ID, tableLogBuffer) - carrierMergedRepo = - FakeMobileConnectionRepository(SUB_ID, tableLogBuffer).apply { - // Mimicks the real carrier merged repository - this.isAllowedDuringAirplaneMode.value = true - } - - 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) - } + private val kosmos = + testKosmos().apply { + useUnconfinedTestDispatcher() + fakeFeatureFlagsClassic.set(ROAMING_INDICATOR_VIA_DISPLAY_INFO, true) + } + + private fun runTest(block: suspend KairosTestScope.() -> Unit) = + kosmos.run { runKairosTest { block() } } @Test - fun startingIsCarrierMerged_usesCarrierMergedInitially() = - testScope.runTest { - val carrierMergedOperatorName = "Carrier Merged Operator" - val nonCarrierMergedName = "Non-carrier-merged" + fun startingIsCarrierMerged_usesCarrierMergedInitially() = runTest { + val carrierMergedOperatorName = "Carrier Merged Operator" + val nonCarrierMergedName = "Non-carrier-merged" - carrierMergedRepo.operatorAlphaShort.value = carrierMergedOperatorName - mobileRepo.operatorAlphaShort.value = nonCarrierMergedName + fakeCarrierMergedRepo.operatorAlphaShort.setValue(carrierMergedOperatorName) + fakeMobileRepo.operatorAlphaShort.setValue(nonCarrierMergedName) - initializeRepo(startingIsCarrierMerged = true) + isCarrierMerged.setValue(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) - } + val activeRepo by underTest.activeRepo.collectLastValue() + val operatorAlphaShort by underTest.operatorAlphaShort.collectLastValue() - @Test - fun startingNotCarrierMerged_usesTypicalInitially() = - testScope.runTest { - val carrierMergedOperatorName = "Carrier Merged Operator" - val nonCarrierMergedName = "Typical Operator" + assertThat(activeRepo).isEqualTo(fakeCarrierMergedRepo) + assertThat(operatorAlphaShort).isEqualTo(carrierMergedOperatorName) + } - carrierMergedRepo.operatorAlphaShort.value = carrierMergedOperatorName - mobileRepo.operatorAlphaShort.value = nonCarrierMergedName + @Test + fun startingNotCarrierMerged_usesTypicalInitially() = runTest { + val carrierMergedOperatorName = "Carrier Merged Operator" + val nonCarrierMergedName = "Typical Operator" - initializeRepo(startingIsCarrierMerged = false) + fakeCarrierMergedRepo.operatorAlphaShort.setValue(carrierMergedOperatorName) + fakeMobileRepo.operatorAlphaShort.setValue(nonCarrierMergedName) + isCarrierMerged.setValue(false) - assertThat(underTest.activeRepo.value).isEqualTo(mobileRepo) - assertThat(underTest.operatorAlphaShort.value).isEqualTo(nonCarrierMergedName) - verify(carrierMergedFactory, never()).build(SUB_ID, tableLogBuffer) - } + assertThat(underTest.activeRepo.collectLastValue().value).isEqualTo(fakeMobileRepo) + assertThat(underTest.operatorAlphaShort.collectLastValue().value) + .isEqualTo(nonCarrierMergedName) + } @Test - fun activeRepo_matchesIsCarrierMerged() = - testScope.runTest { - initializeRepo(startingIsCarrierMerged = false) - var latest: MobileConnectionRepository? = null - val job = underTest.activeRepo.onEach { latest = it }.launchIn(this) + fun activeRepo_matchesIsCarrierMerged() = runTest { + isCarrierMerged.setValue(false) - underTest.setIsCarrierMerged(true) + val latest by underTest.activeRepo.collectLastValue() - assertThat(latest).isEqualTo(carrierMergedRepo) + isCarrierMerged.setValue(true) - underTest.setIsCarrierMerged(false) + assertThat(latest).isEqualTo(fakeCarrierMergedRepo) - assertThat(latest).isEqualTo(mobileRepo) + isCarrierMerged.setValue(false) - underTest.setIsCarrierMerged(true) + assertThat(latest).isEqualTo(fakeMobileRepo) - assertThat(latest).isEqualTo(carrierMergedRepo) + isCarrierMerged.setValue(true) - job.cancel() - } + assertThat(latest).isEqualTo(fakeCarrierMergedRepo) + } @Test - fun connectionInfo_getsUpdatesFromRepo_carrierMerged() = - testScope.runTest { - initializeRepo(startingIsCarrierMerged = false) - - var latestName: String? = null - var latestLevel: Int? = null + fun connectionInfo_getsUpdatesFromRepo_carrierMerged() = runTest { + isCarrierMerged.setValue(false) - val nameJob = underTest.operatorAlphaShort.onEach { latestName = it }.launchIn(this) - val levelJob = underTest.primaryLevel.onEach { latestLevel = it }.launchIn(this) + val latestName by underTest.operatorAlphaShort.collectLastValue() + val latestLevel by underTest.primaryLevel.collectLastValue() - underTest.setIsCarrierMerged(true) + isCarrierMerged.setValue(true) - val operator1 = "Carrier Merged Operator" - val level1 = 1 - carrierMergedRepo.operatorAlphaShort.value = operator1 - carrierMergedRepo.primaryLevel.value = level1 + val operator1 = "Carrier Merged Operator" + val level1 = 1 + fakeCarrierMergedRepo.operatorAlphaShort.setValue(operator1) + fakeCarrierMergedRepo.primaryLevel.setValue(level1) - assertThat(latestName).isEqualTo(operator1) - assertThat(latestLevel).isEqualTo(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 + 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) + 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 + 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) - - nameJob.cancel() - levelJob.cancel() - } + assertThat(latestName).isEqualTo(operator3) + assertThat(latestLevel).isEqualTo(level3) + } @Test - fun connectionInfo_getsUpdatesFromRepo_mobile() = - testScope.runTest { - initializeRepo(startingIsCarrierMerged = false) - - var latestName: String? = null - var latestLevel: Int? = null + fun connectionInfo_getsUpdatesFromRepo_mobile() = runTest { + isCarrierMerged.setValue(false) - val nameJob = underTest.operatorAlphaShort.onEach { latestName = it }.launchIn(this) - val levelJob = underTest.primaryLevel.onEach { latestLevel = it }.launchIn(this) + val latestName by underTest.operatorAlphaShort.collectLastValue() + val latestLevel by underTest.primaryLevel.collectLastValue() - underTest.setIsCarrierMerged(false) + isCarrierMerged.setValue(false) - val operator1 = "Typical Merged Operator" - val level1 = 1 - mobileRepo.operatorAlphaShort.value = operator1 - mobileRepo.primaryLevel.value = level1 + val operator1 = "Typical Merged Operator" + val level1 = 1 + fakeMobileRepo.operatorAlphaShort.setValue(operator1) + fakeMobileRepo.primaryLevel.setValue(level1) - assertThat(latestName).isEqualTo(operator1) - assertThat(latestLevel).isEqualTo(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 + 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) + 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 + 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) - - nameJob.cancel() - levelJob.cancel() - } + assertThat(latestName).isEqualTo(operator3) + assertThat(latestLevel).isEqualTo(level3) + } @Test - fun connectionInfo_updatesWhenCarrierMergedUpdates() = - testScope.runTest { - initializeRepo(startingIsCarrierMerged = false) - - var latestName: String? = null - var latestLevel: Int? = null + fun connectionInfo_updatesWhenCarrierMergedUpdates() = runTest { + isCarrierMerged.setValue(false) - val nameJob = underTest.operatorAlphaShort.onEach { latestName = it }.launchIn(this) - val levelJob = underTest.primaryLevel.onEach { latestLevel = it }.launchIn(this) + val latestName by underTest.operatorAlphaShort.collectLastValue() + val latestLevel by underTest.primaryLevel.collectLastValue() - val carrierMergedOperator = "Carrier Merged Operator" - val carrierMergedLevel = 4 - carrierMergedRepo.operatorAlphaShort.value = carrierMergedOperator - carrierMergedRepo.primaryLevel.value = carrierMergedLevel + val carrierMergedOperator = "Carrier Merged Operator" + val carrierMergedLevel = 4 + fakeCarrierMergedRepo.operatorAlphaShort.setValue(carrierMergedOperator) + fakeCarrierMergedRepo.primaryLevel.setValue(carrierMergedLevel) - val mobileName = "Typical Operator" - val mobileLevel = 2 - mobileRepo.operatorAlphaShort.value = mobileName - mobileRepo.primaryLevel.value = mobileLevel + 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) + // Start with the mobile info + assertThat(latestName).isEqualTo(mobileName) + assertThat(latestLevel).isEqualTo(mobileLevel) - // WHEN isCarrierMerged is set to true - underTest.setIsCarrierMerged(true) + // WHEN isCarrierMerged is set to true + isCarrierMerged.setValue(true) - // THEN the carrier merged info is used - assertThat(latestName).isEqualTo(carrierMergedOperator) - assertThat(latestLevel).isEqualTo(carrierMergedLevel) + // 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 + val newCarrierMergedName = "New CM Operator" + val newCarrierMergedLevel = 0 + fakeCarrierMergedRepo.operatorAlphaShort.setValue(newCarrierMergedName) + fakeCarrierMergedRepo.primaryLevel.setValue(newCarrierMergedLevel) - assertThat(latestName).isEqualTo(newCarrierMergedName) - assertThat(latestLevel).isEqualTo(newCarrierMergedLevel) + assertThat(latestName).isEqualTo(newCarrierMergedName) + assertThat(latestLevel).isEqualTo(newCarrierMergedLevel) - // WHEN isCarrierMerged is set to false - underTest.setIsCarrierMerged(false) + // WHEN isCarrierMerged is set to false + isCarrierMerged.setValue(false) - // THEN the typical info is used - assertThat(latestName).isEqualTo(mobileName) - assertThat(latestLevel).isEqualTo(mobileLevel) + // 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 + val newMobileName = "New MobileOperator" + val newMobileLevel = 3 + fakeMobileRepo.operatorAlphaShort.setValue(newMobileName) + fakeMobileRepo.primaryLevel.setValue(newMobileLevel) - assertThat(latestName).isEqualTo(newMobileName) - assertThat(latestLevel).isEqualTo(newMobileLevel) - - nameJob.cancel() - levelJob.cancel() - } + assertThat(latestName).isEqualTo(newMobileName) + assertThat(latestLevel).isEqualTo(newMobileLevel) + } @Test - fun isAllowedDuringAirplaneMode_updatesWhenCarrierMergedUpdates() = - testScope.runTest { - initializeRepo(startingIsCarrierMerged = false) + fun isAllowedDuringAirplaneMode_updatesWhenCarrierMergedUpdates() = runTest { + isCarrierMerged.setValue(false) - val latest by collectLastValue(underTest.isAllowedDuringAirplaneMode) + val latest by underTest.isAllowedDuringAirplaneMode.collectLastValue() - assertThat(latest).isFalse() + assertThat(latest).isFalse() - underTest.setIsCarrierMerged(true) + isCarrierMerged.setValue(true) - assertThat(latest).isTrue() + assertThat(latest).isTrue() - underTest.setIsCarrierMerged(false) + isCarrierMerged.setValue(false) - assertThat(latest).isFalse() - } + 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) + 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 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) + 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) - @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() - } + isCarrierMerged.setValue(true) - @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) + // Stand-up activated repository + underTest - initializeRepo(startingIsCarrierMerged = true) + // WHEN we set up carrier merged info + fakeWifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged.of(SUB_ID, level = 3)) - val job = underTest.primaryLevel.launchIn(this) + // THEN the carrier merged info is logged + assertThat(dumpBuffer()).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}3") - // WHEN we set up carrier merged info - wifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged.of(SUB_ID, level = 3)) + // WHEN we update the info + fakeWifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged.of(SUB_ID, level = 1)) - // 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() - } + // THEN the updates are logged + assertThat(dumpBuffer()).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}1") + } @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) + 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) - val job = underTest.primaryLevel.launchIn(this) + isCarrierMerged.setValue(false) - // WHEN we set up some mobile connection info - val signalStrength = mock<SignalStrength>() - whenever(signalStrength.level).thenReturn(1) + // Stand-up activated repository + underTest + // WHEN we set up some mobile connection info + val cb = getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>(telephonyManager) - .onSignalStrengthsChanged(signalStrength) + 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") + // 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) + // 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") + // 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)) + // 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") + // THEN the new level is logged + assertThat(dumpBuffer()).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}4") - // WHEN isCarrierMerged is set to false - underTest.setIsCarrierMerged(false) + // 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") + // 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 + // 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") - - job.cancel() - } + // THEN the new level is logged + assertThat(dumpBuffer()).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}0") + } @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") + 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) } + } - wifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged.of(SUB_ID, level = 3)) - assertThat(dumpBuffer()).doesNotContain("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}3") + // WHEN we set up some mobile connection info + setSignalLevel(1) - // WHEN isCarrierMerged is set to true - underTest.setIsCarrierMerged(true) + // 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") - // 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") + fakeWifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged.of(SUB_ID, level = 3)) + assertThat(dumpBuffer()).doesNotContain("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}3") - whenever(signalStrength.level).thenReturn(6) - getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>(telephonyManager) - .onSignalStrengthsChanged(signalStrength) - assertThat(dumpBuffer()).doesNotContain("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}6") + // WHEN isCarrierMerged is set to true + isCarrierMerged.setValue(true) - job.cancel() - } + // THEN updates to the normal level aren't logged + setSignalLevel(5) + assertThat(dumpBuffer()).doesNotContain("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}5") - private fun initializeRepo(startingIsCarrierMerged: Boolean) { - underTest = - FullMobileConnectionRepositoryKairos( - SUB_ID, - startingIsCarrierMerged, - tableLogBuffer, - subscriptionModel, - DEFAULT_NAME_MODEL, - SEP, - testScope.backgroundScope, - mobileFactory, - carrierMergedFactory, - ) + setSignalLevel(6) + assertThat(dumpBuffer()).doesNotContain("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}6") } - private fun createRealMobileRepo( + 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) - - return realRepo - } + .activated() - 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, + private fun Kosmos.realCarrierMergedRepo( + telephonyManager: TelephonyManager + ): BuildSpec<CarrierMergedConnectionRepositoryKairos> = buildSpec { + activated { + CarrierMergedConnectionRepositoryKairos( + subId = SUB_ID, + tableLogBuffer = mobileLogger, + telephonyManager = telephonyManager, + wifiRepository = wifiRepository, + isInEcmMode = stateOf(false), ) - 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 3a335b738ea9..32fc35934bd6 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 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod 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_SPN 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,1331 +102,1092 @@ import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrier 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 systemUiCarrierConfig = SystemUiCarrierConfig(SUB_1_ID, testCarrierConfig()) + private val Kosmos.tableLogger: TableLogBuffer by Fixture { + tableLogBufferFactory.getOrCreate("test_buffer", 1) + } + + private val Kosmos.context: Context by Fixture { mock() } - private val testDispatcher = UnconfinedTestDispatcher() - private val testScope = TestScope(testDispatcher) + private val systemUiCarrierConfig = SystemUiCarrierConfig(SUB_1_ID, testCarrierConfig()) - private val subscriptionModel: MutableStateFlow<SubscriptionModel?> = - MutableStateFlow( + private val Kosmos.subscriptionModel: MutableState<SubscriptionModel?> by Fixture { + MutableState( + kairos, SubscriptionModel( subscriptionId = SUB_1_ID, carrierName = DEFAULT_NAME, profileClass = PROFILE_CLASS_UNSET, - ) + ), ) - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - whenever(telephonyManager.subscriptionId).thenReturn(SUB_1_ID) - - underTest = - MobileConnectionRepositoryKairosImpl( - SUB_1_ID, - context, - subscriptionModel, - DEFAULT_NAME_MODEL, - SEP, - connectivityManager, - telephonyManager, - systemUiCarrierConfig, - fakeBroadcastDispatcher, - mobileMappings, - testDispatcher, - logger, - tableLogger, - flags, - testScope.backgroundScope, - ) } - @Test - fun emergencyOnly() = - testScope.runTest { - var latest: Boolean? = null - val job = underTest.isEmergencyOnly.onEach { latest = it }.launchIn(this) + private val kosmos = + testKosmos().apply { + useUnconfinedTestDispatcher() + featureFlagsClassic.fake.set(ROAMING_INDICATOR_VIA_DISPLAY_INFO, true) + telephonyManager.stub { on { subscriptionId } doReturn SUB_1_ID } + } - val serviceState = ServiceState() - serviceState.isEmergencyOnly = true + private fun runTest(block: suspend KairosTestScope.() -> Unit) = + kosmos.run { runKairosTest { block() } } - getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState) + @Test + fun emergencyOnly() = runTest { + val latest by underTest.isEmergencyOnly.collectLastValue() - assertThat(latest).isEqualTo(true) + val serviceState = ServiceState().apply { isEmergencyOnly = true } - job.cancel() - } + getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState) + + assertThat(latest).isEqualTo(true) + } @Test - fun emergencyOnly_toggles() = - testScope.runTest { - var latest: Boolean? = null - val job = underTest.isEmergencyOnly.onEach { latest = it }.launchIn(this) + fun emergencyOnly_toggles() = runTest { + val latest by underTest.isEmergencyOnly.collectLastValue() - val callback = getTelephonyCallbackForType<ServiceStateListener>() - callback.onServiceStateChanged(ServiceState().also { it.isEmergencyOnly = true }) - assertThat(latest).isTrue() + val callback = getTelephonyCallbackForType<ServiceStateListener>() + callback.onServiceStateChanged(ServiceState().apply { isEmergencyOnly = true }) - callback.onServiceStateChanged(ServiceState().also { it.isEmergencyOnly = false }) + assertThat(latest).isTrue() - assertThat(latest).isFalse() + callback.onServiceStateChanged(ServiceState().apply { isEmergencyOnly = false }) - job.cancel() - } + assertThat(latest).isFalse() + } @Test - fun cdmaLevelUpdates() = - testScope.runTest { - var latest: Int? = null - val job = underTest.cdmaLevel.onEach { latest = it }.launchIn(this) + 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) + val callback = getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>() + var strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = true) + callback.onSignalStrengthsChanged(strength) - assertThat(latest).isEqualTo(2) + assertThat(latest).isEqualTo(2) - // gsmLevel updates, no change to cdmaLevel - strength = signalStrength(gsmLevel = 3, cdmaLevel = 2, isGsm = true) - callback.onSignalStrengthsChanged(strength) + // gsmLevel updates, no change to cdmaLevel + strength = signalStrength(gsmLevel = 3, cdmaLevel = 2, isGsm = true) + callback.onSignalStrengthsChanged(strength) - assertThat(latest).isEqualTo(2) - - job.cancel() - } + assertThat(latest).isEqualTo(2) + } @Test - fun gsmLevelUpdates() = - testScope.runTest { - var latest: Int? = null - val job = underTest.primaryLevel.onEach { latest = it }.launchIn(this) - - val callback = getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>() - var strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = true) - callback.onSignalStrengthsChanged(strength) + fun gsmLevelUpdates() = runTest { + val latest by underTest.primaryLevel.collectLastValue() - assertThat(latest).isEqualTo(1) + val callback = getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>() + var strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = true) + callback.onSignalStrengthsChanged(strength) - strength = signalStrength(gsmLevel = 3, cdmaLevel = 2, isGsm = true) - callback.onSignalStrengthsChanged(strength) + assertThat(latest).isEqualTo(1) - assertThat(latest).isEqualTo(3) + strength = signalStrength(gsmLevel = 3, cdmaLevel = 2, isGsm = true) + callback.onSignalStrengthsChanged(strength) - job.cancel() - } + assertThat(latest).isEqualTo(3) + } @Test - fun isGsm() = - testScope.runTest { - var latest: Boolean? = null - val job = underTest.isGsm.onEach { latest = it }.launchIn(this) + 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) + val callback = getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>() + var strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = true) + callback.onSignalStrengthsChanged(strength) - assertThat(latest).isTrue() + assertThat(latest).isTrue() - strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = false) - callback.onSignalStrengthsChanged(strength) + strength = signalStrength(gsmLevel = 1, cdmaLevel = 2, isGsm = false) + callback.onSignalStrengthsChanged(strength) - assertThat(latest).isFalse() - - job.cancel() - } + assertThat(latest).isFalse() + } @Test - fun dataConnectionState_connected() = - testScope.runTest { - var latest: DataConnectionState? = null - val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this) - - val callback = - getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() - callback.onDataConnectionStateChanged(DATA_CONNECTED, 200 /* unused */) + fun dataConnectionState_connected() = runTest { + val latest by underTest.dataConnectionState.collectLastValue() - assertThat(latest).isEqualTo(DataConnectionState.Connected) + val callback = getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() + callback.onDataConnectionStateChanged(DATA_CONNECTED, 200 /* unused */) - job.cancel() - } + assertThat(latest).isEqualTo(DataConnectionState.Connected) + } @Test - fun dataConnectionState_connecting() = - testScope.runTest { - var latest: DataConnectionState? = null - val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this) - - val callback = - getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() - callback.onDataConnectionStateChanged(DATA_CONNECTING, 200 /* unused */) + fun dataConnectionState_connecting() = runTest { + val latest by underTest.dataConnectionState.collectLastValue() - assertThat(latest).isEqualTo(DataConnectionState.Connecting) + val callback = getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() + callback.onDataConnectionStateChanged(DATA_CONNECTING, 200 /* unused */) - job.cancel() - } + assertThat(latest).isEqualTo(DataConnectionState.Connecting) + } @Test - fun dataConnectionState_disconnected() = - testScope.runTest { - var latest: DataConnectionState? = null - val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this) + fun dataConnectionState_disconnected() = runTest { + val latest by underTest.dataConnectionState.collectLastValue() - val callback = - getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() - callback.onDataConnectionStateChanged(DATA_DISCONNECTED, 200 /* unused */) + val callback = getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() + callback.onDataConnectionStateChanged(DATA_DISCONNECTED, 200 /* unused */) - assertThat(latest).isEqualTo(DataConnectionState.Disconnected) - - job.cancel() - } + assertThat(latest).isEqualTo(DataConnectionState.Disconnected) + } @Test - fun dataConnectionState_disconnecting() = - testScope.runTest { - var latest: DataConnectionState? = null - val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this) - - val callback = - getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() - callback.onDataConnectionStateChanged(DATA_DISCONNECTING, 200 /* unused */) + fun dataConnectionState_disconnecting() = runTest { + val latest by underTest.dataConnectionState.collectLastValue() - assertThat(latest).isEqualTo(DataConnectionState.Disconnecting) + val callback = getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() + callback.onDataConnectionStateChanged(DATA_DISCONNECTING, 200 /* unused */) - job.cancel() - } + assertThat(latest).isEqualTo(DataConnectionState.Disconnecting) + } @Test - fun dataConnectionState_suspended() = - testScope.runTest { - var latest: DataConnectionState? = null - val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this) + fun dataConnectionState_suspended() = runTest { + val latest by underTest.dataConnectionState.collectLastValue() - val callback = - getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() - callback.onDataConnectionStateChanged(DATA_SUSPENDED, 200 /* unused */) + val callback = getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() + callback.onDataConnectionStateChanged(DATA_SUSPENDED, 200 /* unused */) - assertThat(latest).isEqualTo(DataConnectionState.Suspended) - - job.cancel() - } + assertThat(latest).isEqualTo(DataConnectionState.Suspended) + } @Test - fun dataConnectionState_handoverInProgress() = - testScope.runTest { - var latest: DataConnectionState? = null - val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this) - - val callback = - getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() - callback.onDataConnectionStateChanged(DATA_HANDOVER_IN_PROGRESS, 200 /* unused */) + fun dataConnectionState_handoverInProgress() = runTest { + val latest by underTest.dataConnectionState.collectLastValue() - assertThat(latest).isEqualTo(DataConnectionState.HandoverInProgress) + val callback = getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() + callback.onDataConnectionStateChanged(DATA_HANDOVER_IN_PROGRESS, 200 /* unused */) - job.cancel() - } + assertThat(latest).isEqualTo(DataConnectionState.HandoverInProgress) + } @Test - fun dataConnectionState_unknown() = - testScope.runTest { - var latest: DataConnectionState? = null - val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this) + fun dataConnectionState_unknown() = runTest { + val latest by underTest.dataConnectionState.collectLastValue() - val callback = - getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() - callback.onDataConnectionStateChanged(DATA_UNKNOWN, 200 /* unused */) + val callback = getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() + callback.onDataConnectionStateChanged(DATA_UNKNOWN, 200 /* unused */) - assertThat(latest).isEqualTo(DataConnectionState.Unknown) - - job.cancel() - } + assertThat(latest).isEqualTo(DataConnectionState.Unknown) + } @Test - fun dataConnectionState_invalid() = - testScope.runTest { - var latest: DataConnectionState? = null - val job = underTest.dataConnectionState.onEach { latest = it }.launchIn(this) - - val callback = - getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() - callback.onDataConnectionStateChanged(45, 200 /* unused */) + fun dataConnectionState_invalid() = runTest { + val latest by underTest.dataConnectionState.collectLastValue() - assertThat(latest).isEqualTo(DataConnectionState.Invalid) + val callback = getTelephonyCallbackForType<TelephonyCallback.DataConnectionStateListener>() + callback.onDataConnectionStateChanged(45, 200 /* unused */) - job.cancel() - } + assertThat(latest).isEqualTo(DataConnectionState.Invalid) + } @Test - fun dataActivity() = - testScope.runTest { - var latest: DataActivityModel? = null - val job = underTest.dataActivityDirection.onEach { latest = it }.launchIn(this) + fun dataActivity() = runTest { + val latest by underTest.dataActivityDirection.collectLastValue() - val callback = getTelephonyCallbackForType<DataActivityListener>() - callback.onDataActivity(DATA_ACTIVITY_INOUT) + val callback = getTelephonyCallbackForType<DataActivityListener>() + callback.onDataActivity(DATA_ACTIVITY_INOUT) - assertThat(latest).isEqualTo(DATA_ACTIVITY_INOUT.toMobileDataActivityModel()) - - job.cancel() - } + assertThat(latest).isEqualTo(DATA_ACTIVITY_INOUT.toMobileDataActivityModel()) + } @Test - fun carrierId_initialValueCaptured() = - testScope.runTest { - whenever(telephonyManager.simCarrierId).thenReturn(1234) - - var latest: Int? = null - val job = underTest.carrierId.onEach { latest = it }.launchIn(this) + fun carrierId_initialValueCaptured() = runTest { + whenever(telephonyManager.simCarrierId).thenReturn(1234) - assertThat(latest).isEqualTo(1234) + val latest by underTest.carrierId.collectLastValue() - job.cancel() - } + assertThat(latest).isEqualTo(1234) + } @Test - fun carrierId_updatesOnBroadcast() = - testScope.runTest { - whenever(telephonyManager.simCarrierId).thenReturn(1234) + fun carrierId_updatesOnBroadcast() = runTest { + whenever(telephonyManager.simCarrierId).thenReturn(1234) - var latest: Int? = null - val job = underTest.carrierId.onEach { latest = it }.launchIn(this) - - fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( - context, - carrierIdIntent(carrierId = 4321), - ) + val latest by underTest.carrierId.collectLastValue() - assertThat(latest).isEqualTo(4321) + fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( + context, + carrierIdIntent(carrierId = 4321), + ) - job.cancel() - } + assertThat(latest).isEqualTo(4321) + } @Test - fun carrierNetworkChange() = - testScope.runTest { - var latest: Boolean? = null - val job = underTest.carrierNetworkChangeActive.onEach { latest = it }.launchIn(this) + fun carrierNetworkChange() = runTest { + val latest by underTest.carrierNetworkChangeActive.collectLastValue() - val callback = getTelephonyCallbackForType<TelephonyCallback.CarrierNetworkListener>() - callback.onCarrierNetworkChange(true) + val callback = getTelephonyCallbackForType<TelephonyCallback.CarrierNetworkListener>() + callback.onCarrierNetworkChange(true) - assertThat(latest).isEqualTo(true) - - job.cancel() - } + assertThat(latest).isEqualTo(true) + } @Test - fun networkType_default() = - testScope.runTest { - var latest: ResolvedNetworkType? = null - val job = underTest.resolvedNetworkType.onEach { latest = it }.launchIn(this) - - val expected = UnknownNetworkType + fun networkType_default() = runTest { + val latest by underTest.resolvedNetworkType.collectLastValue() - assertThat(latest).isEqualTo(expected) + val expected = UnknownNetworkType - job.cancel() - } + assertThat(latest).isEqualTo(expected) + } @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) + 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, + ) - val expected = UnknownNetworkType - assertThat(latest).isEqualTo(expected) - assertThat(latest!!.lookupKey).isEqualTo(MobileMappings.toIconKey(NETWORK_TYPE_UNKNOWN)) + callback.onDisplayInfoChanged(ti) - job.cancel() - } + val expected = UnknownNetworkType + assertThat(latest).isEqualTo(expected) + assertThat(latest!!.lookupKey).isEqualTo(MobileMappings.toIconKey(NETWORK_TYPE_UNKNOWN)) + } @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) + fun networkType_updatesUsingDefault() = runTest { + val latest by underTest.resolvedNetworkType.collectLastValue() - val expected = DefaultNetworkType(mobileMappings.toIconKey(type)) - assertThat(latest).isEqualTo(expected) + 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) - job.cancel() - } + val expected = DefaultNetworkType(mobileMappingsProxy.toIconKey(type)) + assertThat(latest).isEqualTo(expected) + } @Test - fun networkType_updatesUsingOverride() = - testScope.runTest { - var latest: ResolvedNetworkType? = null - val job = underTest.resolvedNetworkType.onEach { latest = it }.launchIn(this) + 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 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() - } + val expected = OverrideNetworkType(mobileMappingsProxy.toIconKeyOverride(type)) + assertThat(latest).isEqualTo(expected) + } @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) + fun networkType_unknownNetworkWithOverride_usesOverrideKey() = runTest { + val latest by underTest.resolvedNetworkType.collectLastValue() - val expected = OverrideNetworkType(mobileMappings.toIconKeyOverride(type)) - assertThat(latest).isEqualTo(expected) + 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) - job.cancel() - } + val expected = OverrideNetworkType(mobileMappingsProxy.toIconKeyOverride(type)) + assertThat(latest).isEqualTo(expected) + } @Test - fun dataEnabled_initial_false() = - testScope.runTest { - whenever(telephonyManager.isDataConnectionAllowed).thenReturn(false) + fun dataEnabled_initial_false() = runTest { + whenever(telephonyManager.isDataConnectionAllowed).thenReturn(false) - assertThat(underTest.dataEnabled.value).isFalse() - } + val latest by underTest.dataEnabled.collectLastValue() - @Test - fun isDataEnabled_tracksTelephonyCallback() = - testScope.runTest { - var latest: Boolean? = null - val job = underTest.dataEnabled.onEach { latest = it }.launchIn(this) + assertThat(latest).isFalse() + } - whenever(telephonyManager.isDataConnectionAllowed).thenReturn(false) - assertThat(underTest.dataEnabled.value).isFalse() + @Test + fun isDataEnabled_tracksTelephonyCallback() = runTest { + val latest by underTest.dataEnabled.collectLastValue() - val callback = getTelephonyCallbackForType<TelephonyCallback.DataEnabledListener>() + whenever(telephonyManager.isDataConnectionAllowed).thenReturn(false) + assertThat(latest).isFalse() - callback.onDataEnabledChanged(true, 1) - assertThat(latest).isTrue() + val callback = getTelephonyCallbackForType<TelephonyCallback.DataEnabledListener>() - callback.onDataEnabledChanged(false, 1) - assertThat(latest).isFalse() + callback.onDataEnabledChanged(true, 1) + assertThat(latest).isTrue() - job.cancel() - } + callback.onDataEnabledChanged(false, 1) + assertThat(latest).isFalse() + } @Test - fun numberOfLevels_isDefault() = - testScope.runTest { - var latest: Int? = null - val job = underTest.numberOfLevels.onEach { latest = it }.launchIn(this) + fun numberOfLevels_isDefault() = runTest { + val latest by underTest.numberOfLevels.collectLastValue() - assertThat(latest).isEqualTo(DEFAULT_NUM_LEVELS) - - job.cancel() - } + assertThat(latest).isEqualTo(DEFAULT_NUM_LEVELS) + } @Test - fun roaming_cdma_queriesTelephonyManager() = - testScope.runTest { - var latest: Boolean? = null - val job = underTest.cdmaRoaming.onEach { latest = it }.launchIn(this) - - val cb = getTelephonyCallbackForType<ServiceStateListener>() + fun roaming_cdma_queriesTelephonyManager() = runTest { + val latest by underTest.cdmaRoaming.collectLastValue() - // CDMA roaming is off, GSM roaming is on - whenever(telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber).thenReturn(ERI_OFF) - cb.onServiceStateChanged(ServiceState().also { it.roaming = true }) + val cb = getTelephonyCallbackForType<ServiceStateListener>() - assertThat(latest).isFalse() + // CDMA roaming is off, GSM roaming is on + whenever(telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber).thenReturn(ERI_OFF) + cb.onServiceStateChanged(ServiceState().also { it.roaming = true }) - // CDMA roaming is on, GSM roaming is off - whenever(telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber).thenReturn(ERI_ON) - cb.onServiceStateChanged(ServiceState().also { it.roaming = false }) + assertThat(latest).isFalse() - assertThat(latest).isTrue() + // CDMA roaming is on, GSM roaming is off + whenever(telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber).thenReturn(ERI_ON) + cb.onServiceStateChanged(ServiceState().also { it.roaming = false }) - job.cancel() - } + 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() = - testScope.runTest { - var latest: Boolean? = null - val job = underTest.cdmaRoaming.onEach { latest = it }.launchIn(this) - - val serviceState = ServiceState() - serviceState.roaming = false + fun cdmaRoaming_ignoresNegativeOne() = runTest { + val latest by underTest.cdmaRoaming.collectLastValue() - val cb = getTelephonyCallbackForType<ServiceStateListener>() + val serviceState = ServiceState() + serviceState.roaming = false - // CDMA roaming is unavailable (-1), GSM roaming is off - whenever(telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber).thenReturn(-1) - cb.onServiceStateChanged(serviceState) + val cb = getTelephonyCallbackForType<ServiceStateListener>() - assertThat(latest).isFalse() + // CDMA roaming is unavailable (-1), GSM roaming is off + whenever(telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber).thenReturn(-1) + cb.onServiceStateChanged(serviceState) - job.cancel() - } + assertThat(latest).isFalse() + } @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) - ) + fun roaming_gsm_queriesDisplayInfo_viaDisplayInfo() = runTest { + // GIVEN flag is true + featureFlagsClassic.fake.set(ROAMING_INDICATOR_VIA_DISPLAY_INFO, true) - assertThat(latest).isFalse() + val latest by underTest.isRoaming.collectLastValue() - // CDMA roaming is off, GSM roaming is on - cb.onDisplayInfoChanged( - TelephonyDisplayInfo(NETWORK_TYPE_LTE, NETWORK_TYPE_UNKNOWN, true, false, false) - ) + val cb = getTelephonyCallbackForType<DisplayInfoListener>() - assertThat(latest).isTrue() + // CDMA roaming is off, GSM roaming is off + whenever(telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber).thenReturn(ERI_OFF) + cb.onDisplayInfoChanged(TelephonyDisplayInfo(NETWORK_TYPE_LTE, NETWORK_TYPE_UNKNOWN, false)) - job.cancel() - } + assertThat(latest).isFalse() - @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() - } + // CDMA roaming is off, GSM roaming is on + cb.onDisplayInfoChanged(TelephonyDisplayInfo(NETWORK_TYPE_LTE, NETWORK_TYPE_UNKNOWN, true)) + + assertThat(latest).isTrue() + } @Test - fun activity_updatesFromCallback() = - testScope.runTest { - var latest: DataActivityModel? = null - val job = underTest.dataActivityDirection.onEach { latest = it }.launchIn(this) + fun roaming_gsm_queriesDisplayInfo_viaServiceState() = runTest { + // GIVEN flag is false + featureFlagsClassic.fake.set(ROAMING_INDICATOR_VIA_DISPLAY_INFO, false) - assertThat(latest) - .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false)) + val latest by underTest.isRoaming.collectLastValue() - val cb = getTelephonyCallbackForType<DataActivityListener>() - cb.onDataActivity(DATA_ACTIVITY_IN) - assertThat(latest) - .isEqualTo(DataActivityModel(hasActivityIn = true, hasActivityOut = false)) + val cb = getTelephonyCallbackForType<ServiceStateListener>() - cb.onDataActivity(DATA_ACTIVITY_OUT) - assertThat(latest) - .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = true)) + // CDMA roaming is off, GSM roaming is off + whenever(telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber).thenReturn(ERI_OFF) + cb.onServiceStateChanged(ServiceState().also { it.roaming = false }) - cb.onDataActivity(DATA_ACTIVITY_INOUT) - assertThat(latest) - .isEqualTo(DataActivityModel(hasActivityIn = true, hasActivityOut = true)) + assertThat(latest).isFalse() - cb.onDataActivity(DATA_ACTIVITY_NONE) - assertThat(latest) - .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false)) + // CDMA roaming is off, GSM roaming is on + cb.onServiceStateChanged(ServiceState().also { it.roaming = true }) - cb.onDataActivity(DATA_ACTIVITY_DORMANT) - assertThat(latest) - .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false)) + assertThat(latest).isTrue() + } - cb.onDataActivity(1234) - assertThat(latest) - .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false)) + @Test + fun activity_updatesFromCallback() = runTest { + val latest by underTest.dataActivityDirection.collectLastValue() - job.cancel() - } + assertThat(latest) + .isEqualTo(DataActivityModel(hasActivityIn = false, hasActivityOut = false)) - @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() - } + 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_defaultWhenSubscriptionModelNull() = - testScope.runTest { - var latest: NetworkNameModel? = null - val job = underTest.carrierName.onEach { latest = it }.launchIn(this) + fun networkNameForSubId_updates() = runTest { + val latest by underTest.carrierName.collectLastValue() - subscriptionModel.value = null + subscriptionModel.setValue( + SubscriptionModel( + subscriptionId = SUB_1_ID, + carrierName = DEFAULT_NAME, + profileClass = PROFILE_CLASS_UNSET, + ) + ) - assertThat(latest?.name).isEqualTo(DEFAULT_NAME) + assertThat(latest?.name).isEqualTo(DEFAULT_NAME) - job.cancel() - } + val updatedName = "Derived Carrier" + subscriptionModel.setValue( + SubscriptionModel( + subscriptionId = SUB_1_ID, + carrierName = updatedName, + profileClass = PROFILE_CLASS_UNSET, + ) + ) + + assertThat(latest?.name).isEqualTo(updatedName) + } @Test - fun networkName_default() = - testScope.runTest { - var latest: NetworkNameModel? = null - val job = underTest.networkName.onEach { latest = it }.launchIn(this) + fun networkNameForSubId_defaultWhenSubscriptionModelNull() = runTest { + val latest by underTest.carrierName.collectLastValue() - assertThat(latest).isEqualTo(DEFAULT_NAME_MODEL) + subscriptionModel.setValue(null) - job.cancel() - } + assertThat(latest?.name).isEqualTo(DEFAULT_NAME) + } @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) + fun networkName_default() = runTest { + val latest by underTest.networkName.collectLastValue() - val intent = spnIntent() - val captor = argumentCaptor<BroadcastReceiver>() - verify(context).registerReceiver(captor.capture(), any()) - captor.lastValue.onReceive(context, intent) + assertThat(latest).isEqualTo(DEFAULT_NAME_MODEL) + } - // spnIntent() sets all values to true and test strings - assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN")) + @Test + @EnableFlags(Flags.FLAG_STATUS_BAR_SWITCH_TO_SPN_FROM_DATA_SPN) + fun networkName_usesBroadcastInfo_returnsDerived() = runTest { + val latest by underTest.networkName.collectLastValue() - job.cancel() - } + 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")) + } @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) + fun networkName_usesBroadcastInfo_returnsDerived_flagOff() = runTest { + val latest by underTest.networkName.collectLastValue() - val intent = spnIntent() - val captor = argumentCaptor<BroadcastReceiver>() - verify(context).registerReceiver(captor.capture(), any()) - captor.lastValue.onReceive(context, intent) + 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() - } + // spnIntent() sets all values to true and test strings + assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN")) + } @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) + fun networkName_broadcastNotForThisSubId_keepsOldValue() = runTest { + val latest by underTest.networkName.collectLastValue() - assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN")) + val intent = spnIntent() + val captor = argumentCaptor<BroadcastReceiver>() + verify(context).registerReceiver(captor.capture(), any()) + captor.lastValue.onReceive(context, intent) - // WHEN an intent with a different subId is sent - val wrongSubIntent = spnIntent(subId = 101) + assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN")) - captor.lastValue.onReceive(context, wrongSubIntent) + // WHEN an intent with a different subId is sent + val wrongSubIntent = spnIntent(subId = 101) - // THEN the previous intent's name is still used - assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN")) + 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 @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) + fun networkName_broadcastNotForThisSubId_keepsOldValue_flagOff() = runTest { + val latest by underTest.networkName.collectLastValue() - val intent = spnIntent() - val captor = argumentCaptor<BroadcastReceiver>() - verify(context).registerReceiver(captor.capture(), any()) - captor.lastValue.onReceive(context, intent) + 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")) + assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN")) - // WHEN an intent with a different subId is sent - val wrongSubIntent = spnIntent(subId = 101) + // WHEN an intent with a different subId is sent + val wrongSubIntent = spnIntent(subId = 101) - captor.lastValue.onReceive(context, wrongSubIntent) + 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() - } + // THEN the previous intent's name is still used + assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN")) + } @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) + fun networkName_broadcastHasNoData_updatesToDefault() = runTest { + val latest by underTest.networkName.collectLastValue() - assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN")) + val intent = spnIntent() + val captor = argumentCaptor<BroadcastReceiver>() + verify(context).registerReceiver(captor.capture(), any()) + captor.lastValue.onReceive(context, intent) - val intentWithoutInfo = spnIntent(showSpn = false, showPlmn = false) + assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN")) - captor.lastValue.onReceive(context, intentWithoutInfo) + val intentWithoutInfo = spnIntent(showSpn = false, showPlmn = false) - assertThat(latest).isEqualTo(DEFAULT_NAME_MODEL) + captor.lastValue.onReceive(context, intentWithoutInfo) - job.cancel() - } + assertThat(latest).isEqualTo(DEFAULT_NAME_MODEL) + } @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) + fun networkName_broadcastHasNoData_updatesToDefault_flagOff() = runTest { + val latest by underTest.networkName.collectLastValue() - val intent = spnIntent() - val captor = argumentCaptor<BroadcastReceiver>() - verify(context).registerReceiver(captor.capture(), any()) - captor.lastValue.onReceive(context, intent) + 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")) + assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived("$PLMN$SEP$DATA_SPN")) - val intentWithoutInfo = spnIntent(showSpn = false, showPlmn = false) + val intentWithoutInfo = spnIntent(showSpn = false, showPlmn = false) - captor.lastValue.onReceive(context, intentWithoutInfo) + captor.lastValue.onReceive(context, intentWithoutInfo) - assertThat(latest).isEqualTo(DEFAULT_NAME_MODEL) - - job.cancel() - } + assertThat(latest).isEqualTo(DEFAULT_NAME_MODEL) + } @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")) - } + fun networkName_usingEagerStrategy_retainsNameBetweenSubscribers() = runTest { + // Use the [StateFlow.value] getter so we can prove that the collection happens + // even when there is no [Job] + + // Starts out default + val latest by underTest.networkName.collectLastValue() + assertThat(latest).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(latest).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")) - } + 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] + + // Starts out default + val latest by underTest.networkName.collectLastValue() + assertThat(latest).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(latest).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")) - } + fun networkName_allFieldsSet_prioritizesDataSpnOverSpn() = 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 = 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")) - } + fun networkName_spnAndPlmn_fallbackToSpnWhenNullDataSpn() = 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 @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")) - } + fun networkName_allFieldsSet_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 = 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")) - } + 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 @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")) - } + 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 @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")) - } + 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")) + } @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")) - } + 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")) + } @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")) - } + 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() = - 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")) - } + 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() = - 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) - } + 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() = - 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", + 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")) + } - getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState) + @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", + ) - assertThat(latest).isEqualTo(shortName) + getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState) - job.cancel() - } + assertThat(latest).isEqualTo(shortName) + } @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() - } + 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) + } + ) - @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 = + assertThat(latest).isTrue() + + getTelephonyCallbackForType<ServiceStateListener>() + .onServiceStateChanged( ServiceState().also { it.voiceRegState = STATE_OUT_OF_SERVICE - it.addNetworkRegistrationInfo(iwlanData) + 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() + } - getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState) - assertThat(latest).isFalse() - - job.cancel() - } + @Test + fun isInService_isIwlan_voiceOutOfService_dataInService() = runTest { + val latest by underTest.isInService.collectLastValue() + + 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) + } + + getTelephonyCallbackForType<ServiceStateListener>().onServiceStateChanged(serviceState) + assertThat(latest).isFalse() + } @Test - fun isNonTerrestrial_updatesFromCallback0() = - testScope.runTest { - val latest by collectLastValue(underTest.isNonTerrestrial) + fun isNonTerrestrial_updatesFromCallback0() = runTest { + val latest by underTest.isNonTerrestrial.collectLastValue() - // Starts out false - assertThat(latest).isFalse() + // Starts out false + assertThat(latest).isFalse() - val callback = getTelephonyCallbackForType<CarrierRoamingNtnListener>() + val callback = getTelephonyCallbackForType<CarrierRoamingNtnListener>() - callback.onCarrierRoamingNtnModeChanged(true) - assertThat(latest).isTrue() + callback.onCarrierRoamingNtnModeChanged(true) + assertThat(latest).isTrue() - callback.onCarrierRoamingNtnModeChanged(false) - assertThat(latest).isFalse() - } + 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) + fun numberOfLevels_usesCarrierConfig() = runTest { + val latest by underTest.numberOfLevels.collectLastValue() - assertThat(latest).isEqualTo(DEFAULT_NUM_LEVELS) - - systemUiCarrierConfig.processNewCarrierConfig( - testCarrierConfigWithOverride(KEY_INFLATE_SIGNAL_STRENGTH_BOOL, true) - ) + assertThat(latest).isEqualTo(DEFAULT_NUM_LEVELS) - assertThat(latest).isEqualTo(DEFAULT_NUM_LEVELS + 1) + systemUiCarrierConfig.processNewCarrierConfig( + testCarrierConfigWithOverride(KEY_INFLATE_SIGNAL_STRENGTH_BOOL, true) + ) - systemUiCarrierConfig.processNewCarrierConfig( - testCarrierConfigWithOverride(KEY_INFLATE_SIGNAL_STRENGTH_BOOL, false) - ) + assertThat(latest).isEqualTo(DEFAULT_NUM_LEVELS + 1) - assertThat(latest).isEqualTo(DEFAULT_NUM_LEVELS) + systemUiCarrierConfig.processNewCarrierConfig( + testCarrierConfigWithOverride(KEY_INFLATE_SIGNAL_STRENGTH_BOOL, false) + ) - job.cancel() - } + assertThat(latest).isEqualTo(DEFAULT_NUM_LEVELS) + } @Test - fun inflateSignalStrength_usesCarrierConfig() = - testScope.runTest { - val latest by collectLastValue(underTest.inflateSignalStrength) + fun inflateSignalStrength_usesCarrierConfig() = runTest { + val latest by underTest.inflateSignalStrength.collectLastValue() - assertThat(latest).isEqualTo(false) + assertThat(latest).isEqualTo(false) - systemUiCarrierConfig.processNewCarrierConfig( - testCarrierConfigWithOverride(KEY_INFLATE_SIGNAL_STRENGTH_BOOL, true) - ) + systemUiCarrierConfig.processNewCarrierConfig( + testCarrierConfigWithOverride(KEY_INFLATE_SIGNAL_STRENGTH_BOOL, true) + ) - assertThat(latest).isEqualTo(true) + assertThat(latest).isEqualTo(true) - systemUiCarrierConfig.processNewCarrierConfig( - testCarrierConfigWithOverride(KEY_INFLATE_SIGNAL_STRENGTH_BOOL, false) - ) + systemUiCarrierConfig.processNewCarrierConfig( + testCarrierConfigWithOverride(KEY_INFLATE_SIGNAL_STRENGTH_BOOL, false) + ) - assertThat(latest).isEqualTo(false) - } + assertThat(latest).isEqualTo(false) + } @Test - fun allowNetworkSliceIndicator_exposesCarrierConfigValue() = - testScope.runTest { - val latest by collectLastValue(underTest.allowNetworkSliceIndicator) + fun allowNetworkSliceIndicator_exposesCarrierConfigValue() = runTest { + val latest by underTest.allowNetworkSliceIndicator.collectLastValue() - systemUiCarrierConfig.processNewCarrierConfig( - testCarrierConfigWithOverride(KEY_SHOW_5G_SLICE_ICON_BOOL, true) - ) + systemUiCarrierConfig.processNewCarrierConfig( + testCarrierConfigWithOverride(KEY_SHOW_5G_SLICE_ICON_BOOL, true) + ) - assertThat(latest).isTrue() + assertThat(latest).isTrue() - systemUiCarrierConfig.processNewCarrierConfig( - testCarrierConfigWithOverride(KEY_SHOW_5G_SLICE_ICON_BOOL, false) - ) + systemUiCarrierConfig.processNewCarrierConfig( + testCarrierConfigWithOverride(KEY_SHOW_5G_SLICE_ICON_BOOL, false) + ) - assertThat(latest).isFalse() - } + assertThat(latest).isFalse() + } @Test - fun isAllowedDuringAirplaneMode_alwaysFalse() = - testScope.runTest { - val latest by collectLastValue(underTest.isAllowedDuringAirplaneMode) + fun isAllowedDuringAirplaneMode_alwaysFalse() = runTest { + val latest by underTest.isAllowedDuringAirplaneMode.collectLastValue() - assertThat(latest).isFalse() - } + assertThat(latest).isFalse() + } @Test - fun hasPrioritizedCaps_defaultFalse() { - assertThat(underTest.hasPrioritizedNetworkCapabilities.value).isFalse() + fun hasPrioritizedCaps_defaultFalse() = runTest { + // stand up under-test to kick-off activation + underTest + + assertThat(kairos.transact { underTest.hasPrioritizedNetworkCapabilities.sample() }) + .isFalse() } @Test - fun hasPrioritizedCaps_trueWhenAvailable() = - testScope.runTest { - val latest by collectLastValue(underTest.hasPrioritizedNetworkCapabilities) + fun hasPrioritizedCaps_trueWhenAvailable() = runTest { + val latest by underTest.hasPrioritizedNetworkCapabilities.collectLastValue() - val callback: NetworkCallback = - withArgCaptor<NetworkCallback> { - verify(connectivityManager).registerNetworkCallback(any(), capture()) - } + val callback: NetworkCallback = + argumentCaptor<NetworkCallback>() + .apply { verify(connectivityManager).registerNetworkCallback(any(), capture()) } + .lastValue - callback.onAvailable(mock()) + callback.onAvailable(mock()) - assertThat(latest).isTrue() - } + assertThat(latest).isTrue() + } @Test - fun hasPrioritizedCaps_becomesFalseWhenNetworkLost() = - testScope.runTest { - val latest by collectLastValue(underTest.hasPrioritizedNetworkCapabilities) + fun hasPrioritizedCaps_becomesFalseWhenNetworkLost() = runTest { + val latest by underTest.hasPrioritizedNetworkCapabilities.collectLastValue() - val callback: NetworkCallback = - withArgCaptor<NetworkCallback> { - verify(connectivityManager).registerNetworkCallback(any(), capture()) - } + val callback: NetworkCallback = + argumentCaptor<NetworkCallback>() + .apply { verify(connectivityManager).registerNetworkCallback(any(), capture()) } + .lastValue - callback.onAvailable(mock()) + callback.onAvailable(mock()) - assertThat(latest).isTrue() + assertThat(latest).isTrue() - callback.onLost(mock()) + callback.onLost(mock()) - assertThat(latest).isFalse() - } + assertThat(latest).isFalse() + } - private inline fun <reified T> getTelephonyCallbackForType(): T { + 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 8542321858a5..e04a96eb3032 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 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod 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.NET_CAPABILITY_VALIDATED 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.ServiceState 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 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 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 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 lateinit var underTest: MobileConnectionsRepositoryKairosImpl - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - whenever(telephonyManager.simOperatorName).thenReturn("") - - // Set up so the individual connection repositories - whenever(telephonyManager.createForSubscriptionId(anyInt())).thenAnswer { invocation -> - telephonyManager.also { - whenever(it.subscriptionId).thenReturn(invocation.getArgument(0)) - } - } - - whenever(logBufferFactory.getOrCreate(anyString(), anyInt())).thenAnswer { _ -> - logcatTableLogBuffer(kosmos, "test") - } - whenever( - wifiPickerTrackerFactory.create( - any(), - any(), - capture(wifiPickerTrackerCallback), - any(), + private val Kosmos.underTest + get() = 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(), ) - ) - .thenReturn(wifiPickerTracker) - - // For convenience, set up the subscription info callbacks - whenever(subscriptionManager.getActiveSubscriptionInfo(anyInt())).thenAnswer { invocation -> - when (invocation.getArgument(0) as Int) { - 1 -> SUB_1 - 2 -> SUB_2 - 3 -> SUB_3 - 4 -> SUB_4 - else -> null + 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() } - connectivityRepository = - ConnectivityRepositoryImpl( - connectivityManager, - ConnectivitySlots(context), - context, - mock(), - mock(), - testScope.backgroundScope, - mock(), - ) + private fun runTest(block: suspend KairosTestScope.() -> Unit) = + kosmos.run { runKairosTest { block() } } - 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(), - ) - - testScope.runCurrent() + @Test + fun testSubscriptions_initiallyEmpty() = runTest { + assertThat(underTest.subscriptions.collectLastValue().value) + .isEqualTo(listOf<SubscriptionModel>()) } @Test - fun testSubscriptions_initiallyEmpty() = - testScope.runTest { - assertThat(underTest.subscriptions.value).isEqualTo(listOf<SubscriptionModel>()) + fun testSubscriptions_listUpdates() = runTest { + val latest by underTest.subscriptions.collectLastValue() + + subscriptionManager.stub { + on { completeActiveSubscriptionInfoList } doReturn listOf(SUB_1, SUB_2) } + getSubscriptionCallback().onSubscriptionsChanged() - @Test - fun testSubscriptions_listUpdates() = - testScope.runTest { - val latest by collectLastValue(underTest.subscriptions) + assertThat(latest).isEqualTo(listOf(MODEL_1, MODEL_2)) + } - whenever(subscriptionManager.completeActiveSubscriptionInfoList) - .thenReturn(listOf(SUB_1, SUB_2)) - getSubscriptionCallback().onSubscriptionsChanged() + @Test + fun testSubscriptions_removingSub_updatesList() = runTest { + val latest by underTest.subscriptions.collectLastValue() - assertThat(latest).isEqualTo(listOf(MODEL_1, MODEL_2)) + // WHEN 2 networks show up + subscriptionManager.stub { + on { completeActiveSubscriptionInfoList } doReturn listOf(SUB_1, SUB_2) } + getSubscriptionCallback().onSubscriptionsChanged() - @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)) + // 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() = - 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) - } + fun subscriptions_subIsOnlyNtn_modelHasExclusivelyNtnTrue() = runTest { + val latest by underTest.subscriptions.collectLastValue() - whenever(subscriptionManager.completeActiveSubscriptionInfoList) - .thenReturn(listOf(onlyNtnSub)) - getSubscriptionCallback().onSubscriptionsChanged() + 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 + } - assertThat(latest).hasSize(1) - assertThat(latest!![0].isExclusivelyNonTerrestrial).isTrue() + subscriptionManager.stub { + on { completeActiveSubscriptionInfoList } doReturn 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) - } + fun subscriptions_subIsNotOnlyNtn_modelHasExclusivelyNtnFalse() = runTest { + val latest by underTest.subscriptions.collectLastValue() - whenever(subscriptionManager.completeActiveSubscriptionInfoList) - .thenReturn(listOf(notOnlyNtnSub)) - getSubscriptionCallback().onSubscriptionsChanged() + 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 + } - assertThat(latest).hasSize(1) - assertThat(latest!![0].isExclusivelyNonTerrestrial).isFalse() + subscriptionManager.stub { + on { completeActiveSubscriptionInfoList } doReturn listOf(notOnlyNtnSub) } + getSubscriptionCallback().onSubscriptionsChanged() - @Test - fun testSubscriptions_carrierMergedOnly_listHasCarrierMerged() = - testScope.runTest { - val latest by collectLastValue(underTest.subscriptions) + assertThat(latest).hasSize(1) + assertThat(latest!![0].isExclusivelyNonTerrestrial).isFalse() + } - setWifiState(isCarrierMerged = true) - whenever(subscriptionManager.completeActiveSubscriptionInfoList) - .thenReturn(listOf(SUB_CM)) - getSubscriptionCallback().onSubscriptionsChanged() + @Test + fun testSubscriptions_carrierMergedOnly_listHasCarrierMerged() = runTest { + val latest by underTest.subscriptions.collectLastValue() - assertThat(latest).isEqualTo(listOf(MODEL_CM)) + setWifiState(isCarrierMerged = true) + subscriptionManager.stub { + on { completeActiveSubscriptionInfoList } doReturn listOf(SUB_CM) } + getSubscriptionCallback().onSubscriptionsChanged() - @Test - fun testSubscriptions_carrierMergedAndOther_listHasBothWithCarrierMergedLast() = - testScope.runTest { - val latest by collectLastValue(underTest.subscriptions) + assertThat(latest).isEqualTo(listOf(MODEL_CM)) + } - setWifiState(isCarrierMerged = true) - whenever(subscriptionManager.completeActiveSubscriptionInfoList) - .thenReturn(listOf(SUB_1, SUB_2, SUB_CM)) - getSubscriptionCallback().onSubscriptionsChanged() + @Test + fun testSubscriptions_carrierMergedAndOther_listHasBothWithCarrierMergedLast() = runTest { + val latest by underTest.subscriptions.collectLastValue() - assertThat(latest).isEqualTo(listOf(MODEL_1, MODEL_2, MODEL_CM)) + 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() = - testScope.runTest { - assertThat(underTest.activeMobileDataSubscriptionId.value).isEqualTo(null) - } + fun testActiveDataSubscriptionId_initialValueIsNull() = runTest { + assertThat(underTest.activeMobileDataSubscriptionId.collectLastValue().value) + .isEqualTo(null) + } @Test - fun testActiveDataSubscriptionId_updates() = - testScope.runTest { - val active by collectLastValue(underTest.activeMobileDataSubscriptionId) + fun testActiveDataSubscriptionId_updates() = runTest { + val active by underTest.activeMobileDataSubscriptionId.collectLastValue() + testScope.runCurrent() - getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>() - .onActiveDataSubscriptionIdChanged(SUB_2_ID) + getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>(telephonyManager) + .onActiveDataSubscriptionIdChanged(SUB_2_ID) - assertThat(active).isEqualTo(SUB_2_ID) - } + assertThat(active).isEqualTo(SUB_2_ID) + } @Test - fun activeSubId_nullIfInvalidSubIdIsReceived() = - testScope.runTest { - val latest by collectLastValue(underTest.activeMobileDataSubscriptionId) + fun activeSubId_nullIfInvalidSubIdIsReceived() = runTest { + val latest by underTest.activeMobileDataSubscriptionId.collectLastValue() + testScope.runCurrent() - getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>() - .onActiveDataSubscriptionIdChanged(SUB_2_ID) + getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>(telephonyManager) + .onActiveDataSubscriptionIdChanged(SUB_2_ID) - assertThat(latest).isNotNull() + assertThat(latest).isNotNull() - getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>() - .onActiveDataSubscriptionIdChanged(INVALID_SUBSCRIPTION_ID) + getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>(telephonyManager) + .onActiveDataSubscriptionIdChanged(INVALID_SUBSCRIPTION_ID) - assertThat(latest).isNull() - } + assertThat(latest).isNull() + } @Test - fun activeRepo_initiallyNull() { - assertThat(underTest.activeMobileDataRepository.value).isNull() + fun activeRepo_initiallyNull() = runTest { + assertThat(underTest.activeMobileDataRepository.collectLastValue().value).isNull() } @Test - fun activeRepo_updatesWithActiveDataId() = - testScope.runTest { - val latest by collectLastValue(underTest.activeMobileDataRepository) - - getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>() - .onActiveDataSubscriptionIdChanged(SUB_2_ID) + fun activeRepo_updatesWithActiveDataId() = runTest { + val latest by underTest.activeMobileDataRepository.collectLastValue() + testScope.runCurrent() - assertThat(latest?.subId).isEqualTo(SUB_2_ID) + // 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() - @Test - fun activeRepo_nullIfActiveDataSubIdBecomesInvalid() = - testScope.runTest { - val latest by collectLastValue(underTest.activeMobileDataRepository) - - getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>() - .onActiveDataSubscriptionIdChanged(SUB_2_ID) - - assertThat(latest).isNotNull() - - getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>() - .onActiveDataSubscriptionIdChanged(INVALID_SUBSCRIPTION_ID) + getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>(telephonyManager) + .onActiveDataSubscriptionIdChanged(SUB_2_ID) - assertThat(latest).isNull() - } + assertThat(latest?.subId).isEqualTo(SUB_2_ID) + } @Test - /** Regression test for b/268146648. */ - fun activeSubIdIsSetBeforeSubscriptionsAreUpdated_doesNotThrow() = - testScope.runTest { - val activeRepo by collectLastValue(underTest.activeMobileDataRepository) - val subscriptions by collectLastValue(underTest.subscriptions) - - getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>() - .onActiveDataSubscriptionIdChanged(SUB_2_ID) + fun activeRepo_nullIfActiveDataSubIdBecomesInvalid() = runTest { + val latest by underTest.activeMobileDataRepository.collectLastValue() + testScope.runCurrent() - assertThat(subscriptions).isEmpty() - assertThat(activeRepo).isNotNull() + // 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() - @Test - fun getRepoForSubId_activeDataSubIdIsRequestedBeforeSubscriptionsUpdate() = - testScope.runTest { - var latestActiveRepo: MobileConnectionRepository? = null - collectLastValue( - underTest.activeMobileDataSubscriptionId.filterNotNull().onEach { - latestActiveRepo = underTest.getRepoForSubId(it) - } - ) + getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>(telephonyManager) + .onActiveDataSubscriptionIdChanged(SUB_2_ID) + testScope.runCurrent() - val latestSubscriptions by collectLastValue(underTest.subscriptions) + assertThat(latest).isNotNull() - // Active data subscription id is sent, but no subscription change has been posted yet - getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>() - .onActiveDataSubscriptionIdChanged(SUB_2_ID) + getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>(telephonyManager) + .onActiveDataSubscriptionIdChanged(INVALID_SUBSCRIPTION_ID) + testScope.runCurrent() - // Subscriptions list is empty - assertThat(latestSubscriptions).isEmpty() - // getRepoForSubId does not throw - assertThat(latestActiveRepo).isNotNull() - } + assertThat(latest).isNull() + } @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() + /** Regression test for b/268146648. */ + fun activeSubIdIsSetBeforeSubscriptionsAreUpdated_doesNotThrow() = runTest { + val activeRepo by underTest.activeMobileDataRepository.collectLastValue() + val subscriptions by underTest.subscriptions.collectLastValue() + testScope.runCurrent() - // GIVEN the subscription list is then updated which includes the active data sub id - whenever(subscriptionManager.completeActiveSubscriptionInfoList) - .thenReturn(listOf(SUB_2)) - getSubscriptionCallback().onSubscriptionsChanged() + getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>(telephonyManager) + .onActiveDataSubscriptionIdChanged(SUB_2_ID) + testScope.runCurrent() - // WHEN requesting a connection repository for the subscription - val newRepo = underTest.getRepoForSubId(SUB_2_ID) + assertThat(subscriptions).isEmpty() + assertThat(activeRepo).isNull() + } - // THEN the newly request repo has been cached and reused - assertThat(activeRepo).isSameInstanceAs(newRepo) + @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 + } + } + } } - @Test - fun testConnectionRepository_validSubId_isCached() = - testScope.runTest { - collectLastValue(underTest.subscriptions) + val latestSubscriptions by underTest.subscriptions.collectLastValue() + testScope.runCurrent() - whenever(subscriptionManager.completeActiveSubscriptionInfoList) - .thenReturn(listOf(SUB_1)) - getSubscriptionCallback().onSubscriptionsChanged() + // Active data subscription id is sent, but no subscription change has been posted yet + getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>(telephonyManager) + .onActiveDataSubscriptionIdChanged(SUB_2_ID) + testScope.runCurrent() - val repo1 = underTest.getRepoForSubId(SUB_1_ID) - val repo2 = underTest.getRepoForSubId(SUB_1_ID) + // Subscriptions list is empty + assertThat(latestSubscriptions).isEmpty() - assertThat(repo1).isSameInstanceAs(repo2) - } + // getRepoForSubId does not throw + assertThat(latestActiveRepo).isNull() + } @Test - fun testConnectionRepository_carrierMergedSubId_isCached() = - testScope.runTest { - collectLastValue(underTest.subscriptions) + fun activeDataSentBeforeSubscriptionList_subscriptionReusesActiveDataRepo() = runTest { + val activeRepo by underTest.activeMobileDataRepository.collectLastValue() + testScope.runCurrent() - getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_CM) - setWifiState(isCarrierMerged = true) - whenever(subscriptionManager.completeActiveSubscriptionInfoList) - .thenReturn(listOf(SUB_CM)) - getSubscriptionCallback().onSubscriptionsChanged() + // GIVEN active repo is updated before the subscription list updates + getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>(telephonyManager) + .onActiveDataSubscriptionIdChanged(SUB_2_ID) + testScope.runCurrent() - val repo1 = underTest.getRepoForSubId(SUB_CM_ID) - val repo2 = underTest.getRepoForSubId(SUB_CM_ID) + assertThat(activeRepo).isNull() - assertThat(repo1).isSameInstanceAs(repo2) + // 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() - @Test - fun testConnectionRepository_carrierMergedAndMobileSubs_usesCorrectRepos() = - testScope.runTest { - collectLastValue(underTest.subscriptions) + // WHEN requesting a connection repository for the subscription + val newRepo = + kairos.transact { underTest.mobileConnectionsBySubId.map { it[SUB_2_ID] }.sample() } - 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() - } + // THEN the newly request repo has been cached and reused + assertThat(activeRepo).isSameInstanceAs(newRepo) + } @Test - fun testSubscriptions_subNoLongerCarrierMerged_repoUpdates() = - testScope.runTest { - collectLastValue(underTest.subscriptions) + fun testConnectionRepository_validSubId_isCached() = runTest { + underTest - 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() + subscriptionManager.stub { + on { completeActiveSubscriptionInfoList } doReturn listOf(SUB_1) } + getSubscriptionCallback().onSubscriptionsChanged() - @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() + val repo1 by underTest.mobileConnectionsBySubId.map { it[SUB_1_ID] }.collectLastValue() + val repo2 by underTest.mobileConnectionsBySubId.map { it[SUB_1_ID] }.collectLastValue() - // 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() - } + assertThat(repo1).isNotNull() + assertThat(repo1).isSameInstanceAs(repo2) + } - @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() + fun testConnectionRepository_carrierMergedSubId_isCached() = runTest { + underTest - // THEN the repo's state is updated despite no listeners - assertThat(underTest.isDeviceEmergencyCallCapable.value).isEqualTo(true) + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_CM) + setWifiState(isCarrierMerged = true) + subscriptionManager.stub { + on { completeActiveSubscriptionInfoList } doReturn listOf(SUB_CM) } + getSubscriptionCallback().onSubscriptionsChanged() - @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 - } - } + val repo1 by underTest.mobileConnectionsBySubId.map { it[SUB_CM_ID] }.collectLastValue() + val repo2 by underTest.mobileConnectionsBySubId.map { it[SUB_CM_ID] }.collectLastValue() - // 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() - } + assertThat(repo1).isNotNull() + assertThat(repo1).isSameInstanceAs(repo2) + } + @SuppressLint("UnspecifiedRegisterReceiverFlag") @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() + fun testDeviceEmergencyCallState_eagerlyChecksState() = runTest { + val latest by underTest.isDeviceEmergencyCallCapable.collectLastValue() - // THEN the device is in ECM, because one of the service states is - assertThat(latest).isFalse() + // Value starts out false + assertThat(latest).isFalse() + telephonyManager.stub { on { activeModemCount } doReturn 1 } + whenever(telephonyManager.getServiceStateForSlot(any())).thenAnswer { _ -> + ServiceState().apply { isEmergencyOnly = true } } - @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() + // WHEN an appropriate intent gets sent out + val intent = serviceStateIntent(subId = -1) + broadcastDispatcher.sendIntentToMatchingReceiversOnly(applicationContext, intent) + testScope.runCurrent() - assertThat(underTest.getSubIdRepoCache()).containsExactly(SUB_1_ID, repo1) - } + // THEN the repo's state is updated despite no listeners + assertThat(latest).isEqualTo(true) + } @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) + fun testDeviceEmergencyCallState_aggregatesAcrossSlots_oneTrue() = runTest { + val latest by underTest.isDeviceEmergencyCallCapable.collectLastValue() - 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) + // 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 = true } + 3 -> ServiceState().apply { isEmergencyOnly = false } + else -> null + } } - /** Regression test for b/261706421 */ - @Test - @Ignore("b/333912012") - fun testConnectionsCache_clearMultipleSubscriptionsAtOnce_doesNotThrow() = - 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) - - // All subscriptions disappear - whenever(subscriptionManager.completeActiveSubscriptionInfoList).thenReturn(listOf()) - getSubscriptionCallback().onSubscriptionsChanged() + // GIVEN a broadcast goes out for the appropriate subID + val intent = serviceStateIntent(subId = -1) + broadcastDispatcher.sendIntentToMatchingReceiversOnly(applicationContext, intent) + testScope.runCurrent() - assertThat(underTest.getSubIdRepoCache()).isEmpty() - } + // THEN the device is in ECM, because one of the service states is + assertThat(latest).isTrue() + } @Test - fun testConnectionsCache_keepsReposCached() = - testScope.runTest { - // Collect subscriptions to start the job - collectLastValue(underTest.subscriptions) - - whenever(subscriptionManager.completeActiveSubscriptionInfoList) - .thenReturn(listOf(SUB_1)) - getSubscriptionCallback().onSubscriptionsChanged() - - val repo1_1 = underTest.getRepoForSubId(SUB_1_ID) - - // All subscriptions disappear - whenever(subscriptionManager.completeActiveSubscriptionInfoList).thenReturn(listOf()) - getSubscriptionCallback().onSubscriptionsChanged() + fun testDeviceEmergencyCallState_aggregatesAcrossSlots_allFalse() = runTest { + val latest by underTest.isDeviceEmergencyCallCapable.collectLastValue() - // 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) + // 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 testConnectionsCache_doesNotDropReferencesThatHaveBeenRealized() = - testScope.runTest { - // Collect subscriptions to start the job - 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)) - getSubscriptionCallback().onSubscriptionsChanged() + // THEN the device is in ECM, because one of the service states is + assertThat(latest).isFalse() + } - // Client grabs a reference to a repository, but doesn't keep it around - underTest.getRepoForSubId(SUB_1_ID) + @Test + fun testConnectionCache_clearsInvalidSubscriptions() = runTest { + underTest - // All subscriptions disappear - whenever(subscriptionManager.completeActiveSubscriptionInfoList).thenReturn(listOf()) - getSubscriptionCallback().onSubscriptionsChanged() + subscriptionManager.stub { + on { completeActiveSubscriptionInfoList } doReturn listOf(SUB_1, SUB_2) + } + getSubscriptionCallback().onSubscriptionsChanged() - val repo1 = underTest.getRepoForSubId(SUB_1_ID) + val repoCache by underTest.mobileConnectionsBySubId.collectLastValue() - assertThat(repo1).isNotNull() - } + assertThat(repoCache?.keys).containsExactly(SUB_1_ID, SUB_2_ID) - @Test - fun testConnectionRepository_invalidSubId_doesNotThrow() = - testScope.runTest { - underTest.getRepoForSubId(SUB_1_ID) - // No exception + // SUB_2 disappears + subscriptionManager.stub { + on { completeActiveSubscriptionInfoList } doReturn listOf(SUB_1) } + getSubscriptionCallback().onSubscriptionsChanged() - @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()) - } + assertThat(repoCache?.keys).containsExactly(SUB_1_ID) + } @Test - fun testDefaultDataSubId_updatesOnBroadcast() = - testScope.runTest { - val latest by collectLastValue(underTest.defaultDataSubId) - - assertThat(latest).isEqualTo(null) + fun testConnectionCache_clearsInvalidSubscriptions_includingCarrierMerged() = runTest { + underTest - val intent2 = - Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED) - .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_2_ID) - fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent2) + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, WIFI_NETWORK_CAPS_CM) + setWifiState(isCarrierMerged = true) + subscriptionManager.stub { + on { completeActiveSubscriptionInfoList } doReturn listOf(SUB_1, SUB_2, SUB_CM) + } + getSubscriptionCallback().onSubscriptionsChanged() - assertThat(latest).isEqualTo(SUB_2_ID) + val repoCache by underTest.mobileConnectionsBySubId.collectLastValue() - val intent1 = - Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED) - .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_1_ID) - fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent1) + assertThat(repoCache?.keys).containsExactly(SUB_1_ID, SUB_2_ID, SUB_CM_ID) - assertThat(latest).isEqualTo(SUB_1_ID) + // SUB_2 and SUB_CM disappear + subscriptionManager.stub { + on { completeActiveSubscriptionInfoList } doReturn listOf(SUB_1) } + getSubscriptionCallback().onSubscriptionsChanged() - @Test - fun defaultDataSubId_fetchesInitialValueOnStart() = - testScope.runTest { - subscriptionManagerProxy.defaultDataSubId = 2 - val latest by collectLastValue(underTest.defaultDataSubId) - - assertThat(latest).isEqualTo(2) - } + assertThat(repoCache?.keys).containsExactly(SUB_1_ID) + } + /** Regression test for b/261706421 */ @Test - fun defaultDataSubId_filtersOutInvalidSubIds() = - testScope.runTest { - subscriptionManagerProxy.defaultDataSubId = INVALID_SUBSCRIPTION_ID - val latest by collectLastValue(underTest.defaultDataSubId) + fun testConnectionsCache_clearMultipleSubscriptionsAtOnce_doesNotThrow() = runTest { + underTest - assertThat(latest).isNull() + subscriptionManager.stub { + on { completeActiveSubscriptionInfoList } doReturn listOf(SUB_1, SUB_2) } + getSubscriptionCallback().onSubscriptionsChanged() - @Test - fun defaultDataSubId_filtersOutInvalidSubIds_fromValidToInvalid() = - testScope.runTest { - subscriptionManagerProxy.defaultDataSubId = 2 - val latest by collectLastValue(underTest.defaultDataSubId) + val repoCache by underTest.mobileConnectionsBySubId.collectLastValue() - assertThat(latest).isEqualTo(2) + assertThat(repoCache?.keys).containsExactly(SUB_1_ID, SUB_2_ID) - val intent = - Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED) - .putExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID) - fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent) + // All subscriptions disappear + subscriptionManager.stub { on { completeActiveSubscriptionInfoList } doReturn listOf() } + getSubscriptionCallback().onSubscriptionsChanged() - assertThat(latest).isNull() - } + assertThat(repoCache).isEmpty() + } @Test - fun defaultDataSubId_fetchesCurrentOnRestart() = - testScope.runTest { - subscriptionManagerProxy.defaultDataSubId = 2 - var latest: Int? = null - var job = underTest.defaultDataSubId.onEach { latest = it }.launchIn(this) - runCurrent() + fun testDefaultDataSubId_updatesOnBroadcast() = runTest { + val latest by underTest.defaultDataSubId.collectLastValue() - assertThat(latest).isEqualTo(2) + assertThat(latest).isEqualTo(null) - job.cancel() + val intent2 = + Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED) + .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_2_ID) + broadcastDispatcher.sendIntentToMatchingReceiversOnly(applicationContext, intent2) - // Collectors go away but come back later + assertThat(latest).isEqualTo(SUB_2_ID) - latest = null + val intent1 = + Intent(TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED) + .putExtra(PhoneConstants.SUBSCRIPTION_KEY, SUB_1_ID) + broadcastDispatcher.sendIntentToMatchingReceiversOnly(applicationContext, intent1) - subscriptionManagerProxy.defaultDataSubId = 1 - - job = underTest.defaultDataSubId.onEach { latest = it }.launchIn(this) - runCurrent() + assertThat(latest).isEqualTo(SUB_1_ID) + } - assertThat(latest).isEqualTo(1) + @Test + fun defaultDataSubId_fetchesInitialValueOnStart() = runTest { + subscriptionManagerProxy.fake.defaultDataSubId = 2 + val latest by underTest.defaultDataSubId.collectLastValue() - job.cancel() - } + assertThat(latest).isEqualTo(2) + } @Test - fun mobileIsDefault_startsAsFalse() { - assertThat(underTest.mobileIsDefault.value).isFalse() + fun mobileIsDefault_startsAsFalse() = runTest { + assertThat(underTest.mobileIsDefault.collectLastValue().value).isFalse() } @Test - fun mobileIsDefault_capsHaveCellular_isDefault() = - testScope.runTest { - val caps = - mock<NetworkCapabilities>().also { - whenever(it.hasTransport(TRANSPORT_CELLULAR)).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).isTrue() - } + assertThat(latest).isTrue() + } @Test - fun mobileIsDefault_capsDoNotHaveCellular_isNotDefault() = - testScope.runTest { - val caps = - mock<NetworkCapabilities>().also { - whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(false) - } + 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() = - 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) - } + 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 collectLastValue(underTest.mobileIsDefault) + val latest by underTest.mobileIsDefault.collectLastValue() - getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) + getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, caps) - assertThat(latest).isTrue() - } + assertThat(latest).isTrue() + } @Test - fun mobileIsDefault_wifiDefault_mobileNotDefault() = - testScope.runTest { - val caps = - mock<NetworkCapabilities>().also { - whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true) - } + fun mobileIsDefault_wifiDefault_mobileNotDefault() = runTest { + val caps = + Mockito.mock(NetworkCapabilities::class.java).stub { + on { hasTransport(TRANSPORT_WIFI) } 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).isFalse() + } @Test - fun mobileIsDefault_ethernetDefault_mobileNotDefault() = - testScope.runTest { - val caps = - mock<NetworkCapabilities>().also { - whenever(it.hasTransport(TRANSPORT_ETHERNET)).thenReturn(true) - } + fun mobileIsDefault_ethernetDefault_mobileNotDefault() = runTest { + val caps = + Mockito.mock(NetworkCapabilities::class.java).stub { + on { hasTransport(TRANSPORT_ETHERNET) } 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).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) - - 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)) - } + fun hasCarrierMergedConnection_isCarrierMergedViaUnderlyingWifi_isTrue() = runTest { + val latest by underTest.hasCarrierMergedConnection.collectLastValue() - 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) - - val underlyingCarrierMergedNetwork = mock<Network>() - val carrierMergedInfo = - mock<WifiInfo>().apply { - whenever(this.isCarrierMerged).thenReturn(true) - whenever(this.isPrimary).thenReturn(true) - } + fun hasCarrierMergedConnection_isCarrierMergedViaUnderlyingCellular_isTrue() = runTest { + val latest by underTest.hasCarrierMergedConnection.collectLastValue() - // The Wifi network that is under the VCN network - val physicalWifiNetwork = newWifiNetwork(carrierMergedInfo) + val underlyingCarrierMergedNetwork = mock<Network>() + val carrierMergedInfo = + mock<WifiInfo> { + on { isCarrierMerged } doReturn true + on { isPrimary } doReturn true + } - 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)) - } + // The Wifi network that is under the VCN network + val physicalWifiNetwork = newWifiNetwork(carrierMergedInfo) - getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, mainCapabilities) - setWifiState(isCarrierMerged = true) + 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) + } - // THEN there's a carrier merged connection - assertThat(latest).isTrue() - } + 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 @@ class MobileConnectionsRepositoryKairosTest : SysuiTestCase() { /** 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 @@ class MobileConnectionsRepositoryKairosTest : SysuiTestCase() { } @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() - - // 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(), - ) + fun config_initiallyFromContext() = runTest { + overrideResource(R.bool.config_showMin3G, true) + val configFromContext = MobileMappings.Config.readConfig(applicationContext) + assertThat(configFromContext.showAtLeast3G).isTrue() - val latest by collectLastValue(underTest.defaultDataSubRatConfig) + val latest by underTest.defaultDataSubRatConfig.collectLastValue() - 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() - - overrideResource(R.bool.config_showMin3G, true) - val configFromContext = MobileMappings.Config.readConfig(context) - assertThat(configFromContext.showAtLeast3G).isTrue() - - // WHEN the change event is fired - fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly( - context, - Intent(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED), - ) + fun carrierConfig_initialValueIsFetched() = runTest { + underTest + testScope.runCurrent() - // WHEN collection starts AFTER the broadcast is sent out - val latest by collectLastValue(underTest.defaultDataSubRatConfig) + // Value starts out false + assertThat(underTest.defaultDataSubRatConfig.sample().showAtLeast3G).isFalse() - // THEN the config has the updated value - assertThat(latest!!.areEqual(configFromContext)).isTrue() - assertThat(latest!!.showAtLeast3G).isTrue() - } + overrideResource(R.bool.config_showMin3G, true) + val configFromContext = MobileMappings.Config.readConfig(applicationContext) + assertThat(configFromContext.showAtLeast3G).isTrue() - @Test - fun activeDataChange_inSameGroup_emitsUnit() = - testScope.runTest { - val latest by collectLastValue(underTest.activeSubChangedInGroupEvent) + assertThat(broadcastDispatcher.numReceiversRegistered).isAtLeast(1) + + // WHEN the change event is fired + broadcastDispatcher.sendIntentToMatchingReceiversOnly( + applicationContext, + Intent(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED), + ) + testScope.runCurrent() - getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>() - .onActiveDataSubscriptionIdChanged(SUB_3_ID_GROUPED) - getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>() - .onActiveDataSubscriptionIdChanged(SUB_4_ID_GROUPED) + // WHEN collection starts AFTER the broadcast is sent out + val latest by underTest.defaultDataSubRatConfig.collectLastValue() - assertThat(latest).isEqualTo(Unit) - } + // 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_notInSameGroup_doesNotEmit() = - testScope.runTest { - val latest by collectLastValue(underTest.activeSubChangedInGroupEvent) + 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() - getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>() - .onActiveDataSubscriptionIdChanged(SUB_3_ID_GROUPED) - getTelephonyCallbackForType<ActiveDataSubscriptionIdListener>() - .onActiveDataSubscriptionIdChanged(SUB_1_ID) + assertThat(eventCount).isEqualTo(1) + } - assertThat(latest).isEqualTo(null) + @Test + 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) - @Test - fun noSubscriptionsInEcmMode_notInEcmMode() = - testScope.runTest { - whenever(telephonyManager.emergencyCallbackMode).thenReturn(false) + assertThat(anySimSecure).isTrue() + } - runCurrent() + @Test + fun noSubscriptionsInEcmMode_notInEcmMode() = runTest { + val latest by underTest.isInEcmMode.collectLastValue() + testScope.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() - } - - private fun TestScope.getSubscriptionCallback(): - SubscriptionManager.OnSubscriptionsChangedListener { - runCurrent() - val callbackCaptor = argumentCaptor<SubscriptionManager.OnSubscriptionsChangedListener>() - verify(subscriptionManager) - .addOnSubscriptionsChangedListener(any(), callbackCaptor.capture()) - return callbackCaptor.value!! + wifiPickerTrackerCallback.allValues.forEach { it.onWifiEntriesChanged() } } - 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 @@ class MobileConnectionsRepositoryKairosTest : SysuiTestCase() { 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 @@ class MobileConnectionsRepositoryKairosTest : SysuiTestCase() { 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 @@ class MobileConnectionsRepositoryKairosTest : SysuiTestCase() { // 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 @@ class MobileConnectionsRepositoryKairosTest : SysuiTestCase() { ) 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 9fddbfb16d4d..a1546949835c 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 @@ fun <T> Flow<List<T>>.logDiffsForTable( 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 a18495e5070b..61c0055ada97 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 android.net.wifi.WifiManager 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.CarrierCon 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.Named 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 @@ abstract class StatusBarPipelineModule { @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 8e53f6443049..2e796263afa9 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 @@ interface MobileConnectionRepositoryKairos { 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 @@ interface MobileConnectionRepositoryKairos { * 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 @@ interface MobileConnectionRepositoryKairos { * 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 @@ interface MobileConnectionRepositoryKairos { * * 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 @@ interface MobileConnectionRepositoryKairos { * 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 b3cbbfd6a05a..79bfb6e48171 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 android.telephony.SubscriptionManager 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 @@ interface MobileConnectionsRepositoryKairos { * 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 @@ interface MobileConnectionsRepositoryKairos { * * 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 @@ interface MobileConnectionsRepositoryKairos { * * 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 @@ interface MobileConnectionsRepositoryKairos { * [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 000000000000..64144d9a0ab5 --- /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 3c855a9387d6..1f5b849c56cc 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 android.os.Bundle 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 @@ import kotlinx.coroutines.flow.stateIn * 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 @@ constructor( } override fun onDemoModeStarted() { - demoMobileConnectionsRepository.startProcessingCommands() trySend(true) } override fun onDemoModeFinished() { - demoMobileConnectionsRepository.stopProcessingCommands() trySend(false) } } @@ -93,114 +99,73 @@ constructor( 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 712ebdc1d0ed..a244feb1739a 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 @@ import kotlinx.coroutines.flow.stateIn * [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, - ) - .stateIn(scope, SharingStarted.WhileSubscribed(), _carrierId.value) - - private val _inflateSignalStrength: MutableStateFlow<Boolean> = MutableStateFlow(false) - override val inflateSignalStrength = - _inflateSignalStrength - .logDiffsForTable( - tableLogBuffer, - columnName = "inflate", - initialValue = _inflateSignalStrength.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(), _inflateSignalStrength.value) + .holdState(First(initialState)) + } + + override val carrierId: State<Int> = + lastEvent + .map { it.firstOrNull()?.carrierId ?: INVALID_SUBSCRIPTION_ID } + .also { + onActivated { + logDiffsForTable( + intState = it, + tableLogBuffer = tableLogBuffer, + columnName = COL_CARRIER_ID, + ) + } + } + + override val inflateSignalStrength: State<Boolean> = buildState { + mobileEvents + .map { ev -> ev.inflateStrength } + .holdState(false) + .also { logDiffsForTable(it, tableLogBuffer, "", columnName = "inflate") } + } // 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 allowNetworkSliceIndicator: State<Boolean> = stateOf(true) + + // 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 } } - .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 + .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) } } - 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() - - // 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 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 dee59bd8876d..925ee541bf73 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.content.Context 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.demo.model 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 + @AssistedFactory + fun interface Factory { + fun create(): DemoMobileConnectionsRepositoryKairos + } + + 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 var carrierMergedSubId: Int? = null + private val mobileEventsBySubId: GroupedEvents<Int, FakeNetworkEventModel> = + mobileEventsWithSubId.map { mapOf(it) }.groupByKey() - private var connectionRepoCache = mutableMapOf<Int, CacheContainer>() - private val subscriptionInfoCache = mutableMapOf<Int, SubscriptionModel>() - val demoModeFinishedEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1) + private val carrierMergedEvents: Events<FakeWifiEventModel.CarrierMerged> = + wifiEvents.filterIsInstance<FakeWifiEventModel.CarrierMerged>() - private val _subscriptions = MutableStateFlow<List<SubscriptionModel>>(listOf()) - override val subscriptions = - _subscriptions - .onEach { infos -> dropUnusedReposFromCache(infos) } - .stateIn(scope, SharingStarted.WhileSubscribed(), _subscriptions.value) + private val wifiEventsBySubId: GroupedEvents<Int, FakeWifiEventModel.CarrierMerged> = + carrierMergedEvents.groupBy { it.subscriptionId } - 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 } + 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) + } - connectionRepoCache = - connectionRepoCache - .filter { currentValidSubscriptionIds.contains(it.key) } - .toMutableMap() + 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 fun maybeCreateSubscription(subId: Int) { - if (!subscriptionInfoCache.containsKey(subId)) { - SubscriptionModel( + 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 } + } + } + + override val subscriptions: State<Collection<SubscriptionModel>> = + subscriptionsById.map { it.values } - _subscriptions.value = subscriptionInfoCache.values.toList() + 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 } } + private val reposBySubId: Incremental<Int, DemoMobileConnectionRepositoryKairos> = + buildIncremental { + subscriptionsById + .asIncremental() + .mapValues { (id, _) -> buildSpec { newRepo(id) } } + .applyLatestSpecForKey() + } + // 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, - ) + 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: StateFlow<MobileConnectionRepository?> = - activeMobileDataSubscriptionId - .map { getRepoForSubId(it) } - .stateIn( - scope, - SharingStarted.WhileSubscribed(), - getRepoForSubId(activeMobileDataSubscriptionId.value), - ) + 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 isAnySimSecure: Flow<Boolean> = flowOf(getIsAnySimSecure()) + override val isDeviceEmergencyCallCapable: State<Boolean> = stateOf(false) - override fun getIsAnySimSecure(): Boolean = false + override val isAnySimSecure: State<Boolean> = stateOf(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 @@ constructor( * 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 fun getRepoForSubId(subId: Int): DemoMobileConnectionRepository { - val current = connectionRepoCache[subId]?.repo - if (current != null) { - return current - } - - val new = createDemoMobileConnectionRepo(subId) - connectionRepoCache[subId] = new - return new.repo - } - - 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) + override val defaultConnectionIsValidated: State<Boolean> = stateOf(true) + + override val isInEcmMode: State<Boolean> = stateOf(false) + + override val mobileConnectionsBySubId: Incremental<Int, DemoMobileConnectionRepositoryKairos> + get() = reposBySubId + + 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 b37938464bd2..f32938335e6d 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_NONE 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 @@ constructor(demoModeController: DemoModeController, @Background scope: Coroutine 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 42171d0dc2b5..54162bb75f3e 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 @@ import com.android.settingslib.SignalIcon * 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 @@ sealed interface FakeNetworkEventModel { 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 4d80efc0a5e7..d61d11bcf6b7 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.CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN 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 @@ import kotlinx.coroutines.withContext * 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 @@ class CarrierMergedConnectionRepositoryKairos( 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 @@ class CarrierMergedConnectionRepositoryKairos( } } - override val cdmaRoaming: StateFlow<Boolean> = MutableStateFlow(ROAMING).asStateFlow() - - 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 cdmaRoaming: State<Boolean> = stateOf(ROAMING) - override val carrierName: StateFlow<NetworkNameModel> = networkName + 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 numberOfLevels: StateFlow<Int> = - wifiRepository.wifiNetwork - .map { - if (it is WifiNetworkModel.CarrierMerged) { - it.numberOfLevels - } else { - DEFAULT_NUM_LEVELS - } + override val carrierName: State<NetworkNameModel> + get() = networkName + + 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 cdmaLevel = - network - .map { it?.level ?: SIGNAL_STRENGTH_NONE_OR_UNKNOWN } - .stateIn(scope, SharingStarted.WhileSubscribed(), SIGNAL_STRENGTH_NONE_OR_UNKNOWN) - - override val dataActivityDirection = wifiRepository.wifiActivity - - override val resolvedNetworkType = - network - .map { - if (it != null) { - ResolvedNetworkType.CarrierMergedNetworkType - } else { - ResolvedNetworkType.UnknownNetworkType - } + } + + override val primaryLevel: State<Int> = + network.map { it?.level ?: SIGNAL_STRENGTH_NONE_OR_UNKNOWN } + + override val cdmaLevel: State<Int> = + network.map { it?.level ?: SIGNAL_STRENGTH_NONE_OR_UNKNOWN } + + override val dataActivityDirection: State<DataActivityModel> = buildState { + wifiRepository.wifiActivity.toState() + } + + 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 dataEnabled: StateFlow<Boolean> = wifiRepository.isWifiEnabled + override val hasPrioritizedNetworkCapabilities: State<Boolean> = stateOf(false) - 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 @@ class CarrierMergedConnectionRepositoryKairos( @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 38e6216f7cfa..1a8ca9577bd7 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 @@ package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod 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 @@ import kotlinx.coroutines.flow.stateIn * 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 - } - - /** - * 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) +@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() { + + 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 = - activeRepo - .flatMapLatest { it.isEmergencyOnly } - .logDiffsForTable( - tableLogBuffer, - columnName = COL_EMERGENCY, - initialValue = activeRepo.value.isEmergencyOnly.value, - ) - .stateIn( - scope, - SharingStarted.WhileSubscribed(), - activeRepo.value.isEmergencyOnly.value, - ) - - override val isRoaming = - activeRepo - .flatMapLatest { it.isRoaming } - .logDiffsForTable( - tableLogBuffer, - columnName = COL_ROAMING, - initialValue = activeRepo.value.isRoaming.value, - ) - .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.isRoaming.value) - - override val operatorAlphaShort = + override val isEmergencyOnly: State<Boolean> = activeRepo - .flatMapLatest { it.operatorAlphaShort } - .logDiffsForTable( - tableLogBuffer, - columnName = COL_OPERATOR, - initialValue = activeRepo.value.operatorAlphaShort.value, - ) - .stateIn( - scope, - SharingStarted.WhileSubscribed(), - activeRepo.value.operatorAlphaShort.value, - ) - - override val isInService = - activeRepo - .flatMapLatest { it.isInService } - .logDiffsForTable( - tableLogBuffer, - columnName = COL_IS_IN_SERVICE, - initialValue = activeRepo.value.isInService.value, - ) - .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.isInService.value) - - override val isNonTerrestrial = + .flatMap { it.isEmergencyOnly } + .also { + onActivated { logDiffsForTable(it, tableLogBuffer, columnName = COL_EMERGENCY) } + } + + override val isRoaming: 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, - ) - - override val isGsm = + .flatMap { it.isRoaming } + .also { onActivated { logDiffsForTable(it, tableLogBuffer, columnName = COL_ROAMING) } } + + override val operatorAlphaShort: State<String?> = activeRepo - .flatMapLatest { it.isGsm } - .logDiffsForTable( - tableLogBuffer, - columnName = COL_IS_GSM, - initialValue = activeRepo.value.isGsm.value, - ) - .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.isGsm.value) - - override val cdmaLevel = + .flatMap { it.operatorAlphaShort } + .also { + onActivated { logDiffsForTable(it, tableLogBuffer, columnName = COL_OPERATOR) } + } + + override val isInService: State<Boolean> = activeRepo - .flatMapLatest { it.cdmaLevel } - .logDiffsForTable( - tableLogBuffer, - columnName = COL_CDMA_LEVEL, - initialValue = activeRepo.value.cdmaLevel.value, - ) - .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.cdmaLevel.value) - - override val primaryLevel = + .flatMap { it.isInService } + .also { + onActivated { logDiffsForTable(it, tableLogBuffer, columnName = COL_IS_IN_SERVICE) } + } + + override val isNonTerrestrial: State<Boolean> = activeRepo - .flatMapLatest { it.primaryLevel } - .logDiffsForTable( - tableLogBuffer, - columnName = COL_PRIMARY_LEVEL, - initialValue = activeRepo.value.primaryLevel.value, - ) - .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.primaryLevel.value) - - override val satelliteLevel: StateFlow<Int> = + .flatMap { it.isNonTerrestrial } + .also { onActivated { logDiffsForTable(it, tableLogBuffer, columnName = COL_IS_NTN) } } + + override val isGsm: State<Boolean> = activeRepo - .flatMapLatest { it.satelliteLevel } - .logDiffsForTable( - tableLogBuffer, - columnName = COL_SATELLITE_LEVEL, - initialValue = activeRepo.value.satelliteLevel.value, - ) - .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.satelliteLevel.value) - - override val dataConnectionState = + .flatMap { it.isGsm } + .also { onActivated { logDiffsForTable(it, tableLogBuffer, columnName = COL_IS_GSM) } } + + override val cdmaLevel: State<Int> = activeRepo - .flatMapLatest { it.dataConnectionState } - .logDiffsForTable( - tableLogBuffer, - initialValue = activeRepo.value.dataConnectionState.value, - ) - .stateIn( - scope, - SharingStarted.WhileSubscribed(), - activeRepo.value.dataConnectionState.value, - ) - - override val dataActivityDirection = + .flatMap { it.cdmaLevel } + .also { + onActivated { logDiffsForTable(it, tableLogBuffer, columnName = COL_CDMA_LEVEL) } + } + + override val primaryLevel: State<Int> = activeRepo - .flatMapLatest { it.dataActivityDirection } - .logDiffsForTable( - tableLogBuffer, - initialValue = activeRepo.value.dataActivityDirection.value, - ) - .stateIn( - scope, - SharingStarted.WhileSubscribed(), - activeRepo.value.dataActivityDirection.value, - ) - - override val carrierNetworkChangeActive = + .flatMap { it.primaryLevel } + .also { + onActivated { logDiffsForTable(it, tableLogBuffer, columnName = COL_PRIMARY_LEVEL) } + } + + override val satelliteLevel: State<Int> = activeRepo - .flatMapLatest { it.carrierNetworkChangeActive } - .logDiffsForTable( - tableLogBuffer, - columnName = COL_CARRIER_NETWORK_CHANGE, - initialValue = activeRepo.value.carrierNetworkChangeActive.value, - ) - .stateIn( - scope, - SharingStarted.WhileSubscribed(), - activeRepo.value.carrierNetworkChangeActive.value, - ) - - override val resolvedNetworkType = + .flatMap { it.satelliteLevel } + .also { + onActivated { + logDiffsForTable(it, tableLogBuffer, columnName = COL_SATELLITE_LEVEL) + } + } + + override val dataConnectionState: State<DataConnectionState> = activeRepo - .flatMapLatest { it.resolvedNetworkType } - .logDiffsForTable( - tableLogBuffer, - initialValue = activeRepo.value.resolvedNetworkType.value, - ) - .stateIn( - scope, - SharingStarted.WhileSubscribed(), - activeRepo.value.resolvedNetworkType.value, - ) - - override val dataEnabled = + .flatMap { it.dataConnectionState } + .also { onActivated { logDiffsForTable(it, tableLogBuffer, columnPrefix = "") } } + + override val dataActivityDirection: State<DataActivityModel> = activeRepo - .flatMapLatest { it.dataEnabled } - .logDiffsForTable( - tableLogBuffer, - columnName = "dataEnabled", - initialValue = activeRepo.value.dataEnabled.value, - ) - .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.dataEnabled.value) - - override val inflateSignalStrength = + .flatMap { it.dataActivityDirection } + .also { onActivated { logDiffsForTable(it, tableLogBuffer, columnPrefix = "") } } + + override val carrierNetworkChangeActive: State<Boolean> = activeRepo - .flatMapLatest { it.inflateSignalStrength } - .logDiffsForTable( - tableLogBuffer, - columnName = "inflate", - initialValue = activeRepo.value.inflateSignalStrength.value, - ) - .stateIn( - scope, - SharingStarted.WhileSubscribed(), - activeRepo.value.inflateSignalStrength.value, - ) - - override val allowNetworkSliceIndicator = + .flatMap { it.carrierNetworkChangeActive } + .also { + onActivated { + logDiffsForTable(it, tableLogBuffer, columnName = COL_CARRIER_NETWORK_CHANGE) + } + } + + override val resolvedNetworkType: State<ResolvedNetworkType> = activeRepo - .flatMapLatest { it.allowNetworkSliceIndicator } - .logDiffsForTable( - tableLogBuffer, - columnName = "allowSlice", - initialValue = activeRepo.value.allowNetworkSliceIndicator.value, - ) - .stateIn( - scope, - SharingStarted.WhileSubscribed(), - activeRepo.value.allowNetworkSliceIndicator.value, - ) - - override val numberOfLevels = + .flatMap { it.resolvedNetworkType } + .also { onActivated { logDiffsForTable(it, tableLogBuffer, columnPrefix = "") } } + + override val dataEnabled: State<Boolean> = activeRepo - .flatMapLatest { it.numberOfLevels } - .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.numberOfLevels.value) + .flatMap { it.dataEnabled } + .also { + onActivated { logDiffsForTable(it, tableLogBuffer, columnName = "dataEnabled") } + } - override val networkName = + override val inflateSignalStrength: State<Boolean> = activeRepo - .flatMapLatest { it.networkName } - .logDiffsForTable( - tableLogBuffer, - columnPrefix = "intent", - initialValue = activeRepo.value.networkName.value, - ) - .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.networkName.value) - - override val carrierName = + .flatMap { it.inflateSignalStrength } + .also { onActivated { logDiffsForTable(it, tableLogBuffer, columnName = "inflate") } } + + override val allowNetworkSliceIndicator: State<Boolean> = activeRepo - .flatMapLatest { it.carrierName } - .logDiffsForTable( - tableLogBuffer, - columnPrefix = "sub", - initialValue = activeRepo.value.carrierName.value, - ) - .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.carrierName.value) - - override val isAllowedDuringAirplaneMode = + .flatMap { it.allowNetworkSliceIndicator } + .also { + onActivated { logDiffsForTable(it, tableLogBuffer, columnName = "allowSlice") } + } + + override val numberOfLevels: State<Int> = activeRepo.flatMap { it.numberOfLevels } + + override val networkName: State<NetworkNameModel> = activeRepo - .flatMapLatest { it.isAllowedDuringAirplaneMode } - .stateIn( - scope, - SharingStarted.WhileSubscribed(), - activeRepo.value.isAllowedDuringAirplaneMode.value, - ) - - override val hasPrioritizedNetworkCapabilities = + .flatMap { it.networkName } + .also { onActivated { logDiffsForTable(it, tableLogBuffer, columnPrefix = "intent") } } + + override val carrierName: State<NetworkNameModel> = activeRepo - .flatMapLatest { it.hasPrioritizedNetworkCapabilities } - .stateIn( - scope, - SharingStarted.WhileSubscribed(), - activeRepo.value.hasPrioritizedNetworkCapabilities.value, - ) + .flatMap { it.carrierName } + .also { onActivated { logDiffsForTable(it, tableLogBuffer, columnPrefix = "sub") } } + + override val isAllowedDuringAirplaneMode: State<Boolean> = + activeRepo.flatMap { it.isAllowedDuringAirplaneMode } + + override val hasPrioritizedNetworkCapabilities: State<Boolean> = + activeRepo.flatMap { it.hasPrioritizedNetworkCapabilities } - override suspend fun isInEcmMode(): Boolean = activeRepo.value.isInEcmMode() + override val isInEcmMode: State<Boolean> = activeRepo.flatMap { it.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 b4a45e24a0cb..bf7c2990b5ab 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 @@ sealed interface CallbackEvent { 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 @@ data class TelephonyCallbackState( 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 @@ data class TelephonyCallbackState( 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 000000000000..9b37f4896878 --- /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 a074acd32de4..abe72e17163b 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.ERI_ON 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.SubscriptionMod 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 @@ class MobileConnectionRepositoryKairosImpl( * 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 { - - override fun onCarrierNetworkChange(active: Boolean) { - logger.logOnCarrierNetworkChange(active, subId) - trySend(CallbackEvent.OnCarrierNetworkChange(active)) - } + 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) + emit(CallbackEvent.OnCarrierNetworkChange(active)) + } - override fun onCarrierRoamingNtnModeChanged(active: Boolean) { - logger.logOnCarrierRoamingNtnModeChanged(active) - trySend(CallbackEvent.OnCarrierRoamingNtnModeChanged(active)) - } + override fun onCarrierRoamingNtnModeChanged(active: Boolean) { + logger.logOnCarrierRoamingNtnModeChanged(active) + emit(CallbackEvent.OnCarrierRoamingNtnModeChanged(active)) + } - override fun onDataActivity(direction: Int) { - logger.logOnDataActivity(direction, subId) - trySend(CallbackEvent.OnDataActivity(direction)) - } + override fun onDataActivity(direction: Int) { + logger.logOnDataActivity(direction, subId) + emit(CallbackEvent.OnDataActivity(direction)) + } - override fun onDataEnabledChanged(enabled: Boolean, reason: Int) { - logger.logOnDataEnabledChanged(enabled, subId) - trySend(CallbackEvent.OnDataEnabledChanged(enabled)) - } + 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) - trySend(CallbackEvent.OnDataConnectionStateChanged(dataState)) - } + 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) - trySend(CallbackEvent.OnDisplayInfoChanged(telephonyDisplayInfo)) - } + override fun onDisplayInfoChanged(telephonyDisplayInfo: TelephonyDisplayInfo) { + logger.logOnDisplayInfoChanged(telephonyDisplayInfo, subId) + emit(CallbackEvent.OnDisplayInfoChanged(telephonyDisplayInfo)) + } - override fun onServiceStateChanged(serviceState: ServiceState) { - logger.logOnServiceStateChanged(serviceState, subId) - trySend(CallbackEvent.OnServiceStateChanged(serviceState)) - } + override fun onServiceStateChanged(serviceState: ServiceState) { + logger.logOnServiceStateChanged(serviceState, subId) + emit(CallbackEvent.OnServiceStateChanged(serviceState)) + } - override fun onSignalStrengthsChanged(signalStrength: SignalStrength) { - logger.logOnSignalStrengthsChanged(signalStrength, subId) - trySend(CallbackEvent.OnSignalStrengthChanged(signalStrength)) - } + override fun onSignalStrengthsChanged(signalStrength: SignalStrength) { + logger.logOnSignalStrengthsChanged(signalStrength, subId) + emit(CallbackEvent.OnSignalStrengthChanged(signalStrength)) + } - override fun onCarrierRoamingNtnSignalStrengthChanged( - signalStrength: NtnSignalStrength - ) { - logger.logNtnSignalStrengthChanged(signalStrength) - trySend( - CallbackEvent.OnCarrierRoamingNtnSignalStrengthChanged( - 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 isEmergencyOnly: State<Boolean> = serviceState.map { it?.isEmergencyOnly == true } - override val isRoaming = + 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 } - } else { - callbackEvents - .mapNotNull { it.onServiceStateChanged } - .map { it.serviceState.roaming } - } - .stateIn(scope, SharingStarted.WhileSubscribed(), false) + displayInfo.map { it?.isRoaming == true } + } else { + serviceState.map { it?.roaming == true } + } - override val operatorAlphaShort = - callbackEvents - .mapNotNull { it.onServiceStateChanged } - .map { it.serviceState.operatorAlphaShort } - .stateIn(scope, SharingStarted.WhileSubscribed(), null) + override val operatorAlphaShort: State<String?> = serviceState.map { it?.operatorAlphaShort } - override val isInService = - callbackEvents - .mapNotNull { it.onServiceStateChanged } - .map { Utils.isInService(it.serviceState) } - .stateIn(scope, SharingStarted.WhileSubscribed(), false) + override val isInService: State<Boolean> = + serviceState.map { it?.let(Utils::isInService) == true } - override val isNonTerrestrial = - callbackEvents - .mapNotNull { it.onCarrierRoamingNtnModeChanged } - .map { it.active } - .stateIn(scope, SharingStarted.WhileSubscribed(), false) + private val carrierRoamingNtnActive: State<Boolean> = buildState { + callbackEvents.mapNotNull { it.onCarrierRoamingNtnModeChanged?.active }.holdState(false) + } - override val isGsm = - callbackEvents - .mapNotNull { it.onSignalStrengthChanged } - .map { it.signalStrength.isGsm } - .stateIn(scope, SharingStarted.WhileSubscribed(), false) + override val isNonTerrestrial: State<Boolean> + get() = carrierRoamingNtnActive - 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) + private val signalStrength: State<SignalStrength?> = buildState { + callbackEvents.mapNotNull { it.onSignalStrengthChanged?.signalStrength }.holdState(null) + } - override val primaryLevel = - callbackEvents - .mapNotNull { it.onSignalStrengthChanged } - .map { it.signalStrength.level } - .stateIn(scope, SharingStarted.WhileSubscribed(), SIGNAL_STRENGTH_NONE_OR_UNKNOWN) + override val isGsm: State<Boolean> = signalStrength.map { it?.isGsm == true } - override val satelliteLevel: StateFlow<Int> = - callbackEvents - .mapNotNull { it.onCarrierRoamingNtnSignalStrengthChanged } - .map { it.signalStrength.level } - .stateIn(scope, SharingStarted.WhileSubscribed(), 0) + override val cdmaLevel: State<Int> = + signalStrength.map { + it?.getCellSignalStrengths(CellSignalStrengthCdma::class.java)?.firstOrNull()?.level + ?: SIGNAL_STRENGTH_NONE_OR_UNKNOWN + } - override val dataConnectionState = - callbackEvents - .mapNotNull { it.onDataConnectionStateChanged } - .map { it.dataState.toDataConnectionType() } - .stateIn(scope, SharingStarted.WhileSubscribed(), Disconnected) + override val primaryLevel: State<Int> = + signalStrength.map { it?.level ?: SIGNAL_STRENGTH_NONE_OR_UNKNOWN } - override val dataActivityDirection = + override val satelliteLevel: State<Int> = buildState { callbackEvents - .mapNotNull { it.onDataActivity } - .map { it.direction.toMobileDataActivityModel() } - .stateIn( - scope, - SharingStarted.WhileSubscribed(), - DataActivityModel(hasActivityIn = false, hasActivityOut = false), - ) + .mapNotNull { it.onCarrierRoamingNtnSignalStrengthChanged?.signalStrength?.level } + .holdState(0) + } - override val carrierNetworkChangeActive = + override val dataConnectionState: State<DataConnectionState> = buildState { callbackEvents - .mapNotNull { it.onCarrierNetworkChange } - .map { it.active } - .stateIn(scope, SharingStarted.WhileSubscribed(), false) + .mapNotNull { it.onDataConnectionStateChanged?.dataState?.toDataConnectionType() } + .holdState(Disconnected) + } - override val resolvedNetworkType = + override val dataActivityDirection: State<DataActivityModel> = buildState { 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) + .mapNotNull { it.onDataActivity?.direction?.toMobileDataActivityModel() } + .holdState(DataActivityModel(hasActivityIn = false, hasActivityOut = false)) + } - override val carrierName = - subscriptionModel - .map { - it?.let { model -> NetworkNameModel.SubscriptionDerived(model.carrierName) } - ?: defaultNetworkName + 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 { + DEFAULT_NUM_LEVELS } - .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 {} + + private val cdmaEnhancedRoamingIndicatorDisplayNumber: Transactional<Int?> = transactionally { + try { + telephonyManager.cdmaEnhancedRoamingIndicatorDisplayNumber + } catch (e: UnsupportedOperationException) { + // Handles the same as a function call failure + null + } + } - override val cdmaRoaming: StateFlow<Boolean> = + 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 @@ class MobileConnectionRepositoryKairosImpl( 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 @@ class MobileConnectionRepositoryKairosImpl( * 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 @@ class MobileConnectionRepositoryKairosImpl( ) == subId ) { logger.logServiceProvidersUpdatedBroadcast(intent) - trySend( + emit( intent.toNetworkNameModel(networkNameSeparator) ?: defaultNetworkName ) @@ -420,22 +406,28 @@ class MobileConnectionRepositoryKairosImpl( awaitClose { context.unregisterReceiver(receiver) } } - .flowOn(bgDispatcher) - .stateIn(scope, SharingStarted.Eagerly, defaultNetworkName) + .holdState(defaultNetworkName) + } - override val dataEnabled = run { - val initial = telephonyManager.isDataConnectionAllowed + override val dataEnabled: State<Boolean> = buildState { callbackEvents - .mapNotNull { it.onDataEnabledChanged } - .map { it.enabled } - .stateIn(scope, SharingStarted.WhileSubscribed(), initial) + .mapNotNull { it.onDataEnabledChanged?.enabled } + .holdState(telephonyManager.isDataConnectionAllowed) } - override suspend fun isInEcmMode(): Boolean = - withContext(bgDispatcher) { telephonyManager.emergencyCallbackMode } + 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 @@ class MobileConnectionRepositoryKairosImpl( * 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 @@ class MobileConnectionRepositoryKairosImpl( awaitClose { connectivityManager.unregisterNetworkCallback(callback) } } - .flowOn(bgDispatcher) - .stateIn(scope, SharingStarted.WhileSubscribed(), 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( + .holdState(false) + } + + @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 51771dd12f0a..e46815954e64 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 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.dagger.MobileSummaryLog 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 @@ constructor( 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 @@ constructor( 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 @@ constructor( 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 @@ constructor( 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 - } + override val isDeviceEmergencyCallCapable: State<Boolean> = buildState { + rebuildOn(serviceStateChangedEvent) { asyncEvent { doAnyModemsSupportEmergencyCalls() } } + .switchEvents() + .holdState(false) + .also { + logDiffsForTable( + it, + tableLogger, + LOGGING_PREFIX, + columnName = "deviceEmergencyOnly", + ) + } + } - // 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 } + 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 } - .flowOn(bgDispatcher) - .distinctUntilChanged() - .logDiffsForTable( - tableLogger, - columnPrefix = LOGGING_PREFIX, - columnName = "deviceEmergencyOnly", - initialValue = false, - ) - .stateIn(scope, SharingStarted.Eagerly, 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() - - override fun getIsAnySimSecure() = keyguardUpdateMonitor.isSimPinSecure - - override fun getRepoForSubId(subId: Int): FullMobileConnectionRepository = - getOrCreateRepoForSubId(subId) - - private fun getOrCreateRepoForSubId(subId: Int) = - subIdRepositoryCache[subId]?.get() - ?: createRepositoryForSubId(subId).also { - subIdRepositoryCache[subId] = WeakReference(it) + .toState(false) + .also { + logDiffsForTable(it, tableLogger, LOGGING_PREFIX, columnName = "isAnySimSecure") } + } + + private val defaultConnections: State<DefaultConnectionModel> = buildState { + connectivityRepository.defaultConnections.toState() + } - override val mobileIsDefault: StateFlow<Boolean> = - connectivityRepository.defaultConnections + override val mobileIsDefault: State<Boolean> = + defaultConnections .map { it.mobile.isDefault } - .distinctUntilChanged() - .logDiffsForTable( - tableLogger, - columnPrefix = LOGGING_PREFIX, - columnName = "mobileIsDefault", - initialValue = false, - ) - .stateIn(scope, SharingStarted.WhileSubscribed(), false) + .also { + onActivated { + logDiffsForTable( + it, + tableLogger, + columnPrefix = LOGGING_PREFIX, + columnName = "mobileIsDefault", + ) + } + } - 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 @@ constructor( * 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) - - override suspend fun isInEcmMode(): Boolean { - if (telephonyManager.emergencyCallbackMode) { - return true - } - return with(subscriptions.value) { - any { getOrCreateRepoForSubId(it.subscriptionId).isInEcmMode() } - } - } - - private fun isValidSubId(subId: Int): Boolean = checkSub(subId, subscriptions.value) - - @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, - ) + .mapAsyncLatest { (prevVal, newVal) -> + if (isActiveSubChangeInGroup(prevVal, newVal)) Unit else null + } + .filterNotNull() } - private fun updateRepos(newInfos: List<SubscriptionModel>) { - subIdRepositoryCache.forEach { (subId, repo) -> - repo.get()?.setIsCarrierMerged(isCarrierMerged(subId)) + 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 } - } - - 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 + private val isInEcmModeTopLevel: State<Boolean> = + telephonyManagerState.map { it.second.isNotEmpty() } - 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 @@ constructor( 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 000000000000..a6209fa72b53 --- /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 000000000000..516053d00ee2 --- /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 000000000000..927209f84f1d --- /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 000000000000..d7f204183467 --- /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 000000000000..8cf3ee8d3c1c --- /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 000000000000..624b2cc12c89 --- /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 000000000000..f57cf99c1309 --- /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 5b709079ace7..a391c44018f5 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 @@ class FakeSubscriptionManagerProxy( 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 00bfa994aabd..bb254a18efb7 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 @@ package com.android.systemui.statusbar.pipeline.shared.data.repository 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 e44061a718d5..f560c502d606 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 @@ package com.android.systemui.statusbar.pipeline.wifi.data.repository 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 000000000000..1638cb7772ac --- /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() } |