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

Commit 739bb97f authored by Steve Elliott's avatar Steve Elliott Committed by Android (Google) Code Review
Browse files

Merge changes I0a80e591,I6f5e9216,Ieb8eb523 into main

* changes:
  [kairos] Fork status bar mobile domain layer
  [kairos] Kairos in Mobile Pipeline data layer
  [kairos] Fork status bar mobile data layer
parents d280057b ec071220
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",
    ],
}

+173 −0
Original line number Diff line number Diff line
/*
 * 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.SubscriptionInfo
import android.telephony.SubscriptionManager
import android.telephony.SubscriptionManager.PROFILE_CLASS_UNSET
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.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.DemoMobileConnectionsRepositoryKairos
import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.validMobileEvent
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Test
import org.junit.runner.RunWith
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
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
 */
@OptIn(ExperimentalKairosApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class MobileRepositorySwitcherKairosTest : SysuiTestCase() {
    private val kosmos =
        testKosmos().apply {
            useUnconfinedTestDispatcher()
            demoModeController.stub {
                // Never start in demo mode
                on { isInDemoMode } doReturn false
            }
            wifiDataSource.stub { on { wifiEvents } doReturn MutableStateFlow(null) }
        }

    private val Kosmos.underTest
        get() = mobileRepositorySwitcherKairos

    private val Kosmos.realRepo
        get() = mobileConnectionsRepositoryKairosImpl

    private fun runTest(block: suspend KairosTestScope.() -> Unit) =
        kosmos.run { runKairosTest { block() } }

    @Test
    fun activeRepoMatchesDemoModeSetting() = runTest {
        demoModeController.stub { on { isInDemoMode } doReturn false }

        val latest by underTest.activeRepo.collectLastValue()

        assertThat(latest).isEqualTo(realRepo)

        startDemoMode()

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

        finishDemoMode()

        assertThat(latest).isEqualTo(realRepo)
    }

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

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

        val latest by underTest.subscriptions.collectLastValue()

        // The real subscriptions has 2 subs
        getSubscriptionCallback().onSubscriptionsChanged()

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

        // Demo mode turns on, and we should see only the demo subscriptions
        startDemoMode()
        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!!.first().subscriptionId).isEqualTo(3)

        finishDemoMode()

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

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

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

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

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

    companion object {
        private const val SUB_1_ID = 1
        private const val SUB_1_NAME = "Carrier $SUB_1_ID"
        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,
                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: 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,
                carrierName = SUB_2_NAME,
                profileClass = PROFILE_CLASS_UNSET,
            )
    }
}
+285 −0
Original line number Diff line number Diff line
/*
 * 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.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.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.model.FakeWifiEventModel
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.MutableStateFlow
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 [DemoMobileConnectionsRepositoryKairos] results in the
 * correct flows emitting from the given connection.
 */
@OptIn(ExperimentalKairosApi::class)
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
internal class DemoMobileConnectionKairosParameterizedTest(private val testCase: TestCase) :
    SysuiTestCase() {

    private val Kosmos.fakeWifiEventFlow by Fixture { MutableStateFlow<FakeWifiEventModel?>(null) }

    private val kosmos =
        testKosmos().apply {
            useUnconfinedTestDispatcher()
            wifiDataSource.stub { on { wifiEvents } doReturn fakeWifiEventFlow }
        }

    private fun runTest(block: suspend KairosTestScope.() -> Unit) =
        kosmos.run { runKairosTest { block() } }

    @Test
    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 suspend fun KairosTestScope.assertConnection(
        conn: DemoMobileConnectionRepositoryKairos,
        model: FakeNetworkEventModel,
    ) {
        when (model) {
            is FakeNetworkEventModel.Mobile -> {
                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)
                }
            }
            // MobileDisabled isn't combinatorial in nature, and is tested in
            // DemoMobileConnectionsRepositoryTest.kt
            else -> {}
        }
    }

    /** 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()
        }
    }
}
+465 −0

File added.

Preview size limit exceeded, changes collapsed.

+244 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading