[Sb refactor] Add location to mobile view models

Test: tests in tests/src/com/android/systemui/pipeline/mobile
Bug: 238425913

Change-Id: I8b06b0fba5b6a1467211a243311fc34091911f4a
This commit is contained in:
Evan Laird
2022-12-09 10:45:48 -05:00
parent 6acee7b1ee
commit 09ae54f571
9 changed files with 227 additions and 35 deletions

View File

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

View File

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

View File

@@ -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<View>(R.id.inout_container)
val activityIn = view.requireViewById<ImageView>(R.id.mobile_in)

View File

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

View File

@@ -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<Int>
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<Int> = flowOf(Color.CYAN)
}
class QsMobileIconViewModel(commonImpl: MobileIconViewModelCommon) :
MobileIconViewModelCommon, LocationBasedMobileViewModel(commonImpl) {
override val tint: Flow<Int> = flowOf(Color.GREEN)
}
class KeyguardMobileIconViewModel(commonImpl: MobileIconViewModelCommon) :
MobileIconViewModelCommon, LocationBasedMobileViewModel(commonImpl) {
override val tint: Flow<Int> = flowOf(Color.MAGENTA)
}

View File

@@ -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<Int>
val roaming: Flow<Boolean>
/** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */
val networkTypeIcon: Flow<Icon?>
val activityInVisible: Flow<Boolean>
val activityOutVisible: Flow<Boolean>
val activityContainerVisible: Flow<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
@@ -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<Boolean> =
iconInteractor.isDefaultDataEnabled.mapLatest { !it }
/** An int consumable by [SignalDrawable] for display */
val iconId: Flow<Int> =
override val iconId: Flow<Int> =
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<Icon?> =
override val networkTypeIcon: Flow<Icon?> =
combine(
iconInteractor.networkTypeIconGroup,
iconInteractor.isDataConnected,
@@ -91,7 +100,7 @@ constructor(
}
}
val roaming: Flow<Boolean> = iconInteractor.isRoaming
override val roaming: Flow<Boolean> = iconInteractor.isRoaming
private val activity: Flow<DataActivityModel?> =
if (!constants.shouldShowActivityConfig) {
@@ -100,9 +109,9 @@ constructor(
iconInteractor.activity
}
val activityInVisible: Flow<Boolean> = activity.map { it?.hasActivityIn ?: false }
val activityOutVisible: Flow<Boolean> = activity.map { it?.hasActivityOut ?: false }
val activityContainerVisible: Flow<Boolean> =
override val activityInVisible: Flow<Boolean> = activity.map { it?.hasActivityIn ?: false }
override val activityOutVisible: Flow<Boolean> = activity.map { it?.hasActivityOut ?: false }
override val activityContainerVisible: Flow<Boolean> =
activity.map { it != null && (it.hasActivityIn || it.hasActivityOut) }
val tint: Flow<Int> = flowOf(Color.CYAN)

View File

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

View File

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

View File

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