Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4dede63a authored by Steve Elliott's avatar Steve Elliott
Browse files

[kairos] Kairos in Mobile Pipeline data layer

Flag: com.android.systemui.status_bar_mobile_icon_kairos
Bug: 383172066
Test: atest
Change-Id: I6f5e921609e4b482bffaf2d63c202d680a94bcef
parent a580b893
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -727,6 +727,7 @@ android_library {
        "TraceurCommon",
        "Traceur-res",
        "aconfig_settings_flags_lib",
        "kairos",
    ],
}

+81 −181
Original line number Diff line number Diff line
/*
 * 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,234 +19,135 @@ 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)

    private val kosmos =
        testKosmos().apply {
            useUnconfinedTestDispatcher()
            demoModeController.stub {
                // Never start in demo mode
        whenever(demoModeController.isInDemoMode).thenReturn(false)

        mobileDataSource =
            mock<DemoModeMobileConnectionDataSource>().also {
                whenever(it.mobileEvents).thenReturn(fakeNetworkEventsFlow)
                on { isInDemoMode } doReturn false
            }
        wifiDataSource =
            mock<DemoModeWifiDataSource>().also {
                whenever(it.wifiEvents).thenReturn(MutableStateFlow(null))
            wifiDataSource.stub { on { wifiEvents } doReturn MutableStateFlow(null) }
        }
        wifiRepository = FakeWifiRepository()

        connectivityRepository = FakeConnectivityRepository()
    private val Kosmos.underTest
        get() = mobileRepositorySwitcherKairos

        realRepo =
            MobileConnectionsRepositoryImpl(
                connectivityRepository,
                subscriptionManager,
                subscriptionManagerProxy,
                telephonyManager,
                logger,
                summaryLogger,
                mobileMappings,
                fakeBroadcastDispatcher,
                context,
                /* bgDispatcher = */ IMMEDIATE,
                scope,
                /* mainDispatcher = */ IMMEDIATE,
                FakeAirplaneModeRepository(),
                wifiRepository,
                mock(),
                mock(),
                mock(),
            )
    private val Kosmos.realRepo
        get() = mobileConnectionsRepositoryKairosImpl

        demoRepo =
            DemoMobileConnectionsRepository(
                mobileDataSource = mobileDataSource,
                wifiDataSource = wifiDataSource,
                scope = scope,
                context = context,
                logFactory = kosmos.tableLogBufferFactory,
            )

        underTest =
            MobileRepositorySwitcherKairos(
                scope = scope,
                realRepository = realRepo,
                demoMobileConnectionsRepository = demoRepo,
                demoModeController = demoModeController,
            )
    }

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

        startDemoMode()

            assertThat(latest).isEqualTo(demoRepo)
        assertThat(latest).isInstanceOf(DemoMobileConnectionsRepositoryKairos::class.java)

        finishDemoMode()

        assertThat(latest).isEqualTo(realRepo)

            job.cancel()
    }

    @Test
    fun subscriptionListUpdatesWhenDemoModeChanges() =
        runBlocking(IMMEDIATE) {
            whenever(demoModeController.isInDemoMode).thenReturn(false)
    fun subscriptionListUpdatesWhenDemoModeChanges() = runTest {
        demoModeController.stub { on { isInDemoMode } doReturn false }

            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
                .thenReturn(listOf(SUB_1, SUB_2))
        subscriptionManager.stub {
            on { completeActiveSubscriptionInfoList } doReturn listOf(SUB_1, SUB_2)
        }

            var latest: List<SubscriptionModel>? = null
            val job = underTest.subscriptions.onEach { latest = it }.launchIn(this)
        val latest by underTest.subscriptions.collectLastValue()

        // The real subscriptions has 2 subs
            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
                .thenReturn(listOf(SUB_1, SUB_2))
        getSubscriptionCallback().onSubscriptionsChanged()

        assertThat(latest).isEqualTo(listOf(MODEL_1, MODEL_2))

        // Demo mode turns on, and we should see only the demo subscriptions
        startDemoMode()
            fakeNetworkEventsFlow.value = validMobileEvent(subId = 3)
        demoModeMobileConnectionDataSourceKairos.fake.mobileEvents.emit(validMobileEvent(subId = 3))

        // Demo mobile connections repository makes arbitrarily-formed subscription info
        // objects, so just validate the data we care about
        assertThat(latest).hasSize(1)
            assertThat(latest!![0].subscriptionId).isEqualTo(3)
        assertThat(latest!!.first().subscriptionId).isEqualTo(3)

        finishDemoMode()

        assertThat(latest).isEqualTo(listOf(MODEL_1, MODEL_2))

            job.cancel()
    }

    private fun startDemoMode() {
        whenever(demoModeController.isInDemoMode).thenReturn(true)
    private fun KairosTestScope.startDemoMode() {
        demoModeController.stub { on { isInDemoMode } doReturn true }
        getDemoModeCallback().onDemoModeStarted()
    }

    private fun finishDemoMode() {
        whenever(demoModeController.isInDemoMode).thenReturn(false)
    private fun KairosTestScope.finishDemoMode() {
        demoModeController.stub { on { isInDemoMode } doReturn false }
        getDemoModeCallback().onDemoModeFinished()
    }

    private fun getSubscriptionCallback(): SubscriptionManager.OnSubscriptionsChangedListener {
        val callbackCaptor =
            kotlinArgumentCaptor<SubscriptionManager.OnSubscriptionsChangedListener>()
        verify(subscriptionManager)
            .addOnSubscriptionsChangedListener(any(), callbackCaptor.capture())
        return callbackCaptor.value
    private fun KairosTestScope.getSubscriptionCallback():
        SubscriptionManager.OnSubscriptionsChangedListener =
        argumentCaptor<SubscriptionManager.OnSubscriptionsChangedListener>()
            .apply {
                verify(subscriptionManager).addOnSubscriptionsChangedListener(any(), capture())
            }
            .lastValue

    private fun getDemoModeCallback(): DemoMode {
        val captor = kotlinArgumentCaptor<DemoMode>()
        verify(demoModeController).addCallback(captor.capture())
        return captor.value
    }
    private fun KairosTestScope.getDemoModeCallback(): DemoMode =
        argumentCaptor<DemoMode>()
            .apply { verify(demoModeController).addCallback(capture()) }
            .lastValue

    companion object {
        private val IMMEDIATE = Dispatchers.Main.immediate

        private const val SUB_1_ID = 1
        private const val SUB_1_NAME = "Carrier $SUB_1_ID"
        private val SUB_1 =
            mock<SubscriptionInfo>().also {
                whenever(it.subscriptionId).thenReturn(SUB_1_ID)
                whenever(it.carrierName).thenReturn(SUB_1_NAME)
                whenever(it.profileClass).thenReturn(PROFILE_CLASS_UNSET)
        private val SUB_1: SubscriptionInfo = mock {
            on { subscriptionId } doReturn SUB_1_ID
            on { carrierName } doReturn SUB_1_NAME
            on { profileClass } doReturn PROFILE_CLASS_UNSET
        }
        private val MODEL_1 =
            SubscriptionModel(
@@ -257,11 +158,10 @@ 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(
+74 −114
Original line number Diff line number Diff line
/*
 * 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,87 +23,61 @@ 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)
    private val kosmos =
        testKosmos().apply {
            useUnconfinedTestDispatcher()
            wifiDataSource.stub { on { wifiEvents } doReturn fakeWifiEventFlow }
        }

        connectionsRepo =
            DemoMobileConnectionsRepositoryKairos(
                mobileDataSource = mockDataSource,
                wifiDataSource = mockWifiDataSource,
                scope = testScope.backgroundScope,
                context = context,
                logFactory = kosmos.tableLogBufferFactory,
            )

        connectionsRepo.startProcessingCommands()
    }

    @After
    fun tearDown() {
        testScope.cancel()
    }
    private fun runTest(block: suspend KairosTestScope.() -> Unit) =
        kosmos.run { runKairosTest { block() } }

    @Test
    fun demoNetworkData() =
        testScope.runTest {
    fun demoNetworkData() = runTest {
        val underTest by
            demoMobileConnectionsRepositoryKairos.mobileConnectionsBySubId
                .map { it[subId] }
                .collectLastValue()
        val networkModel =
            FakeNetworkEventModel.Mobile(
                level = testCase.level,
@@ -117,62 +91,48 @@ internal class DemoMobileConnectionKairosParameterizedTest(private val testCase:
                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
        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 -> {
                kairos.transact {
                    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)
                    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.value).isEqualTo(model.roaming)
                assertThat(conn.networkName.value)
                    assertThat(conn.isRoaming.sample()).isEqualTo(model.roaming)
                    assertThat(conn.networkName.sample())
                        .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)
                    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)
                    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] */
+312 −439

File changed.

Preview size limit exceeded, changes collapsed.

+149 −207

File changed.

Preview size limit exceeded, changes collapsed.

Loading