diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java index c7be2193e9b54..5569718f4a4a3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DemoStatusIcons.java @@ -60,10 +60,12 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da private int mColor; private final MobileIconsViewModel mMobileIconsViewModel; + private final StatusBarLocation mLocation; public DemoStatusIcons( LinearLayout statusIcons, MobileIconsViewModel mobileIconsViewModel, + StatusBarLocation location, int iconSize ) { super(statusIcons.getContext()); @@ -71,6 +73,7 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da mIconSize = iconSize; mColor = DarkIconDispatcher.DEFAULT_ICON_TINT; mMobileIconsViewModel = mobileIconsViewModel; + mLocation = location; if (statusIcons instanceof StatusIconContainer) { setShouldRestrictIcons(((StatusIconContainer) statusIcons).isRestrictingIcons()); @@ -287,7 +290,7 @@ public class DemoStatusIcons extends StatusIconContainer implements DemoMode, Da ModernStatusBarMobileView view = ModernStatusBarMobileView.constructAndBind( mobileContext, "mobile", - mMobileIconsViewModel.viewModelForSub(subId) + mMobileIconsViewModel.viewModelForSub(subId, mLocation) ); // mobile always goes at the end diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java index df3ab493a4dae..57cdc9eed5770 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java @@ -359,6 +359,7 @@ public interface StatusBarIconController { // Whether or not these icons show up in dumpsys protected boolean mShouldLog = false; private StatusBarIconController mController; + private final StatusBarLocation mLocation; // Enables SystemUI demo mode to take effect in this group protected boolean mDemoable = true; @@ -381,6 +382,7 @@ public interface StatusBarIconController { mContext = group.getContext(); mIconSize = mContext.getResources().getDimensionPixelSize( com.android.internal.R.dimen.status_bar_icon_size); + mLocation = location; if (statusBarPipelineFlags.runNewMobileIconsBackend()) { // This starts the flow for the new pipeline, and will notify us of changes if @@ -394,7 +396,7 @@ public interface StatusBarIconController { if (statusBarPipelineFlags.runNewWifiIconBackend()) { // This starts the flow for the new pipeline, and will notify us of changes if // {@link StatusBarPipelineFlags#useNewWifiIcon} is also true. - mWifiViewModel = wifiUiAdapter.bindGroup(mGroup, location); + mWifiViewModel = wifiUiAdapter.bindGroup(mGroup, mLocation); } else { mWifiViewModel = null; } @@ -569,7 +571,7 @@ public interface StatusBarIconController { .constructAndBind( mobileContext, slot, - mMobileIconsViewModel.viewModelForSub(subId) + mMobileIconsViewModel.viewModelForSub(subId, mLocation) ); } @@ -705,7 +707,12 @@ public interface StatusBarIconController { } protected DemoStatusIcons createDemoStatusIcons() { - return new DemoStatusIcons((LinearLayout) mGroup, mMobileIconsViewModel, mIconSize); + return new DemoStatusIcons( + (LinearLayout) mGroup, + mMobileIconsViewModel, + mLocation, + mIconSize + ); } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt index 545e624273f16..ab442b5ab4de3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt @@ -30,7 +30,7 @@ import com.android.settingslib.graph.SignalDrawable import com.android.systemui.R import com.android.systemui.common.ui.binder.IconViewBinder import com.android.systemui.lifecycle.repeatWhenAttached -import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch @@ -39,7 +39,7 @@ object MobileIconBinder { @JvmStatic fun bind( view: ViewGroup, - viewModel: MobileIconViewModel, + viewModel: LocationBasedMobileViewModel, ) { val activityContainer = view.requireViewById(R.id.inout_container) val activityIn = view.requireViewById(R.id.mobile_in) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt index 0ab7bcd968444..e86fee24fe4d5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileView.kt @@ -24,7 +24,7 @@ import com.android.systemui.R import com.android.systemui.statusbar.BaseStatusBarFrameLayout import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconBinder -import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel import java.util.ArrayList class ModernStatusBarMobileView( @@ -71,7 +71,7 @@ class ModernStatusBarMobileView( fun constructAndBind( context: Context, slot: String, - viewModel: MobileIconViewModel, + viewModel: LocationBasedMobileViewModel, ): ModernStatusBarMobileView { return (LayoutInflater.from(context) .inflate(R.layout.status_bar_mobile_signal_group_new, null) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt new file mode 100644 index 0000000000000..070634dedd2ff --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileViewModel.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel + +import android.graphics.Color +import com.android.systemui.statusbar.phone.StatusBarLocation +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +/** + * A view model for an individual mobile icon that embeds the notion of a [StatusBarLocation]. This + * allows the mobile icon to change some view parameters at different locations + * + * @param commonImpl for convenience, this class wraps a base interface that can provides all of the + * common implementations between locations. See [MobileIconViewModel] + */ +abstract class LocationBasedMobileViewModel( + val commonImpl: MobileIconViewModelCommon, +) : MobileIconViewModelCommon by commonImpl { + abstract val tint: Flow + + companion object { + fun viewModelForLocation( + commonImpl: MobileIconViewModelCommon, + loc: StatusBarLocation, + ): LocationBasedMobileViewModel = + when (loc) { + StatusBarLocation.HOME -> HomeMobileIconViewModel(commonImpl) + StatusBarLocation.KEYGUARD -> KeyguardMobileIconViewModel(commonImpl) + StatusBarLocation.QS -> QsMobileIconViewModel(commonImpl) + } + } +} + +class HomeMobileIconViewModel( + commonImpl: MobileIconViewModelCommon, +) : MobileIconViewModelCommon, LocationBasedMobileViewModel(commonImpl) { + override val tint: Flow = flowOf(Color.CYAN) +} + +class QsMobileIconViewModel(commonImpl: MobileIconViewModelCommon) : + MobileIconViewModelCommon, LocationBasedMobileViewModel(commonImpl) { + override val tint: Flow = flowOf(Color.GREEN) +} + +class KeyguardMobileIconViewModel(commonImpl: MobileIconViewModelCommon) : + MobileIconViewModelCommon, LocationBasedMobileViewModel(commonImpl) { + override val tint: Flow = flowOf(Color.MAGENTA) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt index 961283f57def3..226409f66e157 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt @@ -34,6 +34,19 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest +/** Common interface for all of the location-based mobile icon view models. */ +interface MobileIconViewModelCommon { + val subscriptionId: Int + /** An int consumable by [SignalDrawable] for display */ + val iconId: Flow + val roaming: Flow + /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */ + val networkTypeIcon: Flow + val activityInVisible: Flow + val activityOutVisible: Flow + val activityContainerVisible: Flow +} + /** * 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 @@ -41,24 +54,21 @@ import kotlinx.coroutines.flow.mapLatest * * There will be exactly one [MobileIconViewModel] per filtered subscription offered from * [MobileIconsInteractor.filteredSubscriptions] - * - * TODO: figure out where carrier merged and VCN models go (probably here?) */ @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @OptIn(ExperimentalCoroutinesApi::class) class MobileIconViewModel constructor( - val subscriptionId: Int, + override val subscriptionId: Int, iconInteractor: MobileIconInteractor, logger: ConnectivityPipelineLogger, constants: ConnectivityConstants, -) { +) : MobileIconViewModelCommon { /** Whether or not to show the error state of [SignalDrawable] */ private val showExclamationMark: Flow = iconInteractor.isDefaultDataEnabled.mapLatest { !it } - /** An int consumable by [SignalDrawable] for display */ - val iconId: Flow = + override val iconId: Flow = combine(iconInteractor.level, iconInteractor.numberOfLevels, showExclamationMark) { level, numberOfLevels, @@ -68,8 +78,7 @@ constructor( .distinctUntilChanged() .logOutputChange(logger, "iconId($subscriptionId)") - /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */ - val networkTypeIcon: Flow = + override val networkTypeIcon: Flow = combine( iconInteractor.networkTypeIconGroup, iconInteractor.isDataConnected, @@ -91,7 +100,7 @@ constructor( } } - val roaming: Flow = iconInteractor.isRoaming + override val roaming: Flow = iconInteractor.isRoaming private val activity: Flow = if (!constants.shouldShowActivityConfig) { @@ -100,9 +109,9 @@ constructor( iconInteractor.activity } - val activityInVisible: Flow = activity.map { it?.hasActivityIn ?: false } - val activityOutVisible: Flow = activity.map { it?.hasActivityOut ?: false } - val activityContainerVisible: Flow = + override val activityInVisible: Flow = activity.map { it?.hasActivityIn ?: false } + override val activityOutVisible: Flow = activity.map { it?.hasActivityOut ?: false } + override val activityContainerVisible: Flow = activity.map { it != null && (it.hasActivityIn || it.hasActivityOut) } val tint: Flow = flowOf(Color.CYAN) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt index 0b41d319f9dc2..3beb96aa8cf4a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel +import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants @@ -40,13 +41,17 @@ constructor( private val constants: ConnectivityConstants, ) { /** TODO: do we need to cache these? */ - fun viewModelForSub(subId: Int): MobileIconViewModel = - MobileIconViewModel( - subId, - interactor.createMobileConnectionInteractorForSubId(subId), - logger, - constants, - ) + fun viewModelForSub(subId: Int, location: StatusBarLocation): LocationBasedMobileViewModel { + val common = + MobileIconViewModel( + subId, + interactor.createMobileConnectionInteractorForSubId(subId), + logger, + constants, + ) + + return LocationBasedMobileViewModel.viewModelForLocation(common, location) + } class Factory @Inject diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt new file mode 100644 index 0000000000000..52232cbecf98b --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.settingslib.mobile.TelephonyIcons +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconInteractor +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModelTest.Companion.defaultSignal +import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants +import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger +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.mockito.Mock +import org.mockito.MockitoAnnotations + +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +class LocationBasedMobileIconViewModelTest : SysuiTestCase() { + private lateinit var commonImpl: MobileIconViewModelCommon + private lateinit var homeIcon: HomeMobileIconViewModel + private lateinit var qsIcon: QsMobileIconViewModel + private lateinit var keyguardIcon: KeyguardMobileIconViewModel + private val interactor = FakeMobileIconInteractor() + @Mock private lateinit var logger: ConnectivityPipelineLogger + @Mock private lateinit var constants: ConnectivityConstants + + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + interactor.apply { + setLevel(1) + setIsDefaultDataEnabled(true) + setIsFailedConnection(false) + setIconGroup(TelephonyIcons.THREE_G) + setIsEmergencyOnly(false) + setNumberOfLevels(4) + isDataConnected.value = true + } + commonImpl = MobileIconViewModel(SUB_1_ID, interactor, logger, constants) + + homeIcon = HomeMobileIconViewModel(commonImpl) + qsIcon = QsMobileIconViewModel(commonImpl) + keyguardIcon = KeyguardMobileIconViewModel(commonImpl) + } + + @Test + fun `location based view models receive same icon id when common impl updates`() = + testScope.runTest { + var latestHome: Int? = null + val homeJob = homeIcon.iconId.onEach { latestHome = it }.launchIn(this) + + var latestQs: Int? = null + val qsJob = qsIcon.iconId.onEach { latestQs = it }.launchIn(this) + + var latestKeyguard: Int? = null + val keyguardJob = keyguardIcon.iconId.onEach { latestKeyguard = it }.launchIn(this) + + var expected = defaultSignal(level = 1) + + assertThat(latestHome).isEqualTo(expected) + assertThat(latestQs).isEqualTo(expected) + assertThat(latestKeyguard).isEqualTo(expected) + + interactor.setLevel(2) + expected = defaultSignal(level = 2) + + assertThat(latestHome).isEqualTo(expected) + assertThat(latestQs).isEqualTo(expected) + assertThat(latestKeyguard).isEqualTo(expected) + + homeJob.cancel() + qsJob.cancel() + keyguardJob.cancel() + } + + companion object { + private const val SUB_1_ID = 1 + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt index 415ce75345b25..76437379e1a7c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt @@ -340,16 +340,16 @@ class MobileIconViewModelTest : SysuiTestCase() { containerJob.cancel() } - /** Convenience constructor for these tests */ - private fun defaultSignal( - level: Int = 1, - connected: Boolean = true, - ): Int { - return SignalDrawable.getState(level, /* numLevels */ 4, !connected) - } - companion object { private val IMMEDIATE = Dispatchers.Main.immediate private const val SUB_1_ID = 1 + + /** Convenience constructor for these tests */ + fun defaultSignal( + level: Int = 1, + connected: Boolean = true, + ): Int { + return SignalDrawable.getState(level, /* numLevels */ 4, !connected) + } } }