diff options
18 files changed, 1525 insertions, 1627 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelKairosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelKairosTest.kt index 57e63a595b8f..9042ac45cd4d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelKairosTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelKairosTest.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. @@ -19,110 +19,69 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.flags.FakeFeatureFlagsClassic import com.android.systemui.flags.Flags +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.kairos +import com.android.systemui.kairos.runKairosTest +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.log.table.logcatTableLogBuffer import com.android.systemui.statusbar.connectivity.MobileIconCarrierIdOverridesFake -import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository -import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor +import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.airplaneModeInteractor import com.android.systemui.statusbar.pipeline.mobile.data.model.DataConnectionState import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType -import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository -import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository -import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor -import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractorImpl -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.data.model.SubscriptionModel +import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepositoryKairos +import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepositoryKairos +import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepositoryKairos +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractorKairos +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractorKairosImpl +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorKairos +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.mobileIconsInteractorKairos import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants -import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository -import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository +import com.android.systemui.statusbar.pipeline.shared.data.repository.connectivityRepository +import com.android.systemui.statusbar.pipeline.shared.data.repository.fake import com.android.systemui.testKosmos -import com.android.systemui.util.CarrierConfigTracker -import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -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.mock -@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") -@OptIn(ExperimentalCoroutinesApi::class) +@OptIn(ExperimentalKairosApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class LocationBasedMobileIconViewModelKairosTest : SysuiTestCase() { - private val kosmos = testKosmos() - - private lateinit var commonImpl: MobileIconViewModelCommonKairos - private lateinit var homeIcon: HomeMobileIconViewModelKairos - private lateinit var qsIcon: QsMobileIconViewModelKairos - private lateinit var keyguardIcon: KeyguardMobileIconViewModelKairos - private lateinit var iconsInteractor: MobileIconsInteractor - private lateinit var interactor: MobileIconInteractor - private val connectionsRepository = kosmos.fakeMobileConnectionsRepository - private lateinit var repository: FakeMobileConnectionRepository - private lateinit var airplaneModeInteractor: AirplaneModeInteractor - - private val connectivityRepository = FakeConnectivityRepository() - private val flags = - FakeFeatureFlagsClassic().also { - it.set(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS, true) - } - @Mock private lateinit var constants: ConnectivityConstants - private val tableLogBuffer = - logcatTableLogBuffer(kosmos, "LocationBasedMobileIconViewModelTest") - @Mock private lateinit var carrierConfigTracker: CarrierConfigTracker - - private val testDispatcher = UnconfinedTestDispatcher() - private val testScope = TestScope(testDispatcher) - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - airplaneModeInteractor = - AirplaneModeInteractor( - FakeAirplaneModeRepository(), - FakeConnectivityRepository(), - connectionsRepository, - ) - repository = - FakeMobileConnectionRepository(SUB_1_ID, tableLogBuffer).apply { - isInService.value = true - cdmaLevel.value = 1 - primaryLevel.value = 1 - isEmergencyOnly.value = false - numberOfLevels.value = 4 - resolvedNetworkType.value = ResolvedNetworkType.DefaultNetworkType(lookupKey = "3G") - dataConnectionState.value = DataConnectionState.Connected - } + private val Kosmos.commonImpl: MobileIconViewModelKairosCommon by ActivatedKairosFixture { + MobileIconViewModelKairos( + SUB_1_ID, + interactor, + airplaneModeInteractor, + constants, + featureFlagsClassic, + ) + } - connectionsRepository.activeMobileDataRepository.value = repository + private val Kosmos.homeIcon: HomeMobileIconViewModelKairos by + Kosmos.Fixture { HomeMobileIconViewModelKairos(commonImpl, mock()) } - connectivityRepository.apply { setMobileConnected() } + private val Kosmos.qsIcon: QsMobileIconViewModelKairos by + Kosmos.Fixture { QsMobileIconViewModelKairos(commonImpl) } - iconsInteractor = - MobileIconsInteractorImpl( - connectionsRepository, - carrierConfigTracker, - tableLogBuffer, - connectivityRepository, - FakeUserSetupRepository(), - testScope.backgroundScope, - context, - flags, - ) + private val Kosmos.keyguardIcon: KeyguardMobileIconViewModelKairos by + Kosmos.Fixture { KeyguardMobileIconViewModelKairos(commonImpl) } + + private val Kosmos.iconsInteractor: MobileIconsInteractorKairos + get() = mobileIconsInteractorKairos - interactor = - MobileIconInteractorImpl( - testScope.backgroundScope, + private val Kosmos.interactor: MobileIconInteractorKairos by + Kosmos.Fixture { + MobileIconInteractorKairosImpl( iconsInteractor.activeDataConnectionHasDataEnabled, iconsInteractor.alwaysShowDataRatIcon, iconsInteractor.alwaysUseCdmaLevel, @@ -136,50 +95,74 @@ class LocationBasedMobileIconViewModelKairosTest : SysuiTestCase() { context, MobileIconCarrierIdOverridesFake(), ) + } - commonImpl = - MobileIconViewModelKairos( - SUB_1_ID, - interactor, - airplaneModeInteractor, - constants, - testScope.backgroundScope, - ) - - homeIcon = HomeMobileIconViewModelKairos(commonImpl, mock()) - qsIcon = QsMobileIconViewModelKairos(commonImpl) - keyguardIcon = KeyguardMobileIconViewModelKairos(commonImpl) - } + private val Kosmos.repository: FakeMobileConnectionRepositoryKairos by + Kosmos.Fixture { + FakeMobileConnectionRepositoryKairos(SUB_1_ID, kairos, tableLogBuffer).apply { + isInService.setValue(true) + cdmaLevel.setValue(1) + primaryLevel.setValue(1) + isEmergencyOnly.setValue(false) + numberOfLevels.setValue(4) + resolvedNetworkType.setValue( + ResolvedNetworkType.DefaultNetworkType(lookupKey = "3G") + ) + dataConnectionState.setValue(DataConnectionState.Connected) + } + } - @Test - fun locationBasedViewModelsReceiveSameIconIdWhenCommonImplUpdates() = - testScope.runTest { - var latestHome: SignalIconModel? = null - val homeJob = homeIcon.icon.onEach { latestHome = it }.launchIn(this) + private val Kosmos.constants: ConnectivityConstants by Kosmos.Fixture { mock() } + private val Kosmos.tableLogBuffer by + Kosmos.Fixture { logcatTableLogBuffer(this, "LocationBasedMobileIconViewModelTest") } + + private val kosmos = + testKosmos().apply { + useUnconfinedTestDispatcher() + mobileConnectionsRepositoryKairos = + fakeMobileConnectionsRepositoryKairos.apply { + setActiveMobileDataSubscriptionId(SUB_1_ID) + subscriptions.setValue( + listOf( + SubscriptionModel( + SUB_1_ID, + carrierName = "carrierName", + profileClass = 0, + ) + ) + ) + } + connectivityRepository.fake.apply { setMobileConnected() } + featureFlagsClassic.fake.apply { + set(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS, true) + } + } - var latestQs: SignalIconModel? = null - val qsJob = qsIcon.icon.onEach { latestQs = it }.launchIn(this) + private fun runTest(block: suspend KairosTestScope.() -> Unit) = + kosmos.run { runKairosTest { block() } } - var latestKeyguard: SignalIconModel? = null - val keyguardJob = keyguardIcon.icon.onEach { latestKeyguard = it }.launchIn(this) + @Test + fun locationBasedViewModelsReceiveSameIconIdWhenCommonImplUpdates() = runTest { + repository.dataEnabled.setValue(true) + repository.isInService.setValue(true) - var expected = defaultSignal(level = 1) + val latestHome by homeIcon.icon.collectLastValue() + val latestQs by qsIcon.icon.collectLastValue() + val latestKeyguard by keyguardIcon.icon.collectLastValue() - assertThat(latestHome).isEqualTo(expected) - assertThat(latestQs).isEqualTo(expected) - assertThat(latestKeyguard).isEqualTo(expected) + var expected = defaultSignal(level = 1) - repository.setAllLevels(2) - expected = defaultSignal(level = 2) + assertThat(latestHome).isEqualTo(expected) + assertThat(latestQs).isEqualTo(expected) + assertThat(latestKeyguard).isEqualTo(expected) - assertThat(latestHome).isEqualTo(expected) - assertThat(latestQs).isEqualTo(expected) - assertThat(latestKeyguard).isEqualTo(expected) + repository.setAllLevels(2) + expected = defaultSignal(level = 2) - homeJob.cancel() - qsJob.cancel() - keyguardJob.cancel() - } + assertThat(latestHome).isEqualTo(expected) + assertThat(latestQs).isEqualTo(expected) + assertThat(latestKeyguard).isEqualTo(expected) + } companion object { private const val SUB_1_ID = 1 diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairosTest.kt index 6b114a8256f2..68499d1bc57c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairosTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairosTest.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. @@ -28,1039 +28,893 @@ import com.android.systemui.Flags.FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon -import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.flags.FakeFeatureFlagsClassic import com.android.systemui.flags.Flags +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.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.log.table.logcatTableLogBuffer import com.android.systemui.res.R import com.android.systemui.statusbar.connectivity.MobileIconCarrierIdOverridesFake import com.android.systemui.statusbar.core.NewStatusBarIcons import com.android.systemui.statusbar.core.StatusBarRootModernization -import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository -import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor +import com.android.systemui.statusbar.pipeline.airplane.data.repository.airplaneModeRepository +import com.android.systemui.statusbar.pipeline.airplane.data.repository.fake +import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.airplaneModeInteractor 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.FakeMobileConnectionRepository +import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository.Companion.DEFAULT_NETWORK_NAME -import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository -import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository -import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractorImpl -import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorImpl +import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepositoryKairos +import com.android.systemui.statusbar.pipeline.mobile.data.repository.fake +import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepositoryKairos +import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepositoryKairos +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractorKairos +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractorKairosImpl +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.mobileIconsInteractorKairos import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel import com.android.systemui.statusbar.pipeline.mobile.ui.model.MobileContentDescription -import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel -import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository -import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository +import com.android.systemui.statusbar.pipeline.shared.data.repository.connectivityRepository +import com.android.systemui.statusbar.pipeline.shared.data.repository.fake import com.android.systemui.testKosmos -import com.android.systemui.util.CarrierConfigTracker -import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.filterIsInstance -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 kotlinx.coroutines.yield -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 -@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") -@OptIn(ExperimentalCoroutinesApi::class) +@OptIn(ExperimentalKairosApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class MobileIconViewModelKairosTest : SysuiTestCase() { - private val kosmos = testKosmos() - - private var connectivityRepository = FakeConnectivityRepository() - - private lateinit var underTest: MobileIconViewModelKairos - private lateinit var interactor: MobileIconInteractorImpl - private lateinit var iconsInteractor: MobileIconsInteractorImpl - private lateinit var repository: FakeMobileConnectionRepository - private lateinit var connectionsRepository: FakeMobileConnectionsRepository - private lateinit var airplaneModeRepository: FakeAirplaneModeRepository - private lateinit var airplaneModeInteractor: AirplaneModeInteractor - @Mock private lateinit var constants: ConnectivityConstants - private val tableLogBuffer = logcatTableLogBuffer(kosmos, "MobileIconViewModelTest") - @Mock private lateinit var carrierConfigTracker: CarrierConfigTracker - - private val flags = - FakeFeatureFlagsClassic().also { - it.set(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS, true) + + private val Kosmos.underTest: MobileIconViewModelKairos by ActivatedKairosFixture { + MobileIconViewModelKairos( + SUB_1_ID, + interactor, + airplaneModeInteractor, + constants, + featureFlagsClassic, + ) + } + private val Kosmos.interactor: MobileIconInteractorKairos by ActivatedKairosFixture { + MobileIconInteractorKairosImpl( + mobileIconsInteractorKairos.activeDataConnectionHasDataEnabled, + mobileIconsInteractorKairos.alwaysShowDataRatIcon, + mobileIconsInteractorKairos.alwaysUseCdmaLevel, + mobileIconsInteractorKairos.isSingleCarrier, + mobileIconsInteractorKairos.mobileIsDefault, + mobileIconsInteractorKairos.defaultMobileIconMapping, + mobileIconsInteractorKairos.defaultMobileIconGroup, + mobileIconsInteractorKairos.isDefaultConnectionFailed, + mobileIconsInteractorKairos.isForceHidden, + repository, + context, + MobileIconCarrierIdOverridesFake(), + ) + } + private val Kosmos.repository: FakeMobileConnectionRepositoryKairos by + Kosmos.Fixture { + FakeMobileConnectionRepositoryKairos(SUB_1_ID, kairos, tableLogBuffer) + .also { + mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId( + SUB_1_ID + ) + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue( + listOf( + SubscriptionModel( + SUB_1_ID, + carrierName = "carrierName", + profileClass = 0, + ) + ) + ) + } + .apply { + isInService.setValue(true) + dataConnectionState.setValue(DataConnectionState.Connected) + dataEnabled.setValue(true) + setNetworkTypeKey(mobileConnectionsRepositoryKairos.fake.GSM_KEY) + } } - private val testDispatcher = UnconfinedTestDispatcher() - private val testScope = TestScope(testDispatcher) - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - whenever(constants.hasDataCapabilities).thenReturn(true) - - connectionsRepository = - FakeMobileConnectionsRepository(FakeMobileMappingsProxy(), tableLogBuffer) - - repository = - FakeMobileConnectionRepository(SUB_1_ID, tableLogBuffer).apply { - setNetworkTypeKey(connectionsRepository.GSM_KEY) - isInService.value = true - dataConnectionState.value = DataConnectionState.Connected - dataEnabled.value = true + private val Kosmos.constants: ConnectivityConstants by + Kosmos.Fixture { mock { on { hasDataCapabilities } doReturn true } } + private val Kosmos.tableLogBuffer by + Kosmos.Fixture { logcatTableLogBuffer(this, "MobileIconViewModelKairosTest") } + + private val kosmos = + testKosmos().apply { + useUnconfinedTestDispatcher() + mobileConnectionsRepositoryKairos = + fakeMobileConnectionsRepositoryKairos.apply { mobileIsDefault.setValue(true) } + featureFlagsClassic.fake.apply { + set(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS, true) } - connectionsRepository.activeMobileDataRepository.value = repository - connectionsRepository.mobileIsDefault.value = true - - airplaneModeRepository = FakeAirplaneModeRepository() - airplaneModeInteractor = - AirplaneModeInteractor( - airplaneModeRepository, - connectivityRepository, - kosmos.fakeMobileConnectionsRepository, - ) - - iconsInteractor = - MobileIconsInteractorImpl( - connectionsRepository, - carrierConfigTracker, - tableLogBuffer, - connectivityRepository, - FakeUserSetupRepository(), - testScope.backgroundScope, - context, - flags, - ) + } - interactor = - MobileIconInteractorImpl( - testScope.backgroundScope, - iconsInteractor.activeDataConnectionHasDataEnabled, - iconsInteractor.alwaysShowDataRatIcon, - iconsInteractor.alwaysUseCdmaLevel, - iconsInteractor.isSingleCarrier, - iconsInteractor.mobileIsDefault, - iconsInteractor.defaultMobileIconMapping, - iconsInteractor.defaultMobileIconGroup, - iconsInteractor.isDefaultConnectionFailed, - iconsInteractor.isForceHidden, - repository, - context, - MobileIconCarrierIdOverridesFake(), - ) - createAndSetViewModel() - } + private fun runTest(block: suspend KairosTestScope.() -> Unit) = + kosmos.run { runKairosTest { block() } } @Test - fun isVisible_notDataCapable_alwaysFalse() = - testScope.runTest { - // Create a new view model here so the constants are properly read - whenever(constants.hasDataCapabilities).thenReturn(false) - createAndSetViewModel() - - var latest: Boolean? = null - val job = underTest.isVisible.onEach { latest = it }.launchIn(this) + fun isVisible_notDataCapable_alwaysFalse() = runTest { + // Create a new view model here so the constants are properly read + constants.stub { on { hasDataCapabilities } doReturn false } - assertThat(latest).isFalse() + val latest by underTest.isVisible.collectLastValue() - job.cancel() - } + assertThat(latest).isFalse() + } @Test - fun isVisible_notAirplane_notForceHidden_true() = - testScope.runTest { - var latest: Boolean? = null - val job = underTest.isVisible.onEach { latest = it }.launchIn(this) + fun isVisible_notAirplane_notForceHidden_true() = runTest { + val latest by underTest.isVisible.collectLastValue() - airplaneModeRepository.setIsAirplaneMode(false) + airplaneModeRepository.fake.setIsAirplaneMode(false) - assertThat(latest).isTrue() - - job.cancel() - } + assertThat(latest).isTrue() + } @Test - fun isVisible_airplaneAndNotAllowed_false() = - testScope.runTest { - var latest: Boolean? = null - val job = underTest.isVisible.onEach { latest = it }.launchIn(this) - - airplaneModeRepository.setIsAirplaneMode(true) - repository.isAllowedDuringAirplaneMode.value = false - connectivityRepository.setForceHiddenIcons(setOf()) + fun isVisible_airplaneAndNotAllowed_false() = runTest { + val latest by underTest.isVisible.collectLastValue() - assertThat(latest).isFalse() + airplaneModeRepository.fake.setIsAirplaneMode(true) + repository.isAllowedDuringAirplaneMode.setValue(false) + connectivityRepository.fake.setForceHiddenIcons(setOf()) - job.cancel() - } + assertThat(latest).isEqualTo(false) + } /** Regression test for b/291993542. */ @Test - fun isVisible_airplaneButAllowed_true() = - testScope.runTest { - var latest: Boolean? = null - val job = underTest.isVisible.onEach { latest = it }.launchIn(this) - - airplaneModeRepository.setIsAirplaneMode(true) - repository.isAllowedDuringAirplaneMode.value = true - connectivityRepository.setForceHiddenIcons(setOf()) + fun isVisible_airplaneButAllowed_true() = runTest { + val latest by underTest.isVisible.collectLastValue() - assertThat(latest).isTrue() + airplaneModeRepository.fake.setIsAirplaneMode(true) + repository.isAllowedDuringAirplaneMode.setValue(true) + connectivityRepository.fake.setForceHiddenIcons(setOf()) - job.cancel() - } + assertThat(latest).isTrue() + } @Test - fun isVisible_forceHidden_false() = - testScope.runTest { - var latest: Boolean? = null - val job = underTest.isVisible.onEach { latest = it }.launchIn(this) + fun isVisible_forceHidden_false() = runTest { + val latest by underTest.isVisible.collectLastValue() - airplaneModeRepository.setIsAirplaneMode(false) - connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.MOBILE)) + airplaneModeRepository.fake.setIsAirplaneMode(false) + connectivityRepository.fake.setForceHiddenIcons(setOf(ConnectivitySlot.MOBILE)) - assertThat(latest).isFalse() - - job.cancel() - } + assertThat(latest).isFalse() + } @Test - fun isVisible_respondsToUpdates() = - testScope.runTest { - var latest: Boolean? = null - val job = underTest.isVisible.onEach { latest = it }.launchIn(this) - - airplaneModeRepository.setIsAirplaneMode(false) - connectivityRepository.setForceHiddenIcons(setOf()) + fun isVisible_respondsToUpdates() = runTest { + val latest by underTest.isVisible.collectLastValue() - assertThat(latest).isTrue() + airplaneModeRepository.fake.setIsAirplaneMode(false) + connectivityRepository.fake.setForceHiddenIcons(setOf()) - airplaneModeRepository.setIsAirplaneMode(true) - assertThat(latest).isFalse() + assertThat(latest).isEqualTo(true) - repository.isAllowedDuringAirplaneMode.value = true - assertThat(latest).isTrue() + airplaneModeRepository.fake.setIsAirplaneMode(true) + assertThat(latest).isEqualTo(false) - connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.MOBILE)) - assertThat(latest).isFalse() + repository.isAllowedDuringAirplaneMode.setValue(true) + assertThat(latest).isEqualTo(true) - job.cancel() - } + connectivityRepository.fake.setForceHiddenIcons(setOf(ConnectivitySlot.MOBILE)) + assertThat(latest).isEqualTo(false) + } @Test - fun isVisible_satellite_respectsAirplaneMode() = - testScope.runTest { - val latest by collectLastValue(underTest.isVisible) + fun isVisible_satellite_respectsAirplaneMode() = runTest { + val latest by underTest.isVisible.collectLastValue() - repository.isNonTerrestrial.value = true - airplaneModeInteractor.setIsAirplaneMode(false) + repository.isNonTerrestrial.setValue(true) + airplaneModeInteractor.setIsAirplaneMode(false) - assertThat(latest).isTrue() + assertThat(latest).isTrue() - airplaneModeInteractor.setIsAirplaneMode(true) + airplaneModeInteractor.setIsAirplaneMode(true) - assertThat(latest).isFalse() - } + assertThat(latest).isFalse() + } @Test - fun contentDescription_notInService_usesNoPhone() = - testScope.runTest { - val latest by collectLastValue(underTest.contentDescription) + fun contentDescription_notInService_usesNoPhone() = runTest { + val latest by underTest.contentDescription.collectLastValue() - repository.isInService.value = false + repository.isInService.setValue(false) - assertThat(latest as MobileContentDescription.Cellular) - .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL)) - } + assertThat(latest) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL)) + } @Test - fun contentDescription_includesNetworkName() = - testScope.runTest { - val latest by collectLastValue(underTest.contentDescription) + fun contentDescription_includesNetworkName() = runTest { + val latest by underTest.contentDescription.collectLastValue() - repository.isInService.value = true - repository.networkName.value = NetworkNameModel.SubscriptionDerived("Test Network Name") - repository.numberOfLevels.value = 5 - repository.setAllLevels(3) + repository.isInService.setValue(true) + repository.networkName.setValue(NetworkNameModel.SubscriptionDerived("Test Network Name")) + repository.numberOfLevels.setValue(5) + repository.setAllLevels(3) - assertThat(latest as MobileContentDescription.Cellular) - .isEqualTo(MobileContentDescription.Cellular("Test Network Name", THREE_BARS)) - } + assertThat(latest) + .isEqualTo(MobileContentDescription.Cellular("Test Network Name", THREE_BARS)) + } @Test - fun contentDescription_inService_usesLevel() = - testScope.runTest { - val latest by collectLastValue(underTest.contentDescription) + fun contentDescription_inService_usesLevel() = runTest { + val latest by underTest.contentDescription.collectLastValue() - repository.setAllLevels(2) + repository.setAllLevels(2) - assertThat(latest as MobileContentDescription.Cellular) - .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, TWO_BARS)) + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, TWO_BARS)) - repository.setAllLevels(0) + repository.setAllLevels(0) - assertThat(latest as MobileContentDescription.Cellular) - .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL)) - } + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL)) + } @Test - fun contentDescription_nonInflated_invalidLevelUsesNoSignalText() = - testScope.runTest { - val latest by collectLastValue(underTest.contentDescription) + fun contentDescription_nonInflated_invalidLevelUsesNoSignalText() = runTest { + val latest by underTest.contentDescription.collectLastValue() - repository.inflateSignalStrength.value = false - repository.setAllLevels(-1) + repository.inflateSignalStrength.setValue(false) + repository.setAllLevels(-1) - assertThat(latest as MobileContentDescription.Cellular) - .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL)) + assertThat(latest) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL)) - repository.setAllLevels(100) + repository.setAllLevels(100) - assertThat(latest as MobileContentDescription.Cellular) - .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL)) - } + assertThat(latest) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL)) + } @Test - fun contentDescription_nonInflated_levelStrings() = - testScope.runTest { - val latest by collectLastValue(underTest.contentDescription) + fun contentDescription_nonInflated_levelStrings() = runTest { + val latest by underTest.contentDescription.collectLastValue() - repository.inflateSignalStrength.value = false - repository.setAllLevels(0) + repository.inflateSignalStrength.setValue(false) + repository.setAllLevels(0) - assertThat(latest as MobileContentDescription.Cellular) - .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL)) + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL)) - repository.setAllLevels(1) + repository.setAllLevels(1) - assertThat(latest as MobileContentDescription.Cellular) - .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, ONE_BAR)) + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, ONE_BAR)) - repository.setAllLevels(2) + repository.setAllLevels(2) - assertThat(latest as MobileContentDescription.Cellular) - .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, TWO_BARS)) + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, TWO_BARS)) - repository.setAllLevels(3) + repository.setAllLevels(3) - assertThat(latest as MobileContentDescription.Cellular) - .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, THREE_BARS)) + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, THREE_BARS)) - repository.setAllLevels(4) + repository.setAllLevels(4) - assertThat(latest as MobileContentDescription.Cellular) - .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, FULL_BARS)) - } + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, FULL_BARS)) + } @Test - fun contentDescription_inflated_invalidLevelUsesNoSignalText() = - testScope.runTest { - val latest by collectLastValue(underTest.contentDescription) + fun contentDescription_inflated_invalidLevelUsesNoSignalText() = runTest { + val latest by underTest.contentDescription.collectLastValue() - repository.inflateSignalStrength.value = true - repository.numberOfLevels.value = 6 + repository.inflateSignalStrength.setValue(true) + repository.numberOfLevels.setValue(6) + repository.setAllLevels(-2) - repository.setAllLevels(-2) + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL)) - assertThat(latest as MobileContentDescription.Cellular) - .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL)) + repository.setAllLevels(100) - repository.setAllLevels(100) - - assertThat(latest as MobileContentDescription.Cellular) - .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL)) - } + assertThat(latest as MobileContentDescription.Cellular) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, NO_SIGNAL)) + } @Test - fun contentDescription_inflated_levelStrings() = - testScope.runTest { - val latest by collectLastValue(underTest.contentDescription) + fun contentDescription_inflated_levelStrings() = runTest { + val latest by underTest.contentDescription.collectLastValue() - repository.inflateSignalStrength.value = true - repository.numberOfLevels.value = 6 + repository.inflateSignalStrength.setValue(true) + repository.numberOfLevels.setValue(6) - // Note that the _repo_ level is 1 lower than the reported level through the interactor + // Note that the _repo_ level is 1 lower than the reported level through the interactor - repository.setAllLevels(0) + repository.setAllLevels(0) - assertThat(latest as MobileContentDescription.Cellular) - .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, ONE_BAR)) + assertThat(latest) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, ONE_BAR)) - repository.setAllLevels(1) + repository.setAllLevels(1) - assertThat(latest as MobileContentDescription.Cellular) - .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, TWO_BARS)) + assertThat(latest) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, TWO_BARS)) - repository.setAllLevels(2) + repository.setAllLevels(2) - assertThat(latest as MobileContentDescription.Cellular) - .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, THREE_BARS)) + assertThat(latest) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, THREE_BARS)) - repository.setAllLevels(3) + repository.setAllLevels(3) - assertThat(latest as MobileContentDescription.Cellular) - .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, FOUR_BARS)) + assertThat(latest) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, FOUR_BARS)) - repository.setAllLevels(4) + repository.setAllLevels(4) - assertThat(latest as MobileContentDescription.Cellular) - .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, FULL_BARS)) - } + assertThat(latest) + .isEqualTo(MobileContentDescription.Cellular(DEFAULT_NETWORK_NAME, FULL_BARS)) + } @Test - fun contentDescription_nonInflated_testABunchOfLevelsForNull() = - testScope.runTest { - val latest by collectLastValue(underTest.contentDescription) - - repository.inflateSignalStrength.value = false - repository.numberOfLevels.value = 5 - - // -1 and 5 are out of the bounds for non-inflated content descriptions - for (i in -1..5) { - repository.setAllLevels(i) - when (i) { - -1, - 5 -> - assertWithMessage("Level $i is expected to be 'no signal'") - .that((latest as MobileContentDescription.Cellular).levelDescriptionRes) - .isEqualTo(NO_SIGNAL) - else -> - assertWithMessage("Level $i is expected not to be null") - .that(latest) - .isNotNull() - } + fun contentDescription_nonInflated_testABunchOfLevelsForNull() = runTest { + val latest by underTest.contentDescription.collectLastValue() + + repository.inflateSignalStrength.setValue(false) + repository.numberOfLevels.setValue(5) + + // -1 and 5 are out of the bounds for non-inflated content descriptions + for (i in -1..5) { + repository.setAllLevels(i) + when (i) { + -1, + 5 -> + assertWithMessage("Level $i is expected to be null") + .that((latest as MobileContentDescription.Cellular).levelDescriptionRes) + .isEqualTo(NO_SIGNAL) + else -> + assertWithMessage("Level $i is expected not to be null") + .that(latest) + .isNotNull() } } + } @Test - fun contentDescription_inflated_testABunchOfLevelsForNull() = - testScope.runTest { - val latest by collectLastValue(underTest.contentDescription) - repository.inflateSignalStrength.value = true - repository.numberOfLevels.value = 6 - // -1 and 6 are out of the bounds for inflated content descriptions - // Note that the interactor adds 1 to the reported level, hence the -2 to 5 range - for (i in -2..5) { - repository.setAllLevels(i) - when (i) { - -2, - 5 -> - assertWithMessage("Level $i is expected to be 'no signal'") - .that((latest as MobileContentDescription.Cellular).levelDescriptionRes) - .isEqualTo(NO_SIGNAL) - else -> - assertWithMessage("Level $i is not expected to be null") - .that(latest) - .isNotNull() - } + fun contentDescription_inflated_testABunchOfLevelsForNull() = runTest { + val latest by underTest.contentDescription.collectLastValue() + repository.inflateSignalStrength.setValue(true) + repository.numberOfLevels.setValue(6) + // -1 and 6 are out of the bounds for inflated content descriptions + // Note that the interactor adds 1 to the reported level, hence the -2 to 5 range + for (i in -2..5) { + repository.setAllLevels(i) + when (i) { + -2, + 5 -> + assertWithMessage("Level $i is expected to be null") + .that((latest as MobileContentDescription.Cellular).levelDescriptionRes) + .isEqualTo(NO_SIGNAL) + else -> + assertWithMessage("Level $i is not expected to be null") + .that(latest) + .isNotNull() } } + } @Test - fun networkType_dataEnabled_groupIsRepresented() = - testScope.runTest { - val expected = - Icon.Resource( - THREE_G.dataType, - ContentDescription.Resource(THREE_G.dataContentDescription), - ) - connectionsRepository.mobileIsDefault.value = true - repository.setNetworkTypeKey(connectionsRepository.GSM_KEY) - - var latest: Icon? = null - val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) - - assertThat(latest).isEqualTo(expected) - - job.cancel() - } - - @Test - fun networkType_null_whenDisabled() = - testScope.runTest { - repository.setNetworkTypeKey(connectionsRepository.GSM_KEY) - repository.setDataEnabled(false) - connectionsRepository.mobileIsDefault.value = true - var latest: Icon? = null - val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) + fun networkType_dataEnabled_groupIsRepresented() = runTest { + val expected = + Icon.Resource( + THREE_G.dataType, + ContentDescription.Resource(THREE_G.dataContentDescription), + ) + mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true) + repository.setNetworkTypeKey(mobileConnectionsRepositoryKairos.fake.GSM_KEY) - assertThat(latest).isNull() + val latest by underTest.networkTypeIcon.collectLastValue() - job.cancel() - } + assertThat(latest).isEqualTo(expected) + } @Test - fun networkType_null_whenCarrierNetworkChangeActive() = - testScope.runTest { - repository.setNetworkTypeKey(connectionsRepository.GSM_KEY) - repository.carrierNetworkChangeActive.value = true - connectionsRepository.mobileIsDefault.value = true - var latest: Icon? = null - val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) - - assertThat(latest).isNull() + fun networkType_null_whenDisabled() = runTest { + repository.setNetworkTypeKey(mobileConnectionsRepositoryKairos.fake.GSM_KEY) + repository.dataEnabled.setValue(false) + mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true) + val latest by underTest.networkTypeIcon.collectLastValue() - job.cancel() - } + assertThat(latest).isNull() + } @Test - fun networkTypeIcon_notNull_whenEnabled() = - testScope.runTest { - val expected = - Icon.Resource( - THREE_G.dataType, - ContentDescription.Resource(THREE_G.dataContentDescription), - ) - repository.setNetworkTypeKey(connectionsRepository.GSM_KEY) - repository.setDataEnabled(true) - repository.dataConnectionState.value = DataConnectionState.Connected - connectionsRepository.mobileIsDefault.value = true - var latest: Icon? = null - val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) - - assertThat(latest).isEqualTo(expected) - - job.cancel() - } + fun networkType_null_whenCarrierNetworkChangeActive() = runTest { + repository.setNetworkTypeKey(mobileConnectionsRepositoryKairos.fake.GSM_KEY) + repository.carrierNetworkChangeActive.setValue(true) + mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true) + val latest by underTest.networkTypeIcon.collectLastValue() + + assertThat(latest).isNull() + } @Test - fun networkType_nullWhenDataDisconnects() = - testScope.runTest { - val initial = - Icon.Resource( - THREE_G.dataType, - ContentDescription.Resource(THREE_G.dataContentDescription), - ) + fun networkTypeIcon_notNull_whenEnabled() = runTest { + val expected = + Icon.Resource( + THREE_G.dataType, + ContentDescription.Resource(THREE_G.dataContentDescription), + ) + repository.setNetworkTypeKey(mobileConnectionsRepositoryKairos.fake.GSM_KEY) + repository.dataEnabled.setValue(true) + repository.dataConnectionState.setValue(DataConnectionState.Connected) + mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true) + val latest by underTest.networkTypeIcon.collectLastValue() - repository.setNetworkTypeKey(connectionsRepository.GSM_KEY) - var latest: Icon? = null - val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) + assertThat(latest).isEqualTo(expected) + } - assertThat(latest).isEqualTo(initial) + @Test + fun networkType_nullWhenDataDisconnects() = runTest { + val initial = + Icon.Resource( + THREE_G.dataType, + ContentDescription.Resource(THREE_G.dataContentDescription), + ) - repository.dataConnectionState.value = DataConnectionState.Disconnected + repository.setNetworkTypeKey(mobileConnectionsRepositoryKairos.fake.GSM_KEY) + val latest by underTest.networkTypeIcon.collectLastValue() - assertThat(latest).isNull() + assertThat(latest).isEqualTo(initial) - job.cancel() - } + repository.dataConnectionState.setValue(DataConnectionState.Disconnected) - @Test - fun networkType_null_changeToDisabled() = - testScope.runTest { - val expected = - Icon.Resource( - THREE_G.dataType, - ContentDescription.Resource(THREE_G.dataContentDescription), - ) - repository.dataEnabled.value = true - var latest: Icon? = null - val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) + assertThat(latest).isNull() + } - assertThat(latest).isEqualTo(expected) + @Test + fun networkType_null_changeToDisabled() = runTest { + val expected = + Icon.Resource( + THREE_G.dataType, + ContentDescription.Resource(THREE_G.dataContentDescription), + ) + repository.dataEnabled.setValue(true) + val latest by underTest.networkTypeIcon.collectLastValue() - repository.dataEnabled.value = false + assertThat(latest).isEqualTo(expected) - assertThat(latest).isNull() + repository.dataEnabled.setValue(false) - job.cancel() - } + assertThat(latest).isNull() + } @Test - fun networkType_alwaysShow_shownEvenWhenDisabled() = - testScope.runTest { - repository.dataEnabled.value = false - - connectionsRepository.defaultDataSubRatConfig.value = - MobileMappings.Config().also { it.alwaysShowDataRatIcon = true } + fun networkType_alwaysShow_shownEvenWhenDisabled() = runTest { + repository.dataEnabled.setValue(false) - var latest: Icon? = null - val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) + mobileConnectionsRepositoryKairos.fake.defaultDataSubRatConfig.setValue( + MobileMappings.Config().also { it.alwaysShowDataRatIcon = true } + ) - val expected = - Icon.Resource( - THREE_G.dataType, - ContentDescription.Resource(THREE_G.dataContentDescription), - ) - assertThat(latest).isEqualTo(expected) + val latest by underTest.networkTypeIcon.collectLastValue() - job.cancel() - } + val expected = + Icon.Resource( + THREE_G.dataType, + ContentDescription.Resource(THREE_G.dataContentDescription), + ) + assertThat(latest).isEqualTo(expected) + } @Test - fun networkType_alwaysShow_shownEvenWhenDisconnected() = - testScope.runTest { - repository.setNetworkTypeKey(connectionsRepository.GSM_KEY) - repository.dataConnectionState.value = DataConnectionState.Disconnected + fun networkType_alwaysShow_shownEvenWhenDisconnected() = runTest { + repository.setNetworkTypeKey(mobileConnectionsRepositoryKairos.fake.GSM_KEY) + repository.dataConnectionState.setValue(DataConnectionState.Disconnected) - connectionsRepository.defaultDataSubRatConfig.value = - MobileMappings.Config().also { it.alwaysShowDataRatIcon = true } + mobileConnectionsRepositoryKairos.fake.defaultDataSubRatConfig.setValue( + MobileMappings.Config().also { it.alwaysShowDataRatIcon = true } + ) - var latest: Icon? = null - val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) + val latest by underTest.networkTypeIcon.collectLastValue() - val expected = - Icon.Resource( - THREE_G.dataType, - ContentDescription.Resource(THREE_G.dataContentDescription), - ) - assertThat(latest).isEqualTo(expected) - - job.cancel() - } + val expected = + Icon.Resource( + THREE_G.dataType, + ContentDescription.Resource(THREE_G.dataContentDescription), + ) + assertThat(latest).isEqualTo(expected) + } @Test - fun networkType_alwaysShow_shownEvenWhenFailedConnection() = - testScope.runTest { - repository.setNetworkTypeKey(connectionsRepository.GSM_KEY) - connectionsRepository.mobileIsDefault.value = true - connectionsRepository.defaultDataSubRatConfig.value = - MobileMappings.Config().also { it.alwaysShowDataRatIcon = true } - - var latest: Icon? = null - val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) - - val expected = - Icon.Resource( - THREE_G.dataType, - ContentDescription.Resource(THREE_G.dataContentDescription), - ) - assertThat(latest).isEqualTo(expected) - - job.cancel() - } + fun networkType_alwaysShow_shownEvenWhenFailedConnection() = runTest { + repository.setNetworkTypeKey(mobileConnectionsRepositoryKairos.fake.GSM_KEY) + mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true) + mobileConnectionsRepositoryKairos.fake.defaultDataSubRatConfig.setValue( + MobileMappings.Config().also { it.alwaysShowDataRatIcon = true } + ) - @Test - fun networkType_alwaysShow_usesDefaultIconWhenInvalid() = - testScope.runTest { - // The UNKNOWN icon group doesn't have a valid data type icon ID, and the logic from the - // old pipeline was to use the default icon group if the map doesn't exist - repository.setNetworkTypeKey(UNKNOWN.name) - connectionsRepository.defaultDataSubRatConfig.value = - MobileMappings.Config().also { it.alwaysShowDataRatIcon = true } - - var latest: Icon? = null - val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) - - val expected = - Icon.Resource( - connectionsRepository.defaultMobileIconGroup.value.dataType, - ContentDescription.Resource(G.dataContentDescription), - ) - - assertThat(latest).isEqualTo(expected) - - job.cancel() - } + val latest by underTest.networkTypeIcon.collectLastValue() - @Test - fun networkType_alwaysShow_shownWhenNotDefault() = - testScope.runTest { - repository.setNetworkTypeKey(connectionsRepository.GSM_KEY) - connectionsRepository.mobileIsDefault.value = false - connectionsRepository.defaultDataSubRatConfig.value = - MobileMappings.Config().also { it.alwaysShowDataRatIcon = true } - - var latest: Icon? = null - val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) - - val expected = - Icon.Resource( - THREE_G.dataType, - ContentDescription.Resource(THREE_G.dataContentDescription), - ) - assertThat(latest).isEqualTo(expected) - - job.cancel() - } + val expected = + Icon.Resource( + THREE_G.dataType, + ContentDescription.Resource(THREE_G.dataContentDescription), + ) + assertThat(latest).isEqualTo(expected) + } @Test - fun networkType_notShownWhenNotDefault() = - testScope.runTest { - repository.setNetworkTypeKey(connectionsRepository.GSM_KEY) - repository.dataConnectionState.value = DataConnectionState.Connected - connectionsRepository.mobileIsDefault.value = false + fun networkType_alwaysShow_usesDefaultIconWhenInvalid() = runTest { + // The UNKNOWN icon group doesn't have a valid data type icon ID, and the logic from the + // old pipeline was to use the default icon group if the map doesn't exist + repository.setNetworkTypeKey(UNKNOWN.name) + mobileConnectionsRepositoryKairos.fake.defaultDataSubRatConfig.setValue( + MobileMappings.Config().also { it.alwaysShowDataRatIcon = true } + ) - var latest: Icon? = null - val job = underTest.networkTypeIcon.onEach { latest = it }.launchIn(this) + val latest by underTest.networkTypeIcon.collectLastValue() - assertThat(latest).isNull() + val expected = + Icon.Resource( + kairos.transact { + mobileConnectionsRepositoryKairos.fake.defaultMobileIconGroup.sample().dataType + }, + ContentDescription.Resource(G.dataContentDescription), + ) - job.cancel() - } + assertThat(latest).isEqualTo(expected) + } @Test - fun roaming() = - testScope.runTest { - repository.setAllRoaming(true) + fun networkType_alwaysShow_shownWhenNotDefault() = runTest { + repository.setNetworkTypeKey(mobileConnectionsRepositoryKairos.fake.GSM_KEY) + mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(false) + mobileConnectionsRepositoryKairos.fake.defaultDataSubRatConfig.setValue( + MobileMappings.Config().also { it.alwaysShowDataRatIcon = true } + ) - var latest: Boolean? = null - val job = underTest.roaming.onEach { latest = it }.launchIn(this) + val latest by underTest.networkTypeIcon.collectLastValue() - assertThat(latest).isTrue() + val expected = + Icon.Resource( + THREE_G.dataType, + ContentDescription.Resource(THREE_G.dataContentDescription), + ) + assertThat(latest).isEqualTo(expected) + } - repository.setAllRoaming(false) + @Test + fun networkType_notShownWhenNotDefault() = runTest { + repository.setNetworkTypeKey(mobileConnectionsRepositoryKairos.fake.GSM_KEY) + repository.dataConnectionState.setValue(DataConnectionState.Connected) + mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(false) - assertThat(latest).isFalse() + val latest by underTest.networkTypeIcon.collectLastValue() - job.cancel() - } + assertThat(latest).isNull() + } @Test - fun dataActivity_nullWhenConfigIsOff() = - testScope.runTest { - // Create a new view model here so the constants are properly read - whenever(constants.shouldShowActivityConfig).thenReturn(false) - createAndSetViewModel() + fun roaming() = runTest { + repository.setAllRoaming(true) - var inVisible: Boolean? = null - val inJob = underTest.activityInVisible.onEach { inVisible = it }.launchIn(this) + val latest by underTest.roaming.collectLastValue() - var outVisible: Boolean? = null - val outJob = underTest.activityInVisible.onEach { outVisible = it }.launchIn(this) + assertThat(latest).isTrue() - var containerVisible: Boolean? = null - val containerJob = - underTest.activityInVisible.onEach { containerVisible = it }.launchIn(this) + repository.setAllRoaming(false) - repository.dataActivityDirection.value = - DataActivityModel(hasActivityIn = true, hasActivityOut = true) + assertThat(latest).isFalse() + } - assertThat(inVisible).isFalse() - assertThat(outVisible).isFalse() - assertThat(containerVisible).isFalse() + @Test + fun dataActivity_nullWhenConfigIsOff() = runTest { + constants.stub { on { shouldShowActivityConfig } doReturn false } - inJob.cancel() - outJob.cancel() - containerJob.cancel() - } + val inVisible by underTest.activityInVisible.collectLastValue() + + val outVisible by underTest.activityInVisible.collectLastValue() + + val containerVisible by underTest.activityInVisible.collectLastValue() + + repository.dataActivityDirection.setValue( + DataActivityModel(hasActivityIn = true, hasActivityOut = true) + ) + + assertThat(inVisible).isFalse() + assertThat(outVisible).isFalse() + assertThat(containerVisible).isFalse() + } @Test @DisableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS) - fun dataActivity_configOn_testIndicators_staticFlagOff() = - testScope.runTest { - // Create a new view model here so the constants are properly read - whenever(constants.shouldShowActivityConfig).thenReturn(true) - createAndSetViewModel() + fun dataActivity_configOn_testIndicators_staticFlagOff() = runTest { + constants.stub { on { shouldShowActivityConfig } doReturn true } - var inVisible: Boolean? = null - val inJob = underTest.activityInVisible.onEach { inVisible = it }.launchIn(this) + val inVisible by underTest.activityInVisible.collectLastValue() - var outVisible: Boolean? = null - val outJob = underTest.activityOutVisible.onEach { outVisible = it }.launchIn(this) + val outVisible by underTest.activityOutVisible.collectLastValue() - var containerVisible: Boolean? = null - val containerJob = - underTest.activityContainerVisible.onEach { containerVisible = it }.launchIn(this) + val containerVisible by underTest.activityContainerVisible.collectLastValue() - repository.dataActivityDirection.value = - DataActivityModel(hasActivityIn = true, hasActivityOut = false) + repository.dataActivityDirection.setValue( + DataActivityModel(hasActivityIn = true, hasActivityOut = false) + ) - yield() + yield() - assertThat(inVisible).isTrue() - assertThat(outVisible).isFalse() - assertThat(containerVisible).isTrue() + assertThat(inVisible).isTrue() + assertThat(outVisible).isFalse() + assertThat(containerVisible).isTrue() - repository.dataActivityDirection.value = - DataActivityModel(hasActivityIn = false, hasActivityOut = true) + repository.dataActivityDirection.setValue( + DataActivityModel(hasActivityIn = false, hasActivityOut = true) + ) - assertThat(inVisible).isFalse() - assertThat(outVisible).isTrue() - assertThat(containerVisible).isTrue() + assertThat(inVisible).isFalse() + assertThat(outVisible).isTrue() + assertThat(containerVisible).isTrue() - repository.dataActivityDirection.value = - DataActivityModel(hasActivityIn = false, hasActivityOut = false) + repository.dataActivityDirection.setValue( + DataActivityModel(hasActivityIn = false, hasActivityOut = false) + ) - assertThat(inVisible).isFalse() - assertThat(outVisible).isFalse() - assertThat(containerVisible).isFalse() - - inJob.cancel() - outJob.cancel() - containerJob.cancel() - } + assertThat(inVisible).isFalse() + assertThat(outVisible).isFalse() + assertThat(containerVisible).isFalse() + } @Test @EnableFlags(FLAG_STATUS_BAR_STATIC_INOUT_INDICATORS) - fun dataActivity_configOn_testIndicators_staticFlagOn() = - testScope.runTest { - // Create a new view model here so the constants are properly read - whenever(constants.shouldShowActivityConfig).thenReturn(true) - createAndSetViewModel() - - var inVisible: Boolean? = null - val inJob = underTest.activityInVisible.onEach { inVisible = it }.launchIn(this) + fun dataActivity_configOn_testIndicators_staticFlagOn() = runTest { + constants.stub { on { shouldShowActivityConfig } doReturn true } - var outVisible: Boolean? = null - val outJob = underTest.activityOutVisible.onEach { outVisible = it }.launchIn(this) + val inVisible by underTest.activityInVisible.collectLastValue() - var containerVisible: Boolean? = null - val containerJob = - underTest.activityContainerVisible.onEach { containerVisible = it }.launchIn(this) + val outVisible by underTest.activityOutVisible.collectLastValue() - repository.dataActivityDirection.value = - DataActivityModel(hasActivityIn = true, hasActivityOut = false) + val containerVisible by underTest.activityContainerVisible.collectLastValue() - yield() + repository.dataActivityDirection.setValue( + DataActivityModel(hasActivityIn = true, hasActivityOut = false) + ) - assertThat(inVisible).isTrue() - assertThat(outVisible).isFalse() - assertThat(containerVisible).isTrue() + yield() - repository.dataActivityDirection.value = - DataActivityModel(hasActivityIn = false, hasActivityOut = true) + assertThat(inVisible).isTrue() + assertThat(outVisible).isFalse() + assertThat(containerVisible).isTrue() - assertThat(inVisible).isFalse() - assertThat(outVisible).isTrue() - assertThat(containerVisible).isTrue() + repository.dataActivityDirection.setValue( + DataActivityModel(hasActivityIn = false, hasActivityOut = true) + ) - repository.dataActivityDirection.value = - DataActivityModel(hasActivityIn = false, hasActivityOut = false) + assertThat(inVisible).isFalse() + assertThat(outVisible).isTrue() + assertThat(containerVisible).isTrue() - assertThat(inVisible).isFalse() - assertThat(outVisible).isFalse() - assertThat(containerVisible).isTrue() + repository.dataActivityDirection.setValue( + DataActivityModel(hasActivityIn = false, hasActivityOut = false) + ) - inJob.cancel() - outJob.cancel() - containerJob.cancel() - } + assertThat(inVisible).isFalse() + assertThat(outVisible).isFalse() + assertThat(containerVisible).isTrue() + } @Test - fun netTypeBackground_nullWhenNoPrioritizedCapabilities() = - testScope.runTest { - createAndSetViewModel() - - val latest by collectLastValue(underTest.networkTypeBackground) + fun netTypeBackground_nullWhenNoPrioritizedCapabilities() = runTest { + val latest by underTest.networkTypeBackground.collectLastValue() - repository.hasPrioritizedNetworkCapabilities.value = false + repository.hasPrioritizedNetworkCapabilities.setValue(false) - assertThat(latest).isNull() - } + assertThat(latest).isNull() + } @Test @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) - fun netTypeBackground_sliceUiEnabled_notNullWhenPrioritizedCapabilities_newIcons() = - testScope.runTest { - createAndSetViewModel() + fun netTypeBackground_sliceUiEnabled_notNullWhenPrioritizedCapabilities_newIcons() = runTest { + val latest by underTest.networkTypeBackground.collectLastValue() - val latest by collectLastValue(underTest.networkTypeBackground) + repository.hasPrioritizedNetworkCapabilities.setValue(true) - repository.hasPrioritizedNetworkCapabilities.value = true - - assertThat(latest) - .isEqualTo(Icon.Resource(R.drawable.mobile_network_type_background_updated, null)) - } + assertThat(latest) + .isEqualTo(Icon.Resource(R.drawable.mobile_network_type_background_updated, null)) + } @Test @DisableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) - fun netTypeBackground_sliceUiDisabled_notNullWhenPrioritizedCapabilities_oldIcons() = - testScope.runTest { - createAndSetViewModel() - - val latest by collectLastValue(underTest.networkTypeBackground) + fun netTypeBackground_sliceUiDisabled_notNullWhenPrioritizedCapabilities_oldIcons() = runTest { + val latest by underTest.networkTypeBackground.collectLastValue() - repository.hasPrioritizedNetworkCapabilities.value = true + repository.allowNetworkSliceIndicator.setValue(true) + repository.hasPrioritizedNetworkCapabilities.setValue(true) - assertThat(latest) - .isEqualTo(Icon.Resource(R.drawable.mobile_network_type_background, null)) - } + assertThat(latest).isEqualTo(Icon.Resource(R.drawable.mobile_network_type_background, null)) + } @Test - fun nonTerrestrial_defaultProperties() = - testScope.runTest { - repository.isNonTerrestrial.value = true - - val roaming by collectLastValue(underTest.roaming) - val networkTypeIcon by collectLastValue(underTest.networkTypeIcon) - val networkTypeBackground by collectLastValue(underTest.networkTypeBackground) - val activityInVisible by collectLastValue(underTest.activityInVisible) - val activityOutVisible by collectLastValue(underTest.activityOutVisible) - val activityContainerVisible by collectLastValue(underTest.activityContainerVisible) - - assertThat(roaming).isFalse() - assertThat(networkTypeIcon).isNull() - assertThat(networkTypeBackground).isNull() - assertThat(activityInVisible).isFalse() - assertThat(activityOutVisible).isFalse() - assertThat(activityContainerVisible).isFalse() - } + fun nonTerrestrial_defaultProperties() = runTest { + repository.isNonTerrestrial.setValue(true) + + val roaming by underTest.roaming.collectLastValue() + val networkTypeIcon by underTest.networkTypeIcon.collectLastValue() + val networkTypeBackground by underTest.networkTypeBackground.collectLastValue() + val activityInVisible by underTest.activityInVisible.collectLastValue() + val activityOutVisible by underTest.activityOutVisible.collectLastValue() + val activityContainerVisible by underTest.activityContainerVisible.collectLastValue() + + assertThat(roaming).isFalse() + assertThat(networkTypeIcon).isNull() + assertThat(networkTypeBackground).isNull() + assertThat(activityInVisible).isFalse() + assertThat(activityOutVisible).isFalse() + assertThat(activityContainerVisible).isFalse() + } @Test - fun nonTerrestrial_ignoresDefaultProperties() = - testScope.runTest { - repository.isNonTerrestrial.value = true - - val roaming by collectLastValue(underTest.roaming) - val networkTypeIcon by collectLastValue(underTest.networkTypeIcon) - val networkTypeBackground by collectLastValue(underTest.networkTypeBackground) - val activityInVisible by collectLastValue(underTest.activityInVisible) - val activityOutVisible by collectLastValue(underTest.activityOutVisible) - val activityContainerVisible by collectLastValue(underTest.activityContainerVisible) - - repository.setAllRoaming(true) - repository.setNetworkTypeKey(connectionsRepository.LTE_KEY) - // sets the background on cellular - repository.hasPrioritizedNetworkCapabilities.value = true - repository.dataActivityDirection.value = - DataActivityModel(hasActivityIn = true, hasActivityOut = true) - - assertThat(roaming).isFalse() - assertThat(networkTypeIcon).isNull() - assertThat(networkTypeBackground).isNull() - assertThat(activityInVisible).isFalse() - assertThat(activityOutVisible).isFalse() - assertThat(activityContainerVisible).isFalse() - } + fun nonTerrestrial_ignoresDefaultProperties() = runTest { + repository.isNonTerrestrial.setValue(true) + + val roaming by underTest.roaming.collectLastValue() + val networkTypeIcon by underTest.networkTypeIcon.collectLastValue() + val networkTypeBackground by underTest.networkTypeBackground.collectLastValue() + val activityInVisible by underTest.activityInVisible.collectLastValue() + val activityOutVisible by underTest.activityOutVisible.collectLastValue() + val activityContainerVisible by underTest.activityContainerVisible.collectLastValue() + + repository.setAllRoaming(true) + repository.setNetworkTypeKey(mobileConnectionsRepositoryKairos.fake.LTE_KEY) + // sets the background on cellular + repository.hasPrioritizedNetworkCapabilities.setValue(true) + repository.dataActivityDirection.setValue( + DataActivityModel(hasActivityIn = true, hasActivityOut = true) + ) + + assertThat(roaming).isFalse() + assertThat(networkTypeIcon).isNull() + assertThat(networkTypeBackground).isNull() + assertThat(activityInVisible).isFalse() + assertThat(activityOutVisible).isFalse() + assertThat(activityContainerVisible).isFalse() + } @DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) @Test - fun nonTerrestrial_usesSatelliteIcon_flagOff() = - testScope.runTest { - repository.isNonTerrestrial.value = true - repository.setAllLevels(0) - repository.satelliteLevel.value = 0 - - val latest by - collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class)) - - // Level 0 -> no connection - assertThat(latest).isNotNull() - assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_0) - - // 1-2 -> 1 bar - repository.setAllLevels(1) - repository.satelliteLevel.value = 1 - assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) - - repository.setAllLevels(2) - repository.satelliteLevel.value = 2 - assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) - - // 3-4 -> 2 bars - repository.setAllLevels(3) - repository.satelliteLevel.value = 3 - assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) - - repository.setAllLevels(4) - repository.satelliteLevel.value = 4 - assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) - } + fun nonTerrestrial_usesSatelliteIcon_flagOff() = runTest { + repository.isNonTerrestrial.setValue(true) + repository.setAllLevels(0) + repository.satelliteLevel.setValue(0) + + val latest by underTest.icon.map { it as SignalIconModel.Satellite }.collectLastValue() + + // Level 0 -> no connection + assertThat(latest).isNotNull() + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_0) + + // 1-2 -> 1 bar + repository.setAllLevels(1) + repository.satelliteLevel.setValue(1) + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) + + repository.setAllLevels(2) + repository.satelliteLevel.setValue(2) + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) + + // 3-4 -> 2 bars + repository.setAllLevels(3) + repository.satelliteLevel.setValue(3) + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) + + repository.setAllLevels(4) + repository.satelliteLevel.setValue(4) + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) + } @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) @Test - fun nonTerrestrial_usesSatelliteIcon_flagOn() = - testScope.runTest { - repository.isNonTerrestrial.value = true - repository.satelliteLevel.value = 0 + fun nonTerrestrial_usesSatelliteIcon_flagOn() = runTest { + repository.isNonTerrestrial.setValue(true) + repository.satelliteLevel.setValue(0) - val latest by - collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class)) + val latest by underTest.icon.map { it as SignalIconModel.Satellite }.collectLastValue() - // Level 0 -> no connection - assertThat(latest).isNotNull() - assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_0) + // Level 0 -> no connection + assertThat(latest).isNotNull() + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_0) - // 1-2 -> 1 bar - repository.satelliteLevel.value = 1 - assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) + // 1-2 -> 1 bar + repository.satelliteLevel.setValue(1) + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) - repository.satelliteLevel.value = 2 - assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) + repository.satelliteLevel.setValue(2) + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) - // 3-4 -> 2 bars - repository.satelliteLevel.value = 3 - assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) + // 3-4 -> 2 bars + repository.satelliteLevel.setValue(3) + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) - repository.satelliteLevel.value = 4 - assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) - } + repository.satelliteLevel.setValue(4) + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) + } @DisableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) @Test - fun satelliteIcon_ignoresInflateSignalStrength_flagOff() = - testScope.runTest { - // Note that this is the exact same test as above, but with inflateSignalStrength set to - // true we note that the level is unaffected by inflation - repository.inflateSignalStrength.value = true - repository.isNonTerrestrial.value = true - repository.setAllLevels(0) - repository.satelliteLevel.value = 0 - - val latest by - collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class)) - - // Level 0 -> no connection - assertThat(latest).isNotNull() - assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_0) - - // 1-2 -> 1 bar - repository.setAllLevels(1) - repository.satelliteLevel.value = 1 - assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) - - repository.setAllLevels(2) - repository.satelliteLevel.value = 2 - assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) - - // 3-4 -> 2 bars - repository.setAllLevels(3) - repository.satelliteLevel.value = 3 - assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) - - repository.setAllLevels(4) - repository.satelliteLevel.value = 4 - assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) - } + fun satelliteIcon_ignoresInflateSignalStrength_flagOff() = runTest { + // Note that this is the exact same test as above, but with inflateSignalStrength set to + // true we note that the level is unaffected by inflation + repository.inflateSignalStrength.setValue(true) + repository.isNonTerrestrial.setValue(true) + repository.setAllLevels(0) + repository.satelliteLevel.setValue(0) + + val latest by underTest.icon.map { it as SignalIconModel.Satellite }.collectLastValue() + + // Level 0 -> no connection + assertThat(latest).isNotNull() + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_0) + + // 1-2 -> 1 bar + repository.setAllLevels(1) + repository.satelliteLevel.setValue(1) + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) + + repository.setAllLevels(2) + repository.satelliteLevel.setValue(2) + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) + + // 3-4 -> 2 bars + repository.setAllLevels(3) + repository.satelliteLevel.setValue(3) + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) + + repository.setAllLevels(4) + repository.satelliteLevel.setValue(4) + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) + } @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) @Test - fun satelliteIcon_ignoresInflateSignalStrength_flagOn() = - testScope.runTest { - // Note that this is the exact same test as above, but with inflateSignalStrength set to - // true we note that the level is unaffected by inflation - repository.inflateSignalStrength.value = true - repository.isNonTerrestrial.value = true - repository.satelliteLevel.value = 0 - - val latest by - collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class)) - - // Level 0 -> no connection - assertThat(latest).isNotNull() - assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_0) - - // 1-2 -> 1 bar - repository.satelliteLevel.value = 1 - assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) - - repository.satelliteLevel.value = 2 - assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) - - // 3-4 -> 2 bars - repository.satelliteLevel.value = 3 - assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) - - repository.satelliteLevel.value = 4 - assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) - } + fun satelliteIcon_ignoresInflateSignalStrength_flagOn() = runTest { + // Note that this is the exact same test as above, but with inflateSignalStrength set to + // true we note that the level is unaffected by inflation + repository.inflateSignalStrength.setValue(true) + repository.isNonTerrestrial.setValue(true) + repository.satelliteLevel.setValue(0) - private fun createAndSetViewModel() { - underTest = - MobileIconViewModelKairos( - SUB_1_ID, - interactor, - airplaneModeInteractor, - constants, - testScope.backgroundScope, - ) + val latest by underTest.icon.map { it as SignalIconModel.Satellite }.collectLastValue() + + // Level 0 -> no connection + assertThat(latest).isNotNull() + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_0) + + // 1-2 -> 1 bar + repository.satelliteLevel.setValue(1) + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) + + repository.satelliteLevel.setValue(2) + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) + + // 3-4 -> 2 bars + repository.satelliteLevel.setValue(3) + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) + + repository.satelliteLevel.setValue(4) + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) } companion object { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairosTest.kt index e921430394c2..b11bad6f3ad3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairosTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairosTest.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. @@ -16,348 +16,323 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel +import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID import android.telephony.SubscriptionManager.PROFILE_CLASS_UNSET import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.SysuiTestCase -import com.android.systemui.flags.FakeFeatureFlagsClassic -import com.android.systemui.statusbar.phone.StatusBarLocation -import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository -import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor +import com.android.systemui.flags.Flags +import com.android.systemui.flags.fake +import com.android.systemui.flags.featureFlagsClassic +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.DataConnectionState +import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType +import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.UnknownNetworkType import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel -import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository -import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor -import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIconModel -import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger -import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger -import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy -import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants -import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository +import com.android.systemui.statusbar.pipeline.mobile.data.repository.fake +import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepositoryKairos +import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepositoryKairos import com.android.systemui.testKosmos -import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat -import junit.framework.Assert.assertFalse -import junit.framework.Assert.assertTrue -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.isActive -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 -@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") -@OptIn(ExperimentalCoroutinesApi::class) +@OptIn(ExperimentalKairosApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class MobileIconsViewModelKairosTest : SysuiTestCase() { - private val kosmos = testKosmos() - private lateinit var underTest: MobileIconsViewModelKairos - private val interactor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock()) - private val flags = FakeFeatureFlagsClassic() - - private lateinit var airplaneModeInteractor: AirplaneModeInteractor - @Mock private lateinit var constants: ConnectivityConstants - @Mock private lateinit var logger: MobileViewLogger - @Mock private lateinit var verboseLogger: VerboseMobileViewLogger - - private val testDispatcher = UnconfinedTestDispatcher() - private val testScope = TestScope(testDispatcher) + private val Kosmos.underTest + get() = mobileIconsViewModelKairos + + private val kosmos = + testKosmos().apply { + useUnconfinedTestDispatcher() + featureFlagsClassic.fake.apply { + setDefault(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS) + } + mobileConnectionsRepositoryKairos = + fakeMobileConnectionsRepositoryKairos.apply { + val subList = listOf(SUB_1, SUB_2) + setActiveMobileDataSubscriptionId(SUB_1.subscriptionId) + subscriptions.setValue(subList) + } + } - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) + private fun runTest(block: suspend KairosTestScope.() -> Unit) = + kosmos.run { runKairosTest { block() } } + + private fun KairosTestScope.setSubscriptions( + subList: List<SubscriptionModel>, + activeSubId: Int = subList.getOrNull(0)?.subscriptionId ?: INVALID_SUBSCRIPTION_ID, + ) { + println("setSubscriptions: mobileConnectionsRepositoryKairos.fake.subscriptions") + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(subList) + println( + "setSubscriptions: mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId" + ) + mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId(activeSubId) + } - airplaneModeInteractor = - AirplaneModeInteractor( - FakeAirplaneModeRepository(), - FakeConnectivityRepository(), - kosmos.fakeMobileConnectionsRepository, + @Test + fun subscriptionIdsFlow_matchesInteractor() = runTest { + val latest by underTest.subscriptionIds.collectLastValue() + setSubscriptions( + listOf( + SubscriptionModel( + subscriptionId = 1, + isOpportunistic = false, + carrierName = "Carrier 1", + profileClass = PROFILE_CLASS_UNSET, + ) ) - - underTest = - MobileIconsViewModelKairos( - logger, - verboseLogger, - interactor, - airplaneModeInteractor, - constants, - testScope.backgroundScope, + ) + assertThat(latest).isEqualTo(listOf(1)) + + setSubscriptions( + listOf( + SubscriptionModel( + subscriptionId = 2, + isOpportunistic = false, + carrierName = "Carrier 2", + profileClass = PROFILE_CLASS_UNSET, + ), + SubscriptionModel( + subscriptionId = 5, + isOpportunistic = true, + carrierName = "Carrier 5", + profileClass = PROFILE_CLASS_UNSET, + ), + SubscriptionModel( + subscriptionId = 7, + isOpportunistic = true, + carrierName = "Carrier 7", + profileClass = PROFILE_CLASS_UNSET, + ), ) + ) + assertThat(latest).isEqualTo(listOf(2, 5, 7)) - interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) + setSubscriptions(emptyList()) + assertThat(latest).isEmpty() } @Test - fun subscriptionIdsFlow_matchesInteractor() = - testScope.runTest { - var latest: List<Int>? = null - val job = underTest.subscriptionIdsFlow.onEach { latest = it }.launchIn(this) - - interactor.filteredSubscriptions.value = - listOf( - SubscriptionModel( - subscriptionId = 1, - isOpportunistic = false, - carrierName = "Carrier 1", - profileClass = PROFILE_CLASS_UNSET, - ) - ) - assertThat(latest).isEqualTo(listOf(1)) - - interactor.filteredSubscriptions.value = - listOf( - SubscriptionModel( - subscriptionId = 2, - isOpportunistic = false, - carrierName = "Carrier 2", - profileClass = PROFILE_CLASS_UNSET, - ), - SubscriptionModel( - subscriptionId = 5, - isOpportunistic = true, - carrierName = "Carrier 5", - profileClass = PROFILE_CLASS_UNSET, - ), - SubscriptionModel( - subscriptionId = 7, - isOpportunistic = true, - carrierName = "Carrier 7", - profileClass = PROFILE_CLASS_UNSET, - ), - ) - assertThat(latest).isEqualTo(listOf(2, 5, 7)) + fun firstMobileSubShowingNetworkTypeIcon_noSubs_false() = runTest { + val latest by underTest.firstMobileSubShowingNetworkTypeIcon.collectLastValue() - interactor.filteredSubscriptions.value = emptyList() - assertThat(latest).isEmpty() + setSubscriptions(emptyList()) - job.cancel() - } + assertThat(latest).isEqualTo(false) + } @Test - fun caching_mobileIconViewModelIsReusedForSameSubId() = - testScope.runTest { - val model1 = underTest.viewModelForSub(1, StatusBarLocation.HOME) - val model2 = underTest.viewModelForSub(1, StatusBarLocation.QS) + fun firstMobileSubShowingNetworkTypeIcon_oneSub_notShowingRat_false() = runTest { + val latest by underTest.firstMobileSubShowingNetworkTypeIcon.collectLastValue() - assertThat(model1.commonImpl).isSameInstanceAs(model2.commonImpl) - } + setSubscriptions(listOf(SUB_1)) + + // The unknown icon group doesn't show a RAT + mobileConnectionsRepositoryKairos.fake.mobileConnectionsBySubId + .sample()[SUB_1.subscriptionId] + ?.resolvedNetworkType + ?.setValue(UnknownNetworkType) + + assertThat(latest).isFalse() + } @Test - fun caching_invalidViewModelsAreRemovedFromCacheWhenSubDisappears() = - testScope.runTest { - // Retrieve models to trigger caching - val model1 = underTest.viewModelForSub(1, StatusBarLocation.HOME) - val model2 = underTest.viewModelForSub(2, StatusBarLocation.QS) + fun firstMobileSubShowingNetworkTypeIcon_oneSub_showingRat_true() = runTest { + val latest by underTest.firstMobileSubShowingNetworkTypeIcon.collectLastValue() + setSubscriptions(listOf(SUB_1)) - // Both impls are cached - assertThat(underTest.reuseCache.keys).containsExactly(1, 2) + mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true) - // SUB_1 is removed from the list... - interactor.filteredSubscriptions.value = listOf(SUB_2) + // The 3G icon group will show a RAT + val repo = + mobileConnectionsRepositoryKairos.fake.mobileConnectionsBySubId + .sample()[SUB_1.subscriptionId]!! - // ... and dropped from the cache - assertThat(underTest.reuseCache.keys).containsExactly(2) - } + repo.resolvedNetworkType.setValue( + DefaultNetworkType(mobileConnectionsRepositoryKairos.fake.GSM_KEY) + ) + repo.dataConnectionState.setValue(DataConnectionState.Connected) + + assertThat(latest).isEqualTo(true) + } @Test - fun caching_invalidatedViewModelsAreCanceled() = - testScope.runTest { - // Retrieve models to trigger caching - val model1 = underTest.viewModelForSub(1, StatusBarLocation.HOME) - val model2 = underTest.viewModelForSub(2, StatusBarLocation.QS) + fun firstMobileSubShowingNetworkTypeIcon_updatesAsSubUpdates() = runTest { + val latest by underTest.firstMobileSubShowingNetworkTypeIcon.collectLastValue() + setSubscriptions(listOf(SUB_1)) - var scope1 = underTest.reuseCache[1]?.second - var scope2 = underTest.reuseCache[2]?.second + mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true) - // Scopes are not canceled - assertTrue(scope1!!.isActive) - assertTrue(scope2!!.isActive) + val repo = + mobileConnectionsRepositoryKairos.fake.mobileConnectionsBySubId + .sample()[SUB_1.subscriptionId]!! - // SUB_1 is removed from the list... - interactor.filteredSubscriptions.value = listOf(SUB_2) + repo.dataConnectionState.setValue(DataConnectionState.Connected) - // scope1 is canceled - assertFalse(scope1!!.isActive) - assertTrue(scope2!!.isActive) - } + repo.resolvedNetworkType.setValue( + DefaultNetworkType(mobileConnectionsRepositoryKairos.fake.GSM_KEY) + ) + assertThat(latest).isEqualTo(true) + + mobileConnectionsRepositoryKairos.fake.defaultMobileIconGroup.setValue( + TelephonyIcons.UNKNOWN + ) + + repo.resolvedNetworkType.setValue(UnknownNetworkType) + assertThat(latest).isEqualTo(false) + + repo.resolvedNetworkType.setValue( + DefaultNetworkType(mobileConnectionsRepositoryKairos.fake.LTE_KEY) + ) + assertThat(latest).isEqualTo(true) + } @Test - fun firstMobileSubShowingNetworkTypeIcon_noSubs_false() = - testScope.runTest { - var latest: Boolean? = null - val job = - underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this) + fun firstMobileSubShowingNetworkTypeIcon_multipleSubs_lastSubNotShowingRat_false() = runTest { + val latest by underTest.firstMobileSubShowingNetworkTypeIcon.collectLastValue() - interactor.filteredSubscriptions.value = emptyList() + mobileConnectionsRepositoryKairos.fake.defaultMobileIconGroup.setValue( + TelephonyIcons.UNKNOWN + ) + mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true) - assertThat(latest).isFalse() + val repo1 = + mobileConnectionsRepositoryKairos.fake.mobileConnectionsBySubId + .sample()[SUB_1.subscriptionId]!! - job.cancel() - } + repo1.resolvedNetworkType.setValue( + DefaultNetworkType(mobileConnectionsRepositoryKairos.fake.GSM_KEY) + ) + + val repo2 = + mobileConnectionsRepositoryKairos.fake.mobileConnectionsBySubId + .sample()[SUB_2.subscriptionId]!! + + repo2.resolvedNetworkType.setValue(UnknownNetworkType) + + assertThat(latest).isFalse() + } @Test - fun firstMobileSubShowingNetworkTypeIcon_oneSub_notShowingRat_false() = - testScope.runTest { - var latest: Boolean? = null - val job = - underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this) + fun firstMobileSubShowingNetworkTypeIcon_multipleSubs_lastSubShowingRat_true() = runTest { + val latest by underTest.firstMobileSubShowingNetworkTypeIcon.collectLastValue() - interactor.filteredSubscriptions.value = listOf(SUB_1) - // The unknown icon group doesn't show a RAT - interactor.getInteractorForSubId(1)!!.networkTypeIconGroup.value = - NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN) + mobileConnectionsRepositoryKairos.fake.defaultMobileIconGroup.setValue( + TelephonyIcons.UNKNOWN + ) + mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true) - assertThat(latest).isFalse() + val repo1 = + mobileConnectionsRepositoryKairos.fake.mobileConnectionsBySubId + .sample()[SUB_1.subscriptionId]!! - job.cancel() - } + repo1.dataConnectionState.setValue(DataConnectionState.Connected) + repo1.resolvedNetworkType.setValue(UnknownNetworkType) + + val repo2 = + mobileConnectionsRepositoryKairos.fake.mobileConnectionsBySubId + .sample()[SUB_2.subscriptionId]!! + + repo2.dataConnectionState.setValue(DataConnectionState.Connected) + repo2.resolvedNetworkType.setValue( + DefaultNetworkType(mobileConnectionsRepositoryKairos.fake.GSM_KEY) + ) + + assertThat(latest).isEqualTo(true) + } @Test - fun firstMobileSubShowingNetworkTypeIcon_oneSub_showingRat_true() = - testScope.runTest { - var latest: Boolean? = null - val job = - underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this) + fun firstMobileSubShowingNetworkTypeIcon_subListUpdates_valAlsoUpdates() = runTest { + val latest by underTest.firstMobileSubShowingNetworkTypeIcon.collectLastValue() - interactor.filteredSubscriptions.value = listOf(SUB_1) - // The 3G icon group will show a RAT - interactor.getInteractorForSubId(1)!!.networkTypeIconGroup.value = - NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G) + mobileConnectionsRepositoryKairos.fake.defaultMobileIconGroup.setValue( + TelephonyIcons.UNKNOWN + ) + mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true) - assertThat(latest).isTrue() + val repo1 = + mobileConnectionsRepositoryKairos.fake.mobileConnectionsBySubId + .sample()[SUB_1.subscriptionId]!! - job.cancel() - } + repo1.dataConnectionState.setValue(DataConnectionState.Connected) + repo1.resolvedNetworkType.setValue(UnknownNetworkType) - @Test - fun firstMobileSubShowingNetworkTypeIcon_updatesAsSubUpdates() = - testScope.runTest { - var latest: Boolean? = null - val job = - underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this) + val repo2 = + mobileConnectionsRepositoryKairos.fake.mobileConnectionsBySubId + .sample()[SUB_2.subscriptionId]!! - interactor.filteredSubscriptions.value = listOf(SUB_1) - val sub1Interactor = interactor.getInteractorForSubId(1)!! + repo2.dataConnectionState.setValue(DataConnectionState.Connected) + repo2.resolvedNetworkType.setValue( + DefaultNetworkType(mobileConnectionsRepositoryKairos.fake.GSM_KEY) + ) - sub1Interactor.networkTypeIconGroup.value = - NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G) - assertThat(latest).isTrue() + assertThat(latest).isEqualTo(true) - sub1Interactor.networkTypeIconGroup.value = - NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN) - assertThat(latest).isFalse() + // WHEN the sub list gets new subscriptions where the last subscription is not showing + // the network type icon + setSubscriptions(listOf(SUB_1, SUB_2, SUB_3)) - sub1Interactor.networkTypeIconGroup.value = - NetworkTypeIconModel.DefaultIcon(TelephonyIcons.LTE) - assertThat(latest).isTrue() + val repo3 = + mobileConnectionsRepositoryKairos.fake.mobileConnectionsBySubId + .sample()[SUB_3.subscriptionId]!! - job.cancel() - } + repo3.dataConnectionState.setValue(DataConnectionState.Connected) + repo3.resolvedNetworkType.setValue(UnknownNetworkType) + + // THEN the flow updates + assertThat(latest).isEqualTo(false) + } @Test - fun firstMobileSubShowingNetworkTypeIcon_multipleSubs_lastSubNotShowingRat_false() = - testScope.runTest { - var latest: Boolean? = null - val job = - underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this) + fun firstMobileSubShowingNetworkTypeIcon_subListReorders_valAlsoUpdates() = runTest { + val latest by underTest.firstMobileSubShowingNetworkTypeIcon.collectLastValue() - interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) - interactor.getInteractorForSubId(1)?.networkTypeIconGroup?.value = - NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G) - interactor.getInteractorForSubId(2)!!.networkTypeIconGroup.value = - NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN) + mobileConnectionsRepositoryKairos.fake.defaultMobileIconGroup.setValue( + TelephonyIcons.UNKNOWN + ) + mobileConnectionsRepositoryKairos.fake.mobileIsDefault.setValue(true) - assertThat(latest).isFalse() + setSubscriptions(listOf(SUB_1, SUB_2)) + // Immediately switch the order so that we've created both interactors + setSubscriptions(listOf(SUB_2, SUB_1)) - job.cancel() - } + val repos = mobileConnectionsRepositoryKairos.fake.mobileConnectionsBySubId.sample() + val repo1 = repos[SUB_1.subscriptionId]!! + repo1.dataConnectionState.setValue(DataConnectionState.Connected) - @Test - fun firstMobileSubShowingNetworkTypeIcon_multipleSubs_lastSubShowingRat_true() = - testScope.runTest { - var latest: Boolean? = null - val job = - underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this) - - interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) - interactor.getInteractorForSubId(1)?.networkTypeIconGroup?.value = - NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN) - interactor.getInteractorForSubId(2)!!.networkTypeIconGroup.value = - NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G) - - assertThat(latest).isTrue() - job.cancel() - } + val repo2 = repos[SUB_2.subscriptionId]!! + repo2.dataConnectionState.setValue(DataConnectionState.Connected) - @Test - fun firstMobileSubShowingNetworkTypeIcon_subListUpdates_valAlsoUpdates() = - testScope.runTest { - var latest: Boolean? = null - val job = - underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this) - - interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) - interactor.getInteractorForSubId(1)?.networkTypeIconGroup?.value = - NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN) - interactor.getInteractorForSubId(2)!!.networkTypeIconGroup.value = - NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G) - - assertThat(latest).isTrue() - - // WHEN the sub list gets new subscriptions where the last subscription is not showing - // the network type icon - interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2, SUB_3) - interactor.getInteractorForSubId(3)!!.networkTypeIconGroup.value = - NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN) - - // THEN the flow updates - assertThat(latest).isFalse() - - job.cancel() - } + setSubscriptions(listOf(SUB_1, SUB_2)) + repo1.resolvedNetworkType.setValue(UnknownNetworkType) + repo2.resolvedNetworkType.setValue( + DefaultNetworkType(mobileConnectionsRepositoryKairos.fake.GSM_KEY) + ) - @Test - fun firstMobileSubShowingNetworkTypeIcon_subListReorders_valAlsoUpdates() = - testScope.runTest { - var latest: Boolean? = null - val job = - underTest.firstMobileSubShowingNetworkTypeIcon.onEach { latest = it }.launchIn(this) - - interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) - // Immediately switch the order so that we've created both interactors - interactor.filteredSubscriptions.value = listOf(SUB_2, SUB_1) - val sub1Interactor = interactor.getInteractorForSubId(1)!! - val sub2Interactor = interactor.getInteractorForSubId(2)!! - - interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) - sub1Interactor.networkTypeIconGroup.value = - NetworkTypeIconModel.DefaultIcon(TelephonyIcons.UNKNOWN) - sub2Interactor.networkTypeIconGroup.value = - NetworkTypeIconModel.DefaultIcon(TelephonyIcons.THREE_G) - assertThat(latest).isTrue() - - // WHEN sub1 becomes last and sub1 has no network type icon - interactor.filteredSubscriptions.value = listOf(SUB_2, SUB_1) - - // THEN the flow updates - assertThat(latest).isFalse() - - // WHEN sub2 becomes last and sub2 has a network type icon - interactor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) - - // THEN the flow updates - assertThat(latest).isTrue() - - job.cancel() - } + assertThat(latest).isEqualTo(true) + + // WHEN sub1 becomes last and sub1 has no network type icon + setSubscriptions(listOf(SUB_2, SUB_1)) + + // THEN the flow updates + assertThat(latest).isEqualTo(false) + + // WHEN sub2 becomes last and sub2 has a network type icon + setSubscriptions(listOf(SUB_1, SUB_2)) + + // THEN the flow updates + assertThat(latest).isEqualTo(true) + } companion object { private val SUB_1 = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairosTest.kt index ce35d9d8610f..75f5cbb041c4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairosTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairosTest.kt @@ -21,81 +21,83 @@ 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.common.shared.model.Icon +import com.android.systemui.flags.Flags +import com.android.systemui.flags.fake +import com.android.systemui.flags.featureFlagsClassic +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.Kosmos.Fixture -import com.android.systemui.kosmos.runTest -import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher -import com.android.systemui.lifecycle.activateIn import com.android.systemui.statusbar.core.NewStatusBarIcons import com.android.systemui.statusbar.core.StatusBarRootModernization import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel -import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.fakeMobileIconsInteractor -import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel +import com.android.systemui.statusbar.pipeline.mobile.data.repository.fake +import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepositoryKairos +import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepositoryKairos import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat -import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +@OptIn(ExperimentalKairosApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class StackedMobileIconViewModelKairosTest : SysuiTestCase() { - private val kosmos = testKosmos().useUnconfinedTestDispatcher() - private val testScope = kosmos.testScope - - private val Kosmos.underTest: StackedMobileIconViewModelKairos by Fixture { - stackedMobileIconViewModelKairos - } + private val kosmos = + testKosmos().useUnconfinedTestDispatcher().apply { + mobileConnectionsRepositoryKairos = fakeMobileConnectionsRepositoryKairos + featureFlagsClassic.fake.apply { + setDefault(Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS) + } + } - @Before - fun setUp() { - kosmos.underTest.activateIn(testScope) - } + private val Kosmos.underTest + get() = stackedMobileIconViewModelKairos @Test @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) fun dualSim_filtersOutNonDualConnections() = - kosmos.runTest { - fakeMobileIconsInteractor.filteredSubscriptions.value = listOf() + kosmos.runKairosTest { + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf()) assertThat(underTest.dualSim).isNull() - fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1) + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1)) assertThat(underTest.dualSim).isNull() - fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2, SUB_3) + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue( + listOf(SUB_1, SUB_2, SUB_3) + ) assertThat(underTest.dualSim).isNull() - fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1, SUB_2)) assertThat(underTest.dualSim).isNotNull() } @Test @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) fun dualSim_filtersOutNonCellularIcons() = - kosmos.runTest { - fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1) + kosmos.runKairosTest { + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1)) assertThat(underTest.dualSim).isNull() - fakeMobileIconsInteractor - .getInteractorForSubId(SUB_1.subscriptionId)!! - .signalLevelIcon - .value = - SignalIconModel.Satellite( - level = 0, - icon = Icon.Resource(res = 0, contentDescription = null), - ) - fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) + mobileConnectionsRepositoryKairos.fake.mobileConnectionsBySubId + .sample()[SUB_1.subscriptionId]!! + .apply { + isNonTerrestrial.setValue(true) + satelliteLevel.setValue(0) + } + + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1, SUB_2)) assertThat(underTest.dualSim).isNull() } @Test @EnableFlags(NewStatusBarIcons.FLAG_NAME, StatusBarRootModernization.FLAG_NAME) fun dualSim_tracksActiveSubId() = - kosmos.runTest { + kosmos.runKairosTest { // Active sub id is null, order is unchanged - fakeMobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) + mobileConnectionsRepositoryKairos.fake.subscriptions.setValue(listOf(SUB_1, SUB_2)) setIconLevel(SUB_1.subscriptionId, 1) setIconLevel(SUB_2.subscriptionId, 2) @@ -103,16 +105,21 @@ class StackedMobileIconViewModelKairosTest : SysuiTestCase() { assertThat(underTest.dualSim!!.secondary.level).isEqualTo(2) // Active sub is 2, order is swapped - fakeMobileIconsInteractor.activeMobileDataSubscriptionId.value = SUB_2.subscriptionId + mobileConnectionsRepositoryKairos.fake.setActiveMobileDataSubscriptionId( + SUB_2.subscriptionId + ) assertThat(underTest.dualSim!!.primary.level).isEqualTo(2) assertThat(underTest.dualSim!!.secondary.level).isEqualTo(1) } - private fun setIconLevel(subId: Int, level: Int) { - with(kosmos.fakeMobileIconsInteractor.getInteractorForSubId(subId)!!) { - signalLevelIcon.value = - (signalLevelIcon.value as SignalIconModel.Cellular).copy(level = level) + private suspend fun KairosTestScope.setIconLevel(subId: Int, level: Int) { + mobileConnectionsRepositoryKairos.fake.mobileConnectionsBySubId.sample()[subId]!!.apply { + isNonTerrestrial.setValue(false) + isInService.setValue(true) + inflateSignalStrength.setValue(false) + isGsm.setValue(true) + primaryLevel.setValue(level) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelTest.kt index d7bcf88f6941..8a7b698623f8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelTest.kt @@ -45,8 +45,8 @@ class StackedMobileIconViewModelTest : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val testScope = kosmos.testScope - private val Kosmos.underTest: StackedMobileIconViewModel by Fixture { - stackedMobileIconViewModel + private val Kosmos.underTest: StackedMobileIconViewModelImpl by Fixture { + stackedMobileIconViewModelImpl } @Before 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 db1977b3ff45..93489e90fb85 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 @@ -49,6 +49,7 @@ import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIc import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorKairosImpl import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModelKairos import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxyImpl import com.android.systemui.statusbar.pipeline.mobile.util.SubscriptionManagerProxy @@ -94,6 +95,7 @@ import kotlinx.coroutines.flow.Flow MobileRepositorySwitcherKairos.Module::class, MobileConnectionsRepositoryKairosImpl.Module::class, MobileIconsInteractorKairosImpl.Module::class, + MobileIconsViewModelKairos.Module::class, MobileConnectionRepositoryKairosFactoryImpl.Module::class, MobileConnectionsRepositoryKairosAdapter.Module::class, MobileIconsInteractorKairosAdapter.Module::class, @@ -217,6 +219,7 @@ abstract class StatusBarPipelineModule { fun provideFirstMobileSubShowingNetworkTypeIconProvider( mobileIconsViewModel: MobileIconsViewModel ): Supplier<Flow<Boolean>> { + // TODO: kairos-ify return Supplier<Flow<Boolean>> { mobileIconsViewModel.firstMobileSubShowingNetworkTypeIcon } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairos.kt index a9399593973b..3d58f84e1f91 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairos.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairos.kt @@ -48,6 +48,8 @@ interface MobileIconInteractorKairos { /** The table log created for this connection */ val tableLogBuffer: TableLogBuffer + val subscriptionId: Int + /** The current mobile data activity */ val activity: State<DataActivityModel> @@ -146,6 +148,9 @@ class MobileIconInteractorKairosImpl( private val carrierIdOverrides: MobileIconCarrierIdOverrides = MobileIconCarrierIdOverridesImpl(), ) : MobileIconInteractorKairos, KairosBuilder by kairosBuilder() { + override val subscriptionId: Int + get() = connectionRepository.subId + override val tableLogBuffer: TableLogBuffer get() = connectionRepository.tableLogBuffer diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/StackedMobileBindableIcon.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/StackedMobileBindableIcon.kt index fa9fa4c1366f..32ebe884062d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/StackedMobileBindableIcon.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/StackedMobileBindableIcon.kt @@ -24,7 +24,7 @@ import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon import com.android.systemui.statusbar.pipeline.icons.shared.model.ModernStatusBarViewCreator import com.android.systemui.statusbar.pipeline.mobile.ui.binder.StackedMobileIconBinder import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel -import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.StackedMobileIconViewModel +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.StackedMobileIconViewModelImpl import com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarComposeIconView import javax.inject.Inject @@ -34,7 +34,7 @@ class StackedMobileBindableIcon constructor( context: Context, mobileIconsViewModel: MobileIconsViewModel, - viewModelFactory: StackedMobileIconViewModel.Factory, + viewModelFactory: StackedMobileIconViewModelImpl.Factory, ) : BindableIcon { override val slot: String = context.getString(com.android.internal.R.string.status_bar_stacked_mobile) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/StackedMobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/StackedMobileIconBinder.kt index c9fc53ecadc0..fef5bfe2b7d8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/StackedMobileIconBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/StackedMobileIconBinder.kt @@ -25,7 +25,7 @@ import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel -import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.StackedMobileIconViewModel +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.StackedMobileIconViewModelImpl import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIcon import com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarComposeIconView @@ -34,7 +34,7 @@ object StackedMobileIconBinder { fun bind( view: SingleBindableStatusBarComposeIconView, mobileIconsViewModel: MobileIconsViewModel, - viewModelFactory: StackedMobileIconViewModel.Factory, + viewModelFactory: StackedMobileIconViewModelImpl.Factory, ): ModernStatusBarViewBinding { return SingleBindableStatusBarComposeIconView.withDefaultBinding( view = view, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModelKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModelKairos.kt index fce8c85338f3..d2e3a1489040 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModelKairos.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModelKairos.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,14 +17,12 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel import android.graphics.Color +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kairos.State +import com.android.systemui.kairos.combine import com.android.systemui.statusbar.phone.StatusBarLocation -import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractorKairos import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.stateIn /** * A view model for an individual mobile icon that embeds the notion of a [StatusBarLocation]. This @@ -35,45 +33,47 @@ import kotlinx.coroutines.flow.stateIn * @property location the [StatusBarLocation] of this VM. * @property verboseLogger an optional logger to log extremely verbose view updates. */ +@ExperimentalKairosApi abstract class LocationBasedMobileViewModelKairos( - val commonImpl: MobileIconViewModelCommonKairos, + val commonImpl: MobileIconViewModelKairosCommon, val location: StatusBarLocation, val verboseLogger: VerboseMobileViewLogger?, -) : MobileIconViewModelCommonKairos by commonImpl { +) : MobileIconViewModelKairosCommon by commonImpl { val defaultColor: Int = Color.WHITE companion object { fun viewModelForLocation( - commonImpl: MobileIconViewModelCommon, - interactor: MobileIconInteractor, + commonImpl: MobileIconViewModelKairosCommon, + interactor: MobileIconInteractorKairos, verboseMobileViewLogger: VerboseMobileViewLogger, location: StatusBarLocation, - scope: CoroutineScope, - ): LocationBasedMobileViewModel = + ): LocationBasedMobileViewModelKairos = when (location) { StatusBarLocation.HOME -> - HomeMobileIconViewModel(commonImpl, verboseMobileViewLogger) - StatusBarLocation.KEYGUARD -> KeyguardMobileIconViewModel(commonImpl) - StatusBarLocation.QS -> QsMobileIconViewModel(commonImpl) + HomeMobileIconViewModelKairos(commonImpl, verboseMobileViewLogger) + StatusBarLocation.KEYGUARD -> KeyguardMobileIconViewModelKairos(commonImpl) + StatusBarLocation.QS -> QsMobileIconViewModelKairos(commonImpl) StatusBarLocation.SHADE_CARRIER_GROUP -> - ShadeCarrierGroupMobileIconViewModel(commonImpl, interactor, scope) + ShadeCarrierGroupMobileIconViewModelKairos(commonImpl, interactor) } } } +@ExperimentalKairosApi class HomeMobileIconViewModelKairos( - commonImpl: MobileIconViewModelCommonKairos, + commonImpl: MobileIconViewModelKairosCommon, verboseMobileViewLogger: VerboseMobileViewLogger, ) : - MobileIconViewModelCommonKairos, + MobileIconViewModelKairosCommon, LocationBasedMobileViewModelKairos( commonImpl, location = StatusBarLocation.HOME, verboseMobileViewLogger, ) -class QsMobileIconViewModelKairos(commonImpl: MobileIconViewModelCommonKairos) : - MobileIconViewModelCommonKairos, +@ExperimentalKairosApi +class QsMobileIconViewModelKairos(commonImpl: MobileIconViewModelKairosCommon) : + MobileIconViewModelKairosCommon, LocationBasedMobileViewModelKairos( commonImpl, location = StatusBarLocation.QS, @@ -81,30 +81,34 @@ class QsMobileIconViewModelKairos(commonImpl: MobileIconViewModelCommonKairos) : verboseLogger = null, ) +@ExperimentalKairosApi class ShadeCarrierGroupMobileIconViewModelKairos( - commonImpl: MobileIconViewModelCommonKairos, - interactor: MobileIconInteractor, - scope: CoroutineScope, + commonImpl: MobileIconViewModelKairosCommon, + private val interactor: MobileIconInteractorKairos, ) : - MobileIconViewModelCommonKairos, + MobileIconViewModelKairosCommon, LocationBasedMobileViewModelKairos( commonImpl, location = StatusBarLocation.SHADE_CARRIER_GROUP, // Only do verbose logging for the Home location. verboseLogger = null, ) { - private val isSingleCarrier = interactor.isSingleCarrier - val carrierName = interactor.carrierName - override val isVisible: StateFlow<Boolean> = + private val isSingleCarrier: State<Boolean> + get() = interactor.isSingleCarrier + + val carrierName: State<String> + get() = interactor.carrierName + + override val isVisible: State<Boolean> = combine(super.isVisible, isSingleCarrier) { isVisible, isSingleCarrier -> - if (isSingleCarrier) false else isVisible - } - .stateIn(scope, SharingStarted.WhileSubscribed(), super.isVisible.value) + !isSingleCarrier && isVisible + } } -class KeyguardMobileIconViewModelKairos(commonImpl: MobileIconViewModelCommonKairos) : - MobileIconViewModelCommonKairos, +@ExperimentalKairosApi +class KeyguardMobileIconViewModelKairos(commonImpl: MobileIconViewModelKairosCommon) : + MobileIconViewModelKairosCommon, LocationBasedMobileViewModelKairos( commonImpl, location = StatusBarLocation.KEYGUARD, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairos.kt index cc7fc0964dae..0a0f9640a920 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairos.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairos.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,206 +17,192 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel import com.android.systemui.Flags.statusBarStaticInoutIndicators +import com.android.systemui.KairosBuilder +import com.android.systemui.activated import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon +import com.android.systemui.flags.FeatureFlagsClassic +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kairos.State as KairosState +import com.android.systemui.kairos.State +import com.android.systemui.kairos.combine +import com.android.systemui.kairos.flatMap +import com.android.systemui.kairos.map +import com.android.systemui.kairos.stateOf +import com.android.systemui.kairosBuilder import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.res.R import com.android.systemui.statusbar.core.NewStatusBarIcons import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor -import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor -import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractorKairos import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel import com.android.systemui.statusbar.pipeline.mobile.ui.model.MobileContentDescription import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel -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.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.stateIn /** Common interface for all of the location-based mobile icon view models. */ -interface MobileIconViewModelCommonKairos : MobileIconViewModelCommon { - override val subscriptionId: Int +@ExperimentalKairosApi +interface MobileIconViewModelKairosCommon { + val subscriptionId: Int + val iconInteractor: MobileIconInteractorKairos /** True if this view should be visible at all. */ - override val isVisible: StateFlow<Boolean> - override val icon: Flow<SignalIconModel> - override val contentDescription: Flow<MobileContentDescription?> - override val roaming: Flow<Boolean> + val isVisible: KairosState<Boolean> + val icon: KairosState<SignalIconModel> + val contentDescription: KairosState<MobileContentDescription?> + val roaming: KairosState<Boolean> /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */ - override val networkTypeIcon: Flow<Icon.Resource?> + val networkTypeIcon: KairosState<Icon.Resource?> /** The slice attribution. Drawn as a background layer */ - override val networkTypeBackground: StateFlow<Icon.Resource?> - override val activityInVisible: Flow<Boolean> - override val activityOutVisible: Flow<Boolean> - override val activityContainerVisible: Flow<Boolean> + val networkTypeBackground: KairosState<Icon.Resource?> + val activityInVisible: KairosState<Boolean> + val activityOutVisible: KairosState<Boolean> + val activityContainerVisible: KairosState<Boolean> } /** * View model for the state of a single mobile icon. Each [MobileIconViewModel] will keep watch over - * a single line of service via [MobileIconInteractor] and update the UI based on that + * a single line of service via [MobileIconInteractorKairos] and update the UI based on that * subscription's information. * - * There will be exactly one [MobileIconViewModel] per filtered subscription offered from - * [MobileIconsInteractor.filteredSubscriptions]. + * There will be exactly one [MobileIconViewModelKairos] per filtered subscription offered from + * [MobileIconsInteractorKairos.filteredSubscriptions]. * - * For the sake of keeping log spam in check, every flow funding the [MobileIconViewModelCommon] - * interface is implemented as a [StateFlow]. This ensures that each location-based mobile icon view - * model gets the exact same information, as well as allows us to log that unified state only once - * per icon. + * For the sake of keeping log spam in check, every flow funding the + * [MobileIconViewModelKairosCommon] interface is implemented as a [StateFlow]. This ensures that + * each location-based mobile icon view model gets the exact same information, as well as allows us + * to log that unified state only once per icon. */ +@ExperimentalKairosApi class MobileIconViewModelKairos( override val subscriptionId: Int, - iconInteractor: MobileIconInteractor, - airplaneModeInteractor: AirplaneModeInteractor, - constants: ConnectivityConstants, - scope: CoroutineScope, -) : MobileIconViewModelCommonKairos { - private val cellProvider by lazy { - CellularIconViewModelKairos( - subscriptionId, - iconInteractor, - airplaneModeInteractor, - constants, - scope, - ) + override val iconInteractor: MobileIconInteractorKairos, + private val airplaneModeInteractor: AirplaneModeInteractor, + private val constants: ConnectivityConstants, + private val flags: FeatureFlagsClassic, +) : MobileIconViewModelKairosCommon, KairosBuilder by kairosBuilder() { + + private val isAirplaneMode: State<Boolean> = buildState { + airplaneModeInteractor.isAirplaneMode.toState() } private val satelliteProvider by lazy { - CarrierBasedSatelliteViewModelKairosImpl( - subscriptionId, - airplaneModeInteractor, - iconInteractor, - scope, - ) + CarrierBasedSatelliteViewModelKairosImpl(subscriptionId, iconInteractor, isAirplaneMode) } /** * Similar to repository switching, this allows us to split up the logic of satellite/cellular * states, since they are different by nature */ - private val vmProvider: Flow<MobileIconViewModelCommon> = - iconInteractor.isNonTerrestrial - .mapLatest { nonTerrestrial -> - if (nonTerrestrial) { - satelliteProvider - } else { - cellProvider + private val vmProvider: KairosState<MobileIconViewModelKairosCommon> = buildState { + iconInteractor.isNonTerrestrial.mapLatestBuild { nonTerrestrial -> + if (nonTerrestrial) { + satelliteProvider + } else { + activated { + CellularIconViewModelKairos( + subscriptionId, + iconInteractor, + airplaneModeInteractor, + constants, + flags, + ) } } - .stateIn(scope, SharingStarted.WhileSubscribed(), cellProvider) + } + } - override val isVisible: StateFlow<Boolean> = - vmProvider - .flatMapLatest { it.isVisible } - .stateIn(scope, SharingStarted.WhileSubscribed(), false) + override val isVisible: KairosState<Boolean> = vmProvider.flatMap { it.isVisible } - override val icon: Flow<SignalIconModel> = vmProvider.flatMapLatest { it.icon } + override val icon: KairosState<SignalIconModel> = vmProvider.flatMap { it.icon } - override val contentDescription: Flow<MobileContentDescription?> = - vmProvider.flatMapLatest { it.contentDescription } + override val contentDescription: KairosState<MobileContentDescription?> = + vmProvider.flatMap { it.contentDescription } - override val roaming: Flow<Boolean> = vmProvider.flatMapLatest { it.roaming } + override val roaming: KairosState<Boolean> = vmProvider.flatMap { it.roaming } - override val networkTypeIcon: Flow<Icon.Resource?> = - vmProvider.flatMapLatest { it.networkTypeIcon } + override val networkTypeIcon: KairosState<Icon.Resource?> = + vmProvider.flatMap { it.networkTypeIcon } - override val networkTypeBackground: StateFlow<Icon.Resource?> = - vmProvider - .flatMapLatest { it.networkTypeBackground } - .stateIn(scope, SharingStarted.WhileSubscribed(), null) + override val networkTypeBackground: KairosState<Icon.Resource?> = + vmProvider.flatMap { it.networkTypeBackground } - override val activityInVisible: Flow<Boolean> = - vmProvider.flatMapLatest { it.activityInVisible } + override val activityInVisible: KairosState<Boolean> = + vmProvider.flatMap { it.activityInVisible } - override val activityOutVisible: Flow<Boolean> = - vmProvider.flatMapLatest { it.activityOutVisible } + override val activityOutVisible: KairosState<Boolean> = + vmProvider.flatMap { it.activityOutVisible } - override val activityContainerVisible: Flow<Boolean> = - vmProvider.flatMapLatest { it.activityContainerVisible } + override val activityContainerVisible: KairosState<Boolean> = + vmProvider.flatMap { it.activityContainerVisible } } /** Representation of this network when it is non-terrestrial (e.g., satellite) */ +@ExperimentalKairosApi private class CarrierBasedSatelliteViewModelKairosImpl( override val subscriptionId: Int, - airplaneModeInteractor: AirplaneModeInteractor, - interactor: MobileIconInteractor, - scope: CoroutineScope, -) : MobileIconViewModelCommon, MobileIconViewModelCommonKairos { - override val isVisible: StateFlow<Boolean> = - airplaneModeInteractor.isAirplaneMode - .map { !it } - .stateIn(scope, SharingStarted.WhileSubscribed(), false) - - override val icon: Flow<SignalIconModel> = interactor.signalLevelIcon + override val iconInteractor: MobileIconInteractorKairos, + isAirplaneMode: KairosState<Boolean>, +) : MobileIconViewModelKairosCommon { + override val isVisible: KairosState<Boolean> = isAirplaneMode.map { !it } + override val icon: KairosState<SignalIconModel> + get() = iconInteractor.signalLevelIcon - override val contentDescription: Flow<MobileContentDescription?> = MutableStateFlow(null) + override val contentDescription: KairosState<MobileContentDescription?> = stateOf(null) /** These fields are not used for satellite icons currently */ - override val roaming: Flow<Boolean> = flowOf(false) - override val networkTypeIcon: Flow<Icon.Resource?> = flowOf(null) - override val networkTypeBackground: StateFlow<Icon.Resource?> = MutableStateFlow(null) - override val activityInVisible: Flow<Boolean> = flowOf(false) - override val activityOutVisible: Flow<Boolean> = flowOf(false) - override val activityContainerVisible: Flow<Boolean> = flowOf(false) + override val roaming: KairosState<Boolean> = stateOf(false) + override val networkTypeIcon: KairosState<Icon.Resource?> = stateOf(null) + override val networkTypeBackground: KairosState<Icon.Resource?> = stateOf(null) + override val activityInVisible: KairosState<Boolean> = stateOf(false) + override val activityOutVisible: KairosState<Boolean> = stateOf(false) + override val activityContainerVisible: KairosState<Boolean> = stateOf(false) } /** Terrestrial (cellular) icon. */ -@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@ExperimentalKairosApi private class CellularIconViewModelKairos( override val subscriptionId: Int, - iconInteractor: MobileIconInteractor, + override val iconInteractor: MobileIconInteractorKairos, airplaneModeInteractor: AirplaneModeInteractor, constants: ConnectivityConstants, - scope: CoroutineScope, -) : MobileIconViewModelCommon, MobileIconViewModelCommonKairos { - override val isVisible: StateFlow<Boolean> = + flags: FeatureFlagsClassic, +) : MobileIconViewModelKairosCommon, KairosBuilder by kairosBuilder() { + + override val isVisible: KairosState<Boolean> = if (!constants.hasDataCapabilities) { - flowOf(false) - } else { + stateOf(false) + } else { + buildState { combine( - airplaneModeInteractor.isAirplaneMode, - iconInteractor.isAllowedDuringAirplaneMode, - iconInteractor.isForceHidden, - ) { isAirplaneMode, isAllowedDuringAirplaneMode, isForceHidden -> - if (isForceHidden) { - false - } else if (isAirplaneMode) { - isAllowedDuringAirplaneMode - } else { - true + airplaneModeInteractor.isAirplaneMode.toState(), + iconInteractor.isAllowedDuringAirplaneMode, + iconInteractor.isForceHidden, + ) { isAirplaneMode, isAllowedDuringAirplaneMode, isForceHidden -> + if (isForceHidden) { + false + } else if (isAirplaneMode) { + isAllowedDuringAirplaneMode + } else { + true + } + } + .also { + logDiffsForTable(it, iconInteractor.tableLogBuffer, columnName = "visible") } - } } - .distinctUntilChanged() - .logDiffsForTable( - iconInteractor.tableLogBuffer, - columnName = "visible", - initialValue = false, - ) - .stateIn(scope, SharingStarted.WhileSubscribed(), false) + } - override val icon: Flow<SignalIconModel> = iconInteractor.signalLevelIcon + override val icon: KairosState<SignalIconModel> + get() = iconInteractor.signalLevelIcon - override val contentDescription: Flow<MobileContentDescription?> = + override val contentDescription: KairosState<MobileContentDescription?> = combine(iconInteractor.signalLevelIcon, iconInteractor.networkName) { icon, nameModel -> - when (icon) { - is SignalIconModel.Cellular -> - MobileContentDescription.Cellular( - nameModel.name, - icon.levelDescriptionRes(), - ) - else -> null - } + when (icon) { + is SignalIconModel.Cellular -> + MobileContentDescription.Cellular(nameModel.name, icon.levelDescriptionRes()) + else -> null } - .stateIn(scope, SharingStarted.WhileSubscribed(), null) + } private fun SignalIconModel.Cellular.levelDescriptionRes() = when (level) { @@ -241,7 +227,7 @@ private class CellularIconViewModelKairos( else -> R.string.accessibility_no_signal } - private val showNetworkTypeIcon: Flow<Boolean> = + private val showNetworkTypeIcon: KairosState<Boolean> = combine( iconInteractor.isDataConnected, iconInteractor.isDataEnabled, @@ -252,77 +238,72 @@ private class CellularIconViewModelKairos( alwaysShow || (!carrierNetworkChange && (dataEnabled && dataConnected && mobileIsDefault)) } - .distinctUntilChanged() - .logDiffsForTable( - iconInteractor.tableLogBuffer, - columnName = "showNetworkTypeIcon", - initialValue = false, - ) - .stateIn(scope, SharingStarted.WhileSubscribed(), false) - - override val networkTypeIcon: Flow<Icon.Resource?> = - combine(iconInteractor.networkTypeIconGroup, showNetworkTypeIcon) { - networkTypeIconGroup, - shouldShow -> - val desc = - if (networkTypeIconGroup.contentDescription != 0) - ContentDescription.Resource(networkTypeIconGroup.contentDescription) - else null - val icon = - if (networkTypeIconGroup.iconId != 0) - Icon.Resource(networkTypeIconGroup.iconId, desc) - else null - return@combine when { - !shouldShow -> null - else -> icon + .also { + onActivated { + logDiffsForTable( + it, + iconInteractor.tableLogBuffer, + columnName = "showNetworkTypeIcon", + ) } } - .distinctUntilChanged() - .stateIn(scope, SharingStarted.WhileSubscribed(), null) - - override val networkTypeBackground = - iconInteractor.showSliceAttribution - .map { - when { - it && NewStatusBarIcons.isEnabled -> - Icon.Resource(R.drawable.mobile_network_type_background_updated, null) - it -> Icon.Resource(R.drawable.mobile_network_type_background, null) - else -> null + + override val networkTypeIcon: KairosState<Icon.Resource?> = + combine(iconInteractor.networkTypeIconGroup, showNetworkTypeIcon) { + networkTypeIconGroup, + shouldShow -> + val desc = + if (networkTypeIconGroup.contentDescription != 0) { + ContentDescription.Resource(networkTypeIconGroup.contentDescription) + } else { + null + } + val icon = + if (networkTypeIconGroup.iconId != 0) { + Icon.Resource(networkTypeIconGroup.iconId, desc) + } else { + null } + when { + !shouldShow -> null + else -> icon } - .stateIn(scope, SharingStarted.WhileSubscribed(), null) - - override val roaming: StateFlow<Boolean> = - iconInteractor.isRoaming - .logDiffsForTable( - iconInteractor.tableLogBuffer, - columnName = "roaming", - initialValue = false, - ) - .stateIn(scope, SharingStarted.WhileSubscribed(), false) - - private val activity: Flow<DataActivityModel?> = + } + + override val networkTypeBackground: KairosState<Icon.Resource?> = + iconInteractor.showSliceAttribution.map { + when { + it && NewStatusBarIcons.isEnabled -> + Icon.Resource(R.drawable.mobile_network_type_background_updated, null) + it -> Icon.Resource(R.drawable.mobile_network_type_background, null) + else -> null + } + } + + override val roaming: KairosState<Boolean> = + iconInteractor.isRoaming.also { + onActivated { + logDiffsForTable(it, iconInteractor.tableLogBuffer, columnName = "roaming") + } + } + + private val activity: KairosState<DataActivityModel?> = if (!constants.shouldShowActivityConfig) { - flowOf(null) + stateOf(null) } else { iconInteractor.activity } - override val activityInVisible: Flow<Boolean> = - activity - .map { it?.hasActivityIn ?: false } - .stateIn(scope, SharingStarted.WhileSubscribed(), false) + override val activityInVisible: KairosState<Boolean> = + activity.map { it?.hasActivityIn ?: false } - override val activityOutVisible: Flow<Boolean> = - activity - .map { it?.hasActivityOut ?: false } - .stateIn(scope, SharingStarted.WhileSubscribed(), false) + override val activityOutVisible: KairosState<Boolean> = + activity.map { it?.hasActivityOut ?: false } - override val activityContainerVisible: Flow<Boolean> = + override val activityContainerVisible: KairosState<Boolean> = if (statusBarStaticInoutIndicators()) { - flowOf(constants.shouldShowActivityConfig) - } else { - activity.map { it != null && (it.hasActivityIn || it.hasActivityOut) } - } - .stateIn(scope, SharingStarted.WhileSubscribed(), false) + stateOf(constants.shouldShowActivityConfig) + } else { + activity.map { it != null && (it.hasActivityIn || it.hasActivityOut) } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt index a65540738828..ada5500a6f3c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.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. @@ -16,137 +16,149 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel -import androidx.annotation.VisibleForTesting -import com.android.app.tracing.coroutines.launchTraced as launch -import com.android.systemui.coroutines.newTracingContext +import androidx.compose.runtime.State as ComposeState +import androidx.compose.runtime.mutableStateOf +import com.android.systemui.Flags +import com.android.systemui.KairosActivatable +import com.android.systemui.KairosBuilder +import com.android.systemui.activated import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.flags.FeatureFlagsClassic +import com.android.systemui.kairos.BuildScope +import com.android.systemui.kairos.BuildSpec +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kairos.Incremental +import com.android.systemui.kairos.State as KairosState +import com.android.systemui.kairos.State +import com.android.systemui.kairos.buildSpec +import com.android.systemui.kairos.changes +import com.android.systemui.kairos.combine +import com.android.systemui.kairos.flatten +import com.android.systemui.kairos.map +import com.android.systemui.kairos.mapValues +import com.android.systemui.kairos.stateOf +import com.android.systemui.kairosBuilder import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor -import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractorKairos +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractorKairos import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger import com.android.systemui.statusbar.pipeline.mobile.ui.VerboseMobileViewLogger import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants -import java.util.concurrent.ConcurrentHashMap +import dagger.Provides +import dagger.multibindings.ElementsIntoSet import javax.inject.Inject -import kotlin.coroutines.CoroutineContext -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.stateIn +import javax.inject.Provider /** * View model for describing the system's current mobile cellular connections. The result is a list * of [MobileIconViewModel]s which describe the individual icons and can be bound to * [ModernStatusBarMobileView]. */ +@ExperimentalKairosApi @SysUISingleton class MobileIconsViewModelKairos @Inject constructor( val logger: MobileViewLogger, private val verboseLogger: VerboseMobileViewLogger, - private val interactor: MobileIconsInteractor, + private val interactor: MobileIconsInteractorKairos, private val airplaneModeInteractor: AirplaneModeInteractor, private val constants: ConnectivityConstants, - @Background private val scope: CoroutineScope, -) { - @VisibleForTesting - val reuseCache = ConcurrentHashMap<Int, Pair<MobileIconViewModel, CoroutineScope>>() + private val flags: FeatureFlagsClassic, +) : KairosBuilder by kairosBuilder() { - val activeMobileDataSubscriptionId: StateFlow<Int?> = interactor.activeMobileDataSubscriptionId + val activeSubscriptionId: State<Int?> + get() = interactor.activeDataIconInteractor.map { it?.subscriptionId } - val subscriptionIdsFlow: StateFlow<List<Int>> = - interactor.filteredSubscriptions - .mapLatest { subscriptions -> - subscriptions.map { subscriptionModel -> subscriptionModel.subscriptionId } - } - .stateIn(scope, SharingStarted.WhileSubscribed(), listOf()) - - val mobileSubViewModels: StateFlow<List<MobileIconViewModelCommon>> = - subscriptionIdsFlow - .map { ids -> ids.map { commonViewModelForSub(it) } } - .stateIn(scope, SharingStarted.WhileSubscribed(), emptyList()) + val subscriptionIds: KairosState<List<Int>> = + interactor.filteredSubscriptions.map { subscriptions -> + subscriptions.map { it.subscriptionId } + } - private val firstMobileSubViewModel: StateFlow<MobileIconViewModelCommon?> = - mobileSubViewModels - .map { - if (it.isEmpty()) { - null - } else { - // Mobile icons get reversed by [StatusBarIconController], so the last element - // in this list will show up visually first. - it.last() - } - } - .stateIn(scope, SharingStarted.WhileSubscribed(), null) + val icons: Incremental<Int, MobileIconViewModelKairos> = buildIncremental { + interactor.icons + .mapValues { (subId, icon) -> buildSpec { commonViewModel(subId, icon) } } + .applyLatestSpecForKey() + } - /** - * A flow that emits `true` if the mobile sub that's displayed first visually is showing its - * network type icon and `false` otherwise. - */ - val firstMobileSubShowingNetworkTypeIcon: StateFlow<Boolean> = - firstMobileSubViewModel - .flatMapLatest { firstMobileSubViewModel -> - firstMobileSubViewModel?.networkTypeIcon?.map { it != null } ?: flowOf(false) + /** Whether the mobile sub that's displayed first visually is showing its network type icon. */ + val firstMobileSubShowingNetworkTypeIcon: KairosState<Boolean> = buildState { + combine(subscriptionIds.map { it.lastOrNull() }, icons) { lastId, icons -> + icons[lastId]?.networkTypeIcon?.map { it != null } ?: stateOf(false) } - .stateIn(scope, SharingStarted.WhileSubscribed(), false) - - val isStackable: StateFlow<Boolean> = interactor.isStackable - - init { - scope.launch { subscriptionIdsFlow.collect { invalidateCaches(it) } } + .flatten() } - fun viewModelForSub(subId: Int, location: StatusBarLocation): LocationBasedMobileViewModel { - val common = commonViewModelForSub(subId) - return LocationBasedMobileViewModel.viewModelForLocation( - common, - interactor.getMobileConnectionInteractorForSubId(subId), + val isStackable: KairosState<Boolean> + get() = interactor.isStackable + + fun viewModelForSub( + subId: Int, + location: StatusBarLocation, + ): BuildSpec<LocationBasedMobileViewModelKairos> = buildSpec { + val iconInteractor = + interactor.icons.sample().getOrElse(subId) { error("Unknown subscription id: $subId") } + val commonViewModel = + icons.sample().getOrElse(subId) { error("Unknown subscription id: $subId") } + LocationBasedMobileViewModelKairos.viewModelForLocation( + commonViewModel, + iconInteractor, verboseLogger, location, - scope, ) } - private fun commonViewModelForSub(subId: Int): MobileIconViewModelCommon { - return reuseCache.getOrPut(subId) { createViewModel(subId) }.first - } - - private fun createViewModel(subId: Int): Pair<MobileIconViewModel, CoroutineScope> { - // Create a child scope so we can cancel it - val vmScope = scope.createChildScope(newTracingContext("MobileIconViewModel")) - val vm = - MobileIconViewModel( - subId, - interactor.getMobileConnectionInteractorForSubId(subId), - airplaneModeInteractor, - constants, - vmScope, + fun shadeCarrierGroupIcon(subId: Int): BuildSpec<ShadeCarrierGroupMobileIconViewModelKairos> = + buildSpec { + val iconInteractor = + interactor.icons.sample().getOrElse(subId) { + error("Unknown subscription id: $subId") + } + val commonViewModel = + icons.sample().getOrElse(subId) { error("Unknown subscription id: $subId") } + ShadeCarrierGroupMobileIconViewModelKairos(commonViewModel, iconInteractor) + } + + private fun BuildScope.commonViewModel(subId: Int, iconInteractor: MobileIconInteractorKairos) = + activated { + MobileIconViewModelKairos( + subscriptionId = subId, + iconInteractor = iconInteractor, + airplaneModeInteractor = airplaneModeInteractor, + constants = constants, + flags = flags, ) - - return Pair(vm, vmScope) + } + + @dagger.Module + object Module { + @Provides + @ElementsIntoSet + fun bindKairosActivatable( + impl: Provider<MobileIconsViewModelKairos> + ): Set<@JvmSuppressWildcards KairosActivatable> = + if (Flags.statusBarMobileIconKairos()) setOf(impl.get()) else emptySet() } +} - private fun CoroutineScope.createChildScope(extraContext: CoroutineContext) = - CoroutineScope(coroutineContext + Job(coroutineContext[Job]) + extraContext) +@ExperimentalKairosApi +class MobileIconsViewModelKairosComposeWrapper( + val icons: ComposeState<Map<Int, MobileIconViewModelKairos>> +) - private fun invalidateCaches(subIds: List<Int>) { - reuseCache.keys - .filter { !subIds.contains(it) } - .forEach { id -> - reuseCache - .remove(id) - // Cancel the view model's scope after removing it - ?.second - ?.cancel() - } - } +@ExperimentalKairosApi +fun composeWrapper( + viewModel: MobileIconsViewModelKairos +): BuildSpec<MobileIconsViewModelKairosComposeWrapper> = buildSpec { + MobileIconsViewModelKairosComposeWrapper(icons = toComposeState(viewModel.icons)) +} + +@ExperimentalKairosApi +fun <T> BuildScope.toComposeState(state: KairosState<T>): ComposeState<T> { + val initial = state.sample() + val cState = mutableStateOf(initial) + state.changes.observe { cState.value = it } + return cState } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModel.kt index 2c85a5150575..060454c2b1c5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModel.kt @@ -22,6 +22,7 @@ import com.android.systemui.common.shared.model.Icon import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.StackedMobileIconViewModel.DualSim import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -30,10 +31,22 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf +interface StackedMobileIconViewModel { + val dualSim: DualSim? + val networkTypeIcon: Icon.Resource? + val isIconVisible: Boolean + + data class DualSim( + val primary: SignalIconModel.Cellular, + val secondary: SignalIconModel.Cellular, + ) +} + @OptIn(ExperimentalCoroutinesApi::class) -class StackedMobileIconViewModel +class StackedMobileIconViewModelImpl @AssistedInject -constructor(mobileIconsViewModel: MobileIconsViewModel) : ExclusiveActivatable() { +constructor(mobileIconsViewModel: MobileIconsViewModel) : + ExclusiveActivatable(), StackedMobileIconViewModel { private val hydrator = Hydrator("StackedMobileIconViewModel") private val isStackable: Boolean by @@ -52,7 +65,7 @@ constructor(mobileIconsViewModel: MobileIconsViewModel) : ExclusiveActivatable() viewModels.sortedByDescending { it.subscriptionId == activeSubId } } - val dualSim: DualSim? by + override val dualSim: DualSim? by hydrator.hydratedStateOf( traceName = "dualSim", source = @@ -68,7 +81,7 @@ constructor(mobileIconsViewModel: MobileIconsViewModel) : ExclusiveActivatable() initialValue = null, ) - val networkTypeIcon: Icon.Resource? by + override val networkTypeIcon: Icon.Resource? by hydrator.hydratedStateOf( traceName = "networkTypeIcon", source = @@ -78,7 +91,7 @@ constructor(mobileIconsViewModel: MobileIconsViewModel) : ExclusiveActivatable() initialValue = null, ) - val isIconVisible: Boolean by derivedStateOf { isStackable && dualSim != null } + override val isIconVisible: Boolean by derivedStateOf { isStackable && dualSim != null } override suspend fun onActivated(): Nothing { hydrator.activate() @@ -86,11 +99,6 @@ constructor(mobileIconsViewModel: MobileIconsViewModel) : ExclusiveActivatable() @AssistedFactory interface Factory { - fun create(): StackedMobileIconViewModel + fun create(): StackedMobileIconViewModelImpl } - - data class DualSim( - val primary: SignalIconModel.Cellular, - val secondary: SignalIconModel.Cellular, - ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairos.kt index 2dbb02c8f095..402fdf03941d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairos.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairos.kt @@ -16,81 +16,71 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import com.android.systemui.KairosBuilder import com.android.systemui.common.shared.model.Icon -import com.android.systemui.lifecycle.ExclusiveActivatable -import com.android.systemui.lifecycle.Hydrator +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kairos.State as KairosState +import com.android.systemui.kairos.combine +import com.android.systemui.kairos.flatMap +import com.android.systemui.kairos.stateOf +import com.android.systemui.kairosBuilder import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.StackedMobileIconViewModel.DualSim +import com.android.systemui.util.composable.kairos.hydratedComposeStateOf import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf -@OptIn(ExperimentalCoroutinesApi::class) +@OptIn(ExperimentalKairosApi::class) class StackedMobileIconViewModelKairos @AssistedInject -constructor(mobileIconsViewModel: MobileIconsViewModel) : ExclusiveActivatable() { - private val hydrator = Hydrator("StackedMobileIconViewModel") +constructor(mobileIcons: MobileIconsViewModelKairos) : + KairosBuilder by kairosBuilder(), StackedMobileIconViewModel { private val isStackable: Boolean by - hydrator.hydratedStateOf( - traceName = "isStackable", - source = mobileIconsViewModel.isStackable, - initialValue = false, - ) + hydratedComposeStateOf(mobileIcons.isStackable, initialValue = false) - private val iconViewModelFlow: Flow<List<MobileIconViewModelCommon>> = - combine( - mobileIconsViewModel.mobileSubViewModels, - mobileIconsViewModel.activeMobileDataSubscriptionId, - ) { viewModels, activeSubId -> - // Sort to get the active subscription first, if it's set - viewModels.sortedByDescending { it.subscriptionId == activeSubId } + private val iconList: KairosState<List<MobileIconViewModelKairos>> = + combine(mobileIcons.icons, mobileIcons.activeSubscriptionId) { iconsBySubId, activeSubId -> + buildList { + activeSubId?.let { iconsBySubId[activeSubId]?.let { add(it) } } + addAll(iconsBySubId.values.asSequence().filter { it.subscriptionId != activeSubId }) + } } - val dualSim: DualSim? by - hydrator.hydratedStateOf( - traceName = "dualSim", - source = - iconViewModelFlow.flatMapLatest { viewModels -> - combine(viewModels.map { it.icon }) { icons -> - icons - .toList() - .filterIsInstance<SignalIconModel.Cellular>() - .takeIf { it.size == 2 } - ?.let { DualSim(it[0], it[1]) } - } - }, + override val dualSim: DualSim? by + hydratedComposeStateOf( + iconList.flatMap { icons -> + icons.map { it.icon }.combine { signalIcons -> tryParseDualSim(signalIcons) } + }, initialValue = null, ) - val networkTypeIcon: Icon.Resource? by - hydrator.hydratedStateOf( - traceName = "networkTypeIcon", - source = - iconViewModelFlow.flatMapLatest { viewModels -> - viewModels.firstOrNull()?.networkTypeIcon ?: flowOf(null) - }, + override val networkTypeIcon: Icon.Resource? by + hydratedComposeStateOf( + iconList.flatMap { icons -> icons.firstOrNull()?.networkTypeIcon ?: stateOf(null) }, initialValue = null, ) - val isIconVisible: Boolean by derivedStateOf { isStackable && dualSim != null } + override val isIconVisible: Boolean + get() = isStackable && dualSim != null - override suspend fun onActivated(): Nothing { - hydrator.activate() + private fun tryParseDualSim(icons: List<SignalIconModel>): DualSim? { + var first: SignalIconModel.Cellular? = null + var second: SignalIconModel.Cellular? = null + for (icon in icons) { + when { + icon !is SignalIconModel.Cellular -> continue + first == null -> first = icon + second == null -> second = icon + else -> return null + } + } + return first?.let { second?.let { DualSim(first, second) } } } @AssistedFactory interface Factory { fun create(): StackedMobileIconViewModelKairos } - - data class DualSim( - val primary: SignalIconModel.Cellular, - val secondary: SignalIconModel.Cellular, - ) } diff --git a/packages/SystemUI/src/com/android/systemui/util/composable/kairos/HydratedComposeStateOf.kt b/packages/SystemUI/src/com/android/systemui/util/composable/kairos/HydratedComposeStateOf.kt new file mode 100644 index 000000000000..0d53a001f3f4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/composable/kairos/HydratedComposeStateOf.kt @@ -0,0 +1,31 @@ +/* + * 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.util.composable.kairos + +import androidx.compose.runtime.mutableStateOf +import com.android.systemui.KairosBuilder +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kairos.State + +@ExperimentalKairosApi +fun <T> KairosBuilder.hydratedComposeStateOf( + source: State<T>, + initialValue: T, +): androidx.compose.runtime.State<T> = + mutableStateOf(initialValue).also { state -> + onActivated { source.observe { state.value = it } } + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairosKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairosKosmos.kt new file mode 100644 index 000000000000..83b8283b1892 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairosKosmos.kt @@ -0,0 +1,38 @@ +/* + * 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.ui.viewmodel + +import com.android.systemui.flags.featureFlagsClassic +import com.android.systemui.kairos.ActivatedKairosFixture +import com.android.systemui.kairos.ExperimentalKairosApi +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.airplaneModeInteractor +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.mobileIconsInteractorKairos +import com.android.systemui.statusbar.pipeline.mobile.ui.mobileViewLogger +import com.android.systemui.util.mockito.mock + +@ExperimentalKairosApi +val Kosmos.mobileIconsViewModelKairos by ActivatedKairosFixture { + MobileIconsViewModelKairos( + logger = mobileViewLogger, + verboseLogger = mock(), + interactor = mobileIconsInteractorKairos, + airplaneModeInteractor = airplaneModeInteractor, + constants = mock(), + flags = featureFlagsClassic, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairosKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairosKosmos.kt index 3ee33802e9d5..ad42a89dc237 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairosKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairosKosmos.kt @@ -16,7 +16,11 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel +import com.android.systemui.kairos.ActivatedKairosFixture +import com.android.systemui.kairos.ExperimentalKairosApi import com.android.systemui.kosmos.Kosmos -val Kosmos.stackedMobileIconViewModelKairos by - Kosmos.Fixture { StackedMobileIconViewModelKairos(mobileIconsViewModel) } +@ExperimentalKairosApi +val Kosmos.stackedMobileIconViewModelKairos by ActivatedKairosFixture { + StackedMobileIconViewModelKairos(mobileIconsViewModelKairos) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKosmos.kt index 880ba5eee5d2..0a8e0a7d48b6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKosmos.kt @@ -18,5 +18,8 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel import com.android.systemui.kosmos.Kosmos -val Kosmos.stackedMobileIconViewModel: StackedMobileIconViewModel by - Kosmos.Fixture { StackedMobileIconViewModel(mobileIconsViewModel) } +var Kosmos.stackedMobileIconViewModel: StackedMobileIconViewModel by + Kosmos.Fixture { stackedMobileIconViewModelImpl } + +val Kosmos.stackedMobileIconViewModelImpl by + Kosmos.Fixture { StackedMobileIconViewModelImpl(mobileIconsViewModel) } |