[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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user