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

Commit a580b893 authored by Steve Elliott's avatar Steve Elliott
Browse files

[kairos] Fork status bar mobile data layer

Flag: com.android.systemui.status_bar_mobile_icon_kairos
Bug: 383172066
Test: atest
Change-Id: Ieb8eb523b8737600b288d843cdffbe7e00adc572
parent 0a40deba
Loading
Loading
Loading
Loading
+273 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.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.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.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.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

/**
 * 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")
@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))
            }
        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(),
            )

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

    @Test
    fun activeRepoMatchesDemoModeSetting() =
        runBlocking(IMMEDIATE) {
            whenever(demoModeController.isInDemoMode).thenReturn(false)

            var latest: MobileConnectionsRepository? = null
            val job = underTest.activeRepo.onEach { latest = it }.launchIn(this)

            assertThat(latest).isEqualTo(realRepo)

            startDemoMode()

            assertThat(latest).isEqualTo(demoRepo)

            finishDemoMode()

            assertThat(latest).isEqualTo(realRepo)

            job.cancel()
        }

    @Test
    fun subscriptionListUpdatesWhenDemoModeChanges() =
        runBlocking(IMMEDIATE) {
            whenever(demoModeController.isInDemoMode).thenReturn(false)

            whenever(subscriptionManager.completeActiveSubscriptionInfoList)
                .thenReturn(listOf(SUB_1, SUB_2))

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

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

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

            finishDemoMode()

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

            job.cancel()
        }

    private fun startDemoMode() {
        whenever(demoModeController.isInDemoMode).thenReturn(true)
        getDemoModeCallback().onDemoModeStarted()
    }

    private fun finishDemoMode() {
        whenever(demoModeController.isInDemoMode).thenReturn(false)
        getDemoModeCallback().onDemoModeFinished()
    }

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

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

    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 MODEL_1 =
            SubscriptionModel(
                subscriptionId = SUB_1_ID,
                carrierName = SUB_1_NAME,
                profileClass = PROFILE_CLASS_UNSET,
            )

        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 MODEL_2 =
            SubscriptionModel(
                subscriptionId = SUB_2_ID,
                carrierName = SUB_2_NAME,
                profileClass = PROFILE_CLASS_UNSET,
            )
    }
}
+325 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.demo

import android.telephony.Annotation
import android.telephony.TelephonyManager
import android.telephony.TelephonyManager.DATA_ACTIVITY_NONE
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.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.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 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.
 */
@OptIn(ExperimentalCoroutinesApi::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 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()
    }

    @After
    fun tearDown() {
        testScope.cancel()
    }

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

    private fun TestScope.assertConnection(
        conn: DemoMobileConnectionRepository,
        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)

                // 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)
            }
            // MobileDisabled isn't combinatorial in nature, and is tested in
            // DemoMobileConnectionsRepositoryTest.kt
            else -> {}
        }

        job.cancel()
    }

    /** Matches [FakeNetworkEventModel] */
    internal data class TestCase(
        val level: Int,
        val dataType: SignalIcon.MobileIconGroup,
        val subId: Int,
        val carrierId: Int,
        val inflateStrength: Boolean,
        @Annotation.DataActivityType val activity: Int,
        val carrierNetworkChange: Boolean,
        val roaming: Boolean,
        val name: String,
        val slice: Boolean,
        val ntn: Boolean,
    ) {
        override fun toString(): String {
            return "INPUT(level=$level, " +
                "dataType=${dataType.name}, " +
                "subId=$subId, " +
                "carrierId=$carrierId, " +
                "inflateStrength=$inflateStrength, " +
                "activity=$activity, " +
                "carrierNetworkChange=$carrierNetworkChange, " +
                "roaming=$roaming, " +
                "name=$name," +
                "slice=$slice" +
                "ntn=$ntn)"
        }

        // Convenience for iterating test data and creating new cases
        fun modifiedBy(
            level: Int? = null,
            dataType: SignalIcon.MobileIconGroup? = null,
            subId: Int? = null,
            carrierId: Int? = null,
            inflateStrength: Boolean? = null,
            @Annotation.DataActivityType activity: Int? = null,
            carrierNetworkChange: Boolean? = null,
            roaming: Boolean? = null,
            name: String? = null,
            slice: Boolean? = null,
            ntn: Boolean? = null,
        ): TestCase =
            TestCase(
                level = level ?: this.level,
                dataType = dataType ?: this.dataType,
                subId = subId ?: this.subId,
                carrierId = carrierId ?: this.carrierId,
                inflateStrength = inflateStrength ?: this.inflateStrength,
                activity = activity ?: this.activity,
                carrierNetworkChange = carrierNetworkChange ?: this.carrierNetworkChange,
                roaming = roaming ?: this.roaming,
                name = name ?: this.name,
                slice = slice ?: this.slice,
                ntn = ntn ?: this.ntn,
            )
    }

    companion object {
        private val subId = 1

        private val booleanList = listOf(true, false)
        private val levels = listOf(0, 1, 2, 3)
        private val dataTypes =
            listOf(
                TelephonyIcons.THREE_G,
                TelephonyIcons.LTE,
                TelephonyIcons.FOUR_G,
                TelephonyIcons.NR_5G,
                TelephonyIcons.NR_5G_PLUS,
            )
        private val carrierIds = listOf(1, 10, 100)
        private val inflateStrength = booleanList
        private val activity =
            listOf(
                TelephonyManager.DATA_ACTIVITY_NONE,
                TelephonyManager.DATA_ACTIVITY_IN,
                TelephonyManager.DATA_ACTIVITY_OUT,
                TelephonyManager.DATA_ACTIVITY_INOUT,
            )
        private val carrierNetworkChange = booleanList
        // false first so the base case doesn't have roaming set (more common)
        private val roaming = listOf(false, true)
        private val names = listOf("name 1", "name 2")
        private val slice = listOf(false, true)
        private val ntn = listOf(false, true)

        @Parameters(name = "{0}") @JvmStatic fun data() = testData()

        /**
         * Generate some test data. For the sake of convenience, we'll parameterize only non-null
         * network event data. So given the lists of test data:
         * ```
         *    list1 = [1, 2, 3]
         *    list2 = [false, true]
         *    list3 = [a, b, c]
         * ```
         *
         * We'll generate test cases for:
         *
         * Test (1, false, a) Test (2, false, a) Test (3, false, a) Test (1, true, a) Test (1,
         * false, b) Test (1, false, c)
         *
         * NOTE: this is not a combinatorial product of all of the possible sets of parameters.
         * Since this test is built to exercise demo mode, the general approach is to define a
         * fully-formed "base case", and from there to make sure to use every valid parameter once,
         * by defining the rest of the test cases against the base case. Specific use-cases can be
         * added to the non-parameterized test, or manually below the generated test cases.
         */
        private fun testData(): List<TestCase> {
            val testSet = mutableSetOf<TestCase>()

            val baseCase =
                TestCase(
                    levels.first(),
                    dataTypes.first(),
                    subId,
                    carrierIds.first(),
                    inflateStrength.first(),
                    activity.first(),
                    carrierNetworkChange.first(),
                    roaming.first(),
                    names.first(),
                    slice.first(),
                    ntn.first(),
                )

            val tail =
                sequenceOf(
                        levels.map { baseCase.modifiedBy(level = it) },
                        dataTypes.map { baseCase.modifiedBy(dataType = it) },
                        carrierIds.map { baseCase.modifiedBy(carrierId = it) },
                        inflateStrength.map { baseCase.modifiedBy(inflateStrength = it) },
                        activity.map { baseCase.modifiedBy(activity = it) },
                        carrierNetworkChange.map { baseCase.modifiedBy(carrierNetworkChange = it) },
                        roaming.map { baseCase.modifiedBy(roaming = it) },
                        names.map { baseCase.modifiedBy(name = it) },
                        slice.map { baseCase.modifiedBy(slice = it) },
                        ntn.map { baseCase.modifiedBy(ntn = it) },
                    )
                    .flatten()

            testSet.add(baseCase)
            tail.toCollection(testSet)

            return testSet.toList()
        }
    }
}
+592 −0

File added.

Preview size limit exceeded, changes collapsed.

+302 −0

File added.

Preview size limit exceeded, changes collapsed.

+649 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading