[SB Refactor] Turn the wifi ViewModel into an @SysUISingleton and

instead create separate view models per location.

This CL eliminates the many duplicate logs by:
1) Making WifiViewModel a Singleton, so we only ever have one of each
   flow.
2) Adding 1 ViewModel class per location, which just references the
   singleton view model flows.
3) Making each flow inside WifiViewModel a StateFlow, so that its logic
   (including its logging logic) isn't duplicated each time we re-use
   one of the flows.

Bug: 238425913
Test: manual: Verified wifi icon is tinted different colors in each of
the 3 locations
Test: manual: Verify wifi icon still updates
Test: manual: Verify we don't get duplicate activity logs
Test: statusbar.pipeline tests

Change-Id: I6ab0245a83858875c4e63baf9bb6a8c482d1fe55
This commit is contained in:
Caitlin Shkuratov
2022-09-20 17:57:16 +00:00
parent 4513768e3b
commit e41d5cc763
21 changed files with 565 additions and 137 deletions

View File

@@ -30,6 +30,7 @@ import com.android.systemui.qs.carrier.QSCarrierGroupController;
import com.android.systemui.qs.dagger.QSScope;
import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider;
import com.android.systemui.statusbar.phone.StatusBarIconController;
import com.android.systemui.statusbar.phone.StatusBarLocation;
import com.android.systemui.statusbar.phone.StatusIconContainer;
import com.android.systemui.statusbar.policy.Clock;
import com.android.systemui.statusbar.policy.VariableDateViewController;
@@ -104,7 +105,7 @@ class QuickStatusBarHeaderController extends ViewController<QuickStatusBarHeader
mView.requireViewById(R.id.date_clock)
);
mIconManager = tintedIconManagerFactory.create(mIconContainer);
mIconManager = tintedIconManagerFactory.create(mIconContainer, StatusBarLocation.QS);
mDemoModeReceiver = new ClockDemoModeReceiver(mClockView);
mColorExtractor = colorExtractor;
mOnColorsChangedListener = (extractor, which) -> {

View File

@@ -48,6 +48,7 @@ import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.QQS
import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.QS_HEADER_CONSTRAINT
import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
import com.android.systemui.statusbar.phone.StatusBarIconController
import com.android.systemui.statusbar.phone.StatusBarLocation
import com.android.systemui.statusbar.phone.StatusIconContainer
import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent.CentralSurfacesScope
import com.android.systemui.statusbar.phone.dagger.StatusBarViewModule.LARGE_SCREEN_BATTERY_CONTROLLER
@@ -261,7 +262,7 @@ class LargeScreenShadeHeaderController @Inject constructor(
batteryMeterViewController.ignoreTunerUpdates()
batteryIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
iconManager = tintedIconManagerFactory.create(iconContainer)
iconManager = tintedIconManagerFactory.create(iconContainer, StatusBarLocation.QS)
iconManager.setTint(
Utils.getColorAttrDefaultColor(header.context, android.R.attr.textColorPrimary)
)

View File

@@ -352,8 +352,8 @@ public class KeyguardStatusBarViewController extends ViewController<KeyguardStat
mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback);
mDisableStateTracker.startTracking(mCommandQueue, mView.getDisplay().getDisplayId());
if (mTintedIconManager == null) {
mTintedIconManager =
mTintedIconManagerFactory.create(mView.findViewById(R.id.statusIcons));
mTintedIconManager = mTintedIconManagerFactory.create(
mView.findViewById(R.id.statusIcons), StatusBarLocation.KEYGUARD);
mTintedIconManager.setBlockList(getBlockedIcons());
mStatusBarIconController.addIconGroup(mTintedIconManager);
}

View File

@@ -56,7 +56,6 @@ import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Provider;
public interface StatusBarIconController {
@@ -139,13 +138,15 @@ public interface StatusBarIconController {
public DarkIconManager(
LinearLayout linearLayout,
StatusBarLocation location,
StatusBarPipelineFlags statusBarPipelineFlags,
Provider<WifiViewModel> wifiViewModelProvider,
WifiViewModel wifiViewModel,
MobileContextProvider mobileContextProvider,
DarkIconDispatcher darkIconDispatcher) {
super(linearLayout,
location,
statusBarPipelineFlags,
wifiViewModelProvider,
wifiViewModel,
mobileContextProvider);
mIconHPadding = mContext.getResources().getDimensionPixelSize(
R.dimen.status_bar_icon_padding);
@@ -204,27 +205,28 @@ public interface StatusBarIconController {
@SysUISingleton
public static class Factory {
private final StatusBarPipelineFlags mStatusBarPipelineFlags;
private final Provider<WifiViewModel> mWifiViewModelProvider;
private final WifiViewModel mWifiViewModel;
private final MobileContextProvider mMobileContextProvider;
private final DarkIconDispatcher mDarkIconDispatcher;
@Inject
public Factory(
StatusBarPipelineFlags statusBarPipelineFlags,
Provider<WifiViewModel> wifiViewModelProvider,
WifiViewModel wifiViewModel,
MobileContextProvider mobileContextProvider,
DarkIconDispatcher darkIconDispatcher) {
mStatusBarPipelineFlags = statusBarPipelineFlags;
mWifiViewModelProvider = wifiViewModelProvider;
mWifiViewModel = wifiViewModel;
mMobileContextProvider = mobileContextProvider;
mDarkIconDispatcher = darkIconDispatcher;
}
public DarkIconManager create(LinearLayout group) {
public DarkIconManager create(LinearLayout group, StatusBarLocation location) {
return new DarkIconManager(
group,
location,
mStatusBarPipelineFlags,
mWifiViewModelProvider,
mWifiViewModel,
mMobileContextProvider,
mDarkIconDispatcher);
}
@@ -239,12 +241,14 @@ public interface StatusBarIconController {
public TintedIconManager(
ViewGroup group,
StatusBarLocation location,
StatusBarPipelineFlags statusBarPipelineFlags,
Provider<WifiViewModel> wifiViewModelProvider,
WifiViewModel wifiViewModel,
MobileContextProvider mobileContextProvider) {
super(group,
location,
statusBarPipelineFlags,
wifiViewModelProvider,
wifiViewModel,
mobileContextProvider);
}
@@ -278,24 +282,25 @@ public interface StatusBarIconController {
@SysUISingleton
public static class Factory {
private final StatusBarPipelineFlags mStatusBarPipelineFlags;
private final Provider<WifiViewModel> mWifiViewModelProvider;
private final WifiViewModel mWifiViewModel;
private final MobileContextProvider mMobileContextProvider;
@Inject
public Factory(
StatusBarPipelineFlags statusBarPipelineFlags,
Provider<WifiViewModel> wifiViewModelProvider,
WifiViewModel wifiViewModel,
MobileContextProvider mobileContextProvider) {
mStatusBarPipelineFlags = statusBarPipelineFlags;
mWifiViewModelProvider = wifiViewModelProvider;
mWifiViewModel = wifiViewModel;
mMobileContextProvider = mobileContextProvider;
}
public TintedIconManager create(ViewGroup group) {
public TintedIconManager create(ViewGroup group, StatusBarLocation location) {
return new TintedIconManager(
group,
location,
mStatusBarPipelineFlags,
mWifiViewModelProvider,
mWifiViewModel,
mMobileContextProvider);
}
}
@@ -306,8 +311,9 @@ public interface StatusBarIconController {
*/
class IconManager implements DemoModeCommandReceiver {
protected final ViewGroup mGroup;
private final StatusBarLocation mLocation;
private final StatusBarPipelineFlags mStatusBarPipelineFlags;
private final Provider<WifiViewModel> mWifiViewModelProvider;
private final WifiViewModel mWifiViewModel;
private final MobileContextProvider mMobileContextProvider;
protected final Context mContext;
protected final int mIconSize;
@@ -324,12 +330,14 @@ public interface StatusBarIconController {
public IconManager(
ViewGroup group,
StatusBarLocation location,
StatusBarPipelineFlags statusBarPipelineFlags,
Provider<WifiViewModel> wifiViewModelProvider,
WifiViewModel wifiViewModel,
MobileContextProvider mobileContextProvider) {
mGroup = group;
mLocation = location;
mStatusBarPipelineFlags = statusBarPipelineFlags;
mWifiViewModelProvider = wifiViewModelProvider;
mWifiViewModel = wifiViewModel;
mMobileContextProvider = mobileContextProvider;
mContext = group.getContext();
mIconSize = mContext.getResources().getDimensionPixelSize(
@@ -446,7 +454,7 @@ public interface StatusBarIconController {
private ModernStatusBarWifiView onCreateModernStatusBarWifiView(String slot) {
return ModernStatusBarWifiView.constructAndBind(
mContext, slot, mWifiViewModelProvider.get());
mContext, slot, mWifiViewModel, mLocation);
}
private StatusBarMobileView onCreateStatusBarMobileView(int subId, String slot) {

View File

@@ -0,0 +1,27 @@
/*
* 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.phone
/** An enumeration of the different locations that host a status bar. */
enum class StatusBarLocation {
/** Home screen or in-app. */
HOME,
/** Keyguard (aka lockscreen). */
KEYGUARD,
/** Quick settings (inside the shade). */
QS,
}

View File

@@ -68,6 +68,7 @@ import com.android.systemui.statusbar.phone.PhoneStatusBarView;
import com.android.systemui.statusbar.phone.StatusBarHideIconsForBouncerManager;
import com.android.systemui.statusbar.phone.StatusBarIconController;
import com.android.systemui.statusbar.phone.StatusBarIconController.DarkIconManager;
import com.android.systemui.statusbar.phone.StatusBarLocation;
import com.android.systemui.statusbar.phone.StatusBarLocationPublisher;
import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent;
import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent.Startable;
@@ -250,7 +251,8 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue
mStatusBar.restoreHierarchyState(
savedInstanceState.getSparseParcelableArray(EXTRA_PANEL_STATE));
}
mDarkIconManager = mDarkIconManagerFactory.create(view.findViewById(R.id.statusIcons));
mDarkIconManager = mDarkIconManagerFactory.create(
view.findViewById(R.id.statusIcons), StatusBarLocation.HOME);
mDarkIconManager.setShouldLog(true);
updateBlockedIcons();
mStatusBarIconController.addIconGroup(mDarkIconManager);

View File

@@ -26,6 +26,8 @@ import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.R
import com.android.systemui.common.ui.binder.IconViewBinder
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.statusbar.phone.StatusBarLocation
import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel
import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.flow.collect
@@ -41,11 +43,29 @@ import kotlinx.coroutines.launch
*/
@OptIn(InternalCoroutinesApi::class)
object WifiViewBinder {
/** Binds the view to the view-model, continuing to update the former based on the latter. */
/**
* Binds the view to the appropriate view-model based on the given location. The view will
* continue to be updated following updates from the view-model.
*/
@JvmStatic
fun bind(
view: ViewGroup,
viewModel: WifiViewModel,
wifiViewModel: WifiViewModel,
location: StatusBarLocation,
) {
when (location) {
StatusBarLocation.HOME -> bind(view, wifiViewModel.home)
StatusBarLocation.KEYGUARD -> bind(view, wifiViewModel.keyguard)
StatusBarLocation.QS -> bind(view, wifiViewModel.qs)
}
}
/** Binds the view to the view-model, continuing to update the former based on the latter. */
@JvmStatic
private fun bind(
view: ViewGroup,
viewModel: LocationBasedWifiViewModel,
) {
val iconView = view.requireViewById<ImageView>(R.id.wifi_signal)
val activityInView = view.requireViewById<ImageView>(R.id.wifi_in)

View File

@@ -23,6 +23,7 @@ import android.view.LayoutInflater
import com.android.systemui.R
import com.android.systemui.statusbar.BaseStatusBarWifiView
import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON
import com.android.systemui.statusbar.phone.StatusBarLocation
import com.android.systemui.statusbar.pipeline.wifi.ui.binder.WifiViewBinder
import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel
@@ -72,21 +73,22 @@ class ModernStatusBarWifiView(
companion object {
/**
* Inflates a new instance of [ModernStatusBarWifiView], binds it to [viewModel], and
* Inflates a new instance of [ModernStatusBarWifiView], binds it to a view model, and
* returns it.
*/
@JvmStatic
fun constructAndBind(
context: Context,
slot: String,
viewModel: WifiViewModel,
wifiViewModel: WifiViewModel,
location: StatusBarLocation,
): ModernStatusBarWifiView {
return (
LayoutInflater.from(context).inflate(R.layout.new_status_bar_wifi_group, null)
as ModernStatusBarWifiView
).also {
it.setSlot(slot)
WifiViewBinder.bind(it, viewModel)
WifiViewBinder.bind(it, wifiViewModel, location)
}
}
}

View File

@@ -0,0 +1,42 @@
/*
* 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.wifi.ui.viewmodel
import android.graphics.Color
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
import kotlinx.coroutines.flow.Flow
/**
* A view model for the wifi icon shown on the "home" page (aka, when the device is unlocked and not
* showing the shade, so the user is on the home-screen, or in an app).
*/
class HomeWifiViewModel(
statusBarPipelineFlags: StatusBarPipelineFlags,
wifiIcon: Flow<Icon?>,
isActivityInViewVisible: Flow<Boolean>,
isActivityOutViewVisible: Flow<Boolean>,
isActivityContainerVisible: Flow<Boolean>,
) :
LocationBasedWifiViewModel(
statusBarPipelineFlags,
debugTint = Color.CYAN,
wifiIcon,
isActivityInViewVisible,
isActivityOutViewVisible,
isActivityContainerVisible,
)

View File

@@ -0,0 +1,39 @@
/*
* 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.wifi.ui.viewmodel
import android.graphics.Color
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
import kotlinx.coroutines.flow.Flow
/** A view model for the wifi icon shown on keyguard (lockscreen). */
class KeyguardWifiViewModel(
statusBarPipelineFlags: StatusBarPipelineFlags,
wifiIcon: Flow<Icon?>,
isActivityInViewVisible: Flow<Boolean>,
isActivityOutViewVisible: Flow<Boolean>,
isActivityContainerVisible: Flow<Boolean>,
) :
LocationBasedWifiViewModel(
statusBarPipelineFlags,
debugTint = Color.MAGENTA,
wifiIcon,
isActivityInViewVisible,
isActivityOutViewVisible,
isActivityContainerVisible,
)

View File

@@ -0,0 +1,67 @@
/*
* 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.wifi.ui.viewmodel
import android.graphics.Color
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
/**
* A view model for a wifi icon in a specific location. This allows us to control parameters that
* are location-specific (for example, different tints of the icon in different locations).
*
* Must be subclassed for each distinct location.
*/
abstract class LocationBasedWifiViewModel(
statusBarPipelineFlags: StatusBarPipelineFlags,
debugTint: Int,
/** The wifi icon that should be displayed. Null if we shouldn't display any icon. */
val wifiIcon: Flow<Icon?>,
/** True if the activity in view should be visible. */
val isActivityInViewVisible: Flow<Boolean>,
/** True if the activity out view should be visible. */
val isActivityOutViewVisible: Flow<Boolean>,
/** True if the activity container view should be visible. */
val isActivityContainerVisible: Flow<Boolean>,
) {
/** The color that should be used to tint the icon. */
val tint: Flow<Int> =
flowOf(
if (statusBarPipelineFlags.useNewPipelineDebugColoring()) {
debugTint
} else {
DEFAULT_TINT
}
)
companion object {
/**
* A default icon tint.
*
* TODO(b/238425913): The tint is actually controlled by
* [com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager]. We
* should use that logic instead of white as a default.
*/
private const val DEFAULT_TINT = Color.WHITE
}
}

View File

@@ -0,0 +1,39 @@
/*
* 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.wifi.ui.viewmodel
import android.graphics.Color
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
import kotlinx.coroutines.flow.Flow
/** A view model for the wifi icon shown in quick settings (when the shade is pulled down). */
class QsWifiViewModel(
statusBarPipelineFlags: StatusBarPipelineFlags,
wifiIcon: Flow<Icon?>,
isActivityInViewVisible: Flow<Boolean>,
isActivityOutViewVisible: Flow<Boolean>,
isActivityContainerVisible: Flow<Boolean>,
) :
LocationBasedWifiViewModel(
statusBarPipelineFlags,
debugTint = Color.GREEN,
wifiIcon,
isActivityInViewVisible,
isActivityOutViewVisible,
isActivityContainerVisible,
)

View File

@@ -17,7 +17,6 @@
package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel
import android.content.Context
import android.graphics.Color
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.annotation.VisibleForTesting
@@ -26,6 +25,8 @@ import com.android.settingslib.AccessibilityContentDescriptions.WIFI_NO_CONNECTI
import com.android.systemui.R
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS
import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_INTERNET_ICONS
import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_NETWORK
@@ -37,68 +38,90 @@ import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiIntera
import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants
import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
/**
* Models the UI state for the status bar wifi icon.
*
* This class exposes three view models, one per status bar location:
* - [home]
* - [keyguard]
* - [qs]
* In order to get the UI state for the wifi icon, you must use one of those view models (whichever
* is correct for your location).
*
* Internally, this class maintains the current state of the wifi icon and notifies those three
* view models of any changes.
*/
class WifiViewModel @Inject constructor(
@SysUISingleton
class WifiViewModel
@Inject
constructor(
constants: WifiConstants,
private val context: Context,
logger: ConnectivityPipelineLogger,
interactor: WifiInteractor,
@Application private val scope: CoroutineScope,
statusBarPipelineFlags: StatusBarPipelineFlags,
) {
/**
* The drawable resource ID to use for the wifi icon. Null if we shouldn't display any icon.
*/
/** The drawable resource ID to use for the wifi icon. Null if we shouldn't display any icon. */
@DrawableRes
private val iconResId: Flow<Int?> = interactor.wifiNetwork.map {
when (it) {
is WifiNetworkModel.CarrierMerged -> null
is WifiNetworkModel.Inactive -> WIFI_NO_NETWORK
is WifiNetworkModel.Active ->
when {
it.level == null -> null
it.isValidated -> WIFI_FULL_ICONS[it.level]
else -> WIFI_NO_INTERNET_ICONS[it.level]
private val iconResId: Flow<Int?> =
interactor.wifiNetwork
.map {
when (it) {
is WifiNetworkModel.CarrierMerged -> null
is WifiNetworkModel.Inactive -> WIFI_NO_NETWORK
is WifiNetworkModel.Active ->
when {
it.level == null -> null
it.isValidated -> WIFI_FULL_ICONS[it.level]
else -> WIFI_NO_INTERNET_ICONS[it.level]
}
}
}
}
}
.stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = null)
/** The content description for the wifi icon. */
private val contentDescription: Flow<ContentDescription?> = interactor.wifiNetwork.map {
when (it) {
is WifiNetworkModel.CarrierMerged -> null
is WifiNetworkModel.Inactive ->
ContentDescription.Loaded(
"${context.getString(WIFI_NO_CONNECTION)},${context.getString(NO_INTERNET)}"
)
is WifiNetworkModel.Active ->
when (it.level) {
null -> null
else -> {
val levelDesc = context.getString(WIFI_CONNECTION_STRENGTH[it.level])
when {
it.isValidated -> ContentDescription.Loaded(levelDesc)
else -> ContentDescription.Loaded(
"$levelDesc,${context.getString(NO_INTERNET)}"
)
private val contentDescription: Flow<ContentDescription?> =
interactor.wifiNetwork
.map {
when (it) {
is WifiNetworkModel.CarrierMerged -> null
is WifiNetworkModel.Inactive ->
ContentDescription.Loaded(
"${context.getString(WIFI_NO_CONNECTION)}," +
context.getString(NO_INTERNET)
)
is WifiNetworkModel.Active ->
when (it.level) {
null -> null
else -> {
val levelDesc =
context.getString(WIFI_CONNECTION_STRENGTH[it.level])
when {
it.isValidated -> ContentDescription.Loaded(levelDesc)
else ->
ContentDescription.Loaded(
"$levelDesc,${context.getString(NO_INTERNET)}"
)
}
}
}
}
}
}
}
}
.stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = null)
/**
* The wifi icon that should be displayed. Null if we shouldn't display any icon.
*/
val wifiIcon: Flow<Icon?> = combine(
/** The wifi icon that should be displayed. Null if we shouldn't display any icon. */
private val wifiIcon: Flow<Icon?> =
combine(
interactor.isForceHidden,
iconResId,
contentDescription,
@@ -110,6 +133,7 @@ class WifiViewModel @Inject constructor(
else -> Icon.Resource(iconResId, contentDescription)
}
}
.stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = null)
/** The wifi activity status. Null if we shouldn't display the activity status. */
private val activity: Flow<WifiActivityModel?> =
@@ -125,29 +149,53 @@ class WifiViewModel @Inject constructor(
}
.distinctUntilChanged()
.logOutputChange(logger, "activity")
.stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = null)
/** True if the activity in view should be visible. */
val isActivityInViewVisible: Flow<Boolean> = activity.map { it?.hasActivityIn == true }
private val isActivityInViewVisible: Flow<Boolean> =
activity
.map { it?.hasActivityIn == true }
.stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false)
/** True if the activity out view should be visible. */
val isActivityOutViewVisible: Flow<Boolean> = activity.map { it?.hasActivityOut == true }
private val isActivityOutViewVisible: Flow<Boolean> =
activity
.map { it?.hasActivityOut == true }
.stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false)
/** True if the activity container view should be visible. */
val isActivityContainerVisible: Flow<Boolean> =
combine(isActivityInViewVisible, isActivityOutViewVisible) { activityIn, activityOut ->
activityIn || activityOut
}
private val isActivityContainerVisible: Flow<Boolean> =
combine(isActivityInViewVisible, isActivityOutViewVisible) { activityIn, activityOut ->
activityIn || activityOut
}
.stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false)
// TODO(b/238425913): Update this class to use state flows instead. Right now, we have a ton of
// duplicate activity logs because the cold flows are getting duplicated for the three
// activityVisible flows.
/** A view model for the status bar on the home screen. */
val home: HomeWifiViewModel =
HomeWifiViewModel(
statusBarPipelineFlags,
wifiIcon,
isActivityInViewVisible,
isActivityOutViewVisible,
isActivityContainerVisible,
)
/** The tint that should be applied to the icon. */
val tint: Flow<Int> = if (!statusBarPipelineFlags.useNewPipelineDebugColoring()) {
emptyFlow()
} else {
flowOf(Color.CYAN)
}
/** A view model for the status bar on keyguard. */
val keyguard: KeyguardWifiViewModel =
KeyguardWifiViewModel(
statusBarPipelineFlags,
wifiIcon,
isActivityInViewVisible,
isActivityOutViewVisible,
isActivityContainerVisible,
)
/** A view model for the status bar in quick settings. */
val qs: QsWifiViewModel =
QsWifiViewModel(
statusBarPipelineFlags,
wifiIcon,
isActivityInViewVisible,
isActivityOutViewVisible,
isActivityContainerVisible,
)
companion object {
@StringRes

View File

@@ -110,7 +110,7 @@ class QuickStatusBarHeaderControllerTest : SysuiTestCase() {
`when`(qsCarrierGroupControllerBuilder.build()).thenReturn(qsCarrierGroupController)
`when`(variableDateViewControllerFactory.create(any()))
.thenReturn(variableDateViewController)
`when`(iconManagerFactory.create(any())).thenReturn(iconManager)
`when`(iconManagerFactory.create(any(), any())).thenReturn(iconManager)
`when`(view.resources).thenReturn(mContext.resources)
`when`(view.isAttachedToWindow).thenReturn(true)
`when`(view.context).thenReturn(context)

View File

@@ -176,7 +176,7 @@ class LargeScreenShadeHeaderControllerCombinedTest : SysuiTestCase() {
}
whenever(view.visibility).thenAnswer { _ -> viewVisibility }
whenever(iconManagerFactory.create(any())).thenReturn(iconManager)
whenever(iconManagerFactory.create(any(), any())).thenReturn(iconManager)
whenever(featureFlags.isEnabled(Flags.COMBINED_QS_HEADERS)).thenReturn(true)
whenever(featureFlags.isEnabled(Flags.NEW_HEADER)).thenReturn(true)

View File

@@ -97,7 +97,7 @@ class LargeScreenShadeHeaderControllerTest : SysuiTestCase() {
whenever(view.visibility).thenAnswer { _ -> viewVisibility }
whenever(variableDateViewControllerFactory.create(any()))
.thenReturn(variableDateViewController)
whenever(iconManagerFactory.create(any())).thenReturn(iconManager)
whenever(iconManagerFactory.create(any(), any())).thenReturn(iconManager)
whenever(featureFlags.isEnabled(Flags.COMBINED_QS_HEADERS)).thenReturn(false)
mLargeScreenShadeHeaderController = LargeScreenShadeHeaderController(
view,

View File

@@ -135,7 +135,7 @@ public class KeyguardStatusBarViewControllerTest extends SysuiTestCase {
MockitoAnnotations.initMocks(this);
when(mIconManagerFactory.create(any())).thenReturn(mIconManager);
when(mIconManagerFactory.create(any(), any())).thenReturn(mIconManager);
allowTestableLooperAsMainThread();
TestableLooper.get(this).runWithLooper(() -> {

View File

@@ -51,8 +51,6 @@ import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import javax.inject.Provider;
@RunWith(AndroidTestingRunner.class)
@RunWithLooper
@SmallTest
@@ -79,8 +77,9 @@ public class StatusBarIconControllerTest extends LeakCheckedTest {
LinearLayout layout = new LinearLayout(mContext);
TestDarkIconManager manager = new TestDarkIconManager(
layout,
StatusBarLocation.HOME,
mock(StatusBarPipelineFlags.class),
() -> mock(WifiViewModel.class),
mock(WifiViewModel.class),
mMobileContextProvider,
mock(DarkIconDispatcher.class));
testCallOnAdd_forManager(manager);
@@ -121,13 +120,15 @@ public class StatusBarIconControllerTest extends LeakCheckedTest {
TestDarkIconManager(
LinearLayout group,
StatusBarLocation location,
StatusBarPipelineFlags statusBarPipelineFlags,
Provider<WifiViewModel> wifiViewModelProvider,
WifiViewModel wifiViewModel,
MobileContextProvider contextProvider,
DarkIconDispatcher darkIconDispatcher) {
super(group,
location,
statusBarPipelineFlags,
wifiViewModelProvider,
wifiViewModel,
contextProvider,
darkIconDispatcher);
}
@@ -165,8 +166,9 @@ public class StatusBarIconControllerTest extends LeakCheckedTest {
private static class TestIconManager extends IconManager implements TestableIconManager {
TestIconManager(ViewGroup group, MobileContextProvider contextProvider) {
super(group,
StatusBarLocation.HOME,
mock(StatusBarPipelineFlags.class),
() -> mock(WifiViewModel.class),
mock(WifiViewModel.class),
contextProvider);
}

View File

@@ -431,7 +431,7 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest {
mOperatorNameViewControllerFactory = mock(OperatorNameViewController.Factory.class);
when(mOperatorNameViewControllerFactory.create(any()))
.thenReturn(mOperatorNameViewController);
when(mIconManagerFactory.create(any())).thenReturn(mIconManager);
when(mIconManagerFactory.create(any(), any())).thenReturn(mIconManager);
mSecureSettings = mock(SecureSettings.class);
setUpNotificationIconAreaController();

View File

@@ -20,6 +20,7 @@ import android.testing.TestableLooper.RunWithLooper
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.lifecycle.InstantTaskExecutorRule
import com.android.systemui.statusbar.phone.StatusBarLocation
import com.android.systemui.util.Assert
import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
@@ -45,7 +46,7 @@ class ModernStatusBarWifiViewTest : SysuiTestCase() {
@Test
fun constructAndBind_hasCorrectSlot() {
val view = ModernStatusBarWifiView.constructAndBind(
context, "slotName", mock()
context, "slotName", mock(), StatusBarLocation.HOME
)
assertThat(view.slot).isEqualTo("slotName")

View File

@@ -36,12 +36,15 @@ import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants
import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel
import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel.Companion.NO_INTERNET
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.yield
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
@@ -60,6 +63,7 @@ class WifiViewModelTest : SysuiTestCase() {
private lateinit var connectivityRepository: FakeConnectivityRepository
private lateinit var wifiRepository: FakeWifiRepository
private lateinit var interactor: WifiInteractor
private lateinit var scope: CoroutineScope
@Before
fun setUp() {
@@ -67,20 +71,35 @@ class WifiViewModelTest : SysuiTestCase() {
connectivityRepository = FakeConnectivityRepository()
wifiRepository = FakeWifiRepository()
interactor = WifiInteractor(connectivityRepository, wifiRepository)
scope = CoroutineScope(IMMEDIATE)
createAndSetViewModel()
}
@After
fun tearDown() {
scope.cancel()
}
// Note on testing: [WifiViewModel] exposes 3 different instances of
// [LocationBasedWifiViewModel]. In practice, these 3 different instances will get the exact
// same data for icon, activity, etc. flows. So, most of these tests will test just one of the
// instances. There are also some tests that verify all 3 instances received the same data.
@Test
fun wifiIcon_forceHidden_outputsNull() = runBlocking(IMMEDIATE) {
connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.WIFI))
wifiRepository.setWifiNetwork(WifiNetworkModel.Active(NETWORK_ID, level = 2))
var latest: Icon? = null
// Start as non-null so we can verify we got the update
var latest: Icon? = Icon.Resource(0, null)
val job = underTest
.home
.wifiIcon
.onEach { latest = it }
.launchIn(this)
wifiRepository.setWifiNetwork(WifiNetworkModel.Active(NETWORK_ID, level = 2))
yield()
assertThat(latest).isNull()
job.cancel()
@@ -89,14 +108,17 @@ class WifiViewModelTest : SysuiTestCase() {
@Test
fun wifiIcon_notForceHidden_outputsVisible() = runBlocking(IMMEDIATE) {
connectivityRepository.setForceHiddenIcons(setOf())
wifiRepository.setWifiNetwork(WifiNetworkModel.Active(NETWORK_ID, level = 2))
var latest: Icon? = null
val job = underTest
.home
.wifiIcon
.onEach { latest = it }
.launchIn(this)
wifiRepository.setWifiNetwork(WifiNetworkModel.Active(NETWORK_ID, level = 2))
yield()
assertThat(latest).isInstanceOf(Icon.Resource::class.java)
job.cancel()
@@ -104,13 +126,15 @@ class WifiViewModelTest : SysuiTestCase() {
@Test
fun wifiIcon_inactiveNetwork_outputsNoNetworkIcon() = runBlocking(IMMEDIATE) {
wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive)
var latest: Icon? = null
val job = underTest
.wifiIcon
.onEach { latest = it }
.launchIn(this)
.home
.wifiIcon
.onEach { latest = it }
.launchIn(this)
wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive)
yield()
assertThat(latest).isInstanceOf(Icon.Resource::class.java)
val icon = latest as Icon.Resource
@@ -125,14 +149,16 @@ class WifiViewModelTest : SysuiTestCase() {
@Test
fun wifiIcon_carrierMergedNetwork_outputsNull() = runBlocking(IMMEDIATE) {
wifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged)
var latest: Icon? = null
var latest: Icon? = Icon.Resource(0, null)
val job = underTest
.home
.wifiIcon
.onEach { latest = it }
.launchIn(this)
wifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged)
yield()
assertThat(latest).isNull()
job.cancel()
@@ -140,14 +166,16 @@ class WifiViewModelTest : SysuiTestCase() {
@Test
fun wifiIcon_isActiveNullLevel_outputsNull() = runBlocking(IMMEDIATE) {
wifiRepository.setWifiNetwork(WifiNetworkModel.Active(NETWORK_ID, level = null))
var latest: Icon? = null
var latest: Icon? = Icon.Resource(0, null)
val job = underTest
.home
.wifiIcon
.onEach { latest = it }
.launchIn(this)
wifiRepository.setWifiNetwork(WifiNetworkModel.Active(NETWORK_ID, level = null))
yield()
assertThat(latest).isNull()
job.cancel()
@@ -155,22 +183,23 @@ class WifiViewModelTest : SysuiTestCase() {
@Test
fun wifiIcon_isActiveAndValidated_level1_outputsFull1Icon() = runBlocking(IMMEDIATE) {
val level = 1
wifiRepository.setWifiNetwork(
WifiNetworkModel.Active(
NETWORK_ID,
isValidated = true,
level = level
)
)
var latest: Icon? = null
val job = underTest
.home
.wifiIcon
.onEach { latest = it }
.launchIn(this)
val level = 1
wifiRepository.setWifiNetwork(
WifiNetworkModel.Active(
NETWORK_ID,
isValidated = true,
level,
)
)
yield()
assertThat(latest).isInstanceOf(Icon.Resource::class.java)
val icon = latest as Icon.Resource
assertThat(icon.res).isEqualTo(WIFI_FULL_ICONS[level])
@@ -184,22 +213,23 @@ class WifiViewModelTest : SysuiTestCase() {
@Test
fun wifiIcon_isActiveAndNotValidated_level4_outputsEmpty4Icon() = runBlocking(IMMEDIATE) {
val level = 4
wifiRepository.setWifiNetwork(
WifiNetworkModel.Active(
NETWORK_ID,
isValidated = false,
level = level
)
)
var latest: Icon? = null
val job = underTest
.home
.wifiIcon
.onEach { latest = it }
.launchIn(this)
val level = 4
wifiRepository.setWifiNetwork(
WifiNetworkModel.Active(
NETWORK_ID,
isValidated = false,
level,
)
)
yield()
assertThat(latest).isInstanceOf(Icon.Resource::class.java)
val icon = latest as Icon.Resource
assertThat(icon.res).isEqualTo(WIFI_NO_INTERNET_ICONS[level])
@@ -211,6 +241,47 @@ class WifiViewModelTest : SysuiTestCase() {
job.cancel()
}
@Test
fun wifiIcon_allLocationViewModelsReceiveSameData() = runBlocking(IMMEDIATE) {
var latestHome: Icon? = null
val jobHome = underTest
.home
.wifiIcon
.onEach { latestHome = it }
.launchIn(this)
var latestKeyguard: Icon? = null
val jobKeyguard = underTest
.keyguard
.wifiIcon
.onEach { latestKeyguard = it }
.launchIn(this)
var latestQs: Icon? = null
val jobQs = underTest
.qs
.wifiIcon
.onEach { latestQs = it }
.launchIn(this)
wifiRepository.setWifiNetwork(
WifiNetworkModel.Active(
NETWORK_ID,
isValidated = true,
level = 1
)
)
yield()
assertThat(latestHome).isInstanceOf(Icon.Resource::class.java)
assertThat(latestHome).isEqualTo(latestKeyguard)
assertThat(latestKeyguard).isEqualTo(latestQs)
jobHome.cancel()
jobKeyguard.cancel()
jobQs.cancel()
}
@Test
fun activity_showActivityConfigFalse_outputsFalse() = runBlocking(IMMEDIATE) {
whenever(constants.shouldShowActivityConfig).thenReturn(false)
@@ -219,18 +290,21 @@ class WifiViewModelTest : SysuiTestCase() {
var activityIn: Boolean? = null
val activityInJob = underTest
.isActivityInViewVisible
.onEach { activityIn = it }
.launchIn(this)
.home
.isActivityInViewVisible
.onEach { activityIn = it }
.launchIn(this)
var activityOut: Boolean? = null
val activityOutJob = underTest
.home
.isActivityOutViewVisible
.onEach { activityOut = it }
.launchIn(this)
var activityContainer: Boolean? = null
val activityContainerJob = underTest
.home
.isActivityContainerVisible
.onEach { activityContainer = it }
.launchIn(this)
@@ -253,18 +327,21 @@ class WifiViewModelTest : SysuiTestCase() {
var activityIn: Boolean? = null
val activityInJob = underTest
.home
.isActivityInViewVisible
.onEach { activityIn = it }
.launchIn(this)
var activityOut: Boolean? = null
val activityOutJob = underTest
.home
.isActivityOutViewVisible
.onEach { activityOut = it }
.launchIn(this)
var activityContainer: Boolean? = null
val activityContainerJob = underTest
.home
.isActivityContainerVisible
.onEach { activityContainer = it }
.launchIn(this)
@@ -293,18 +370,21 @@ class WifiViewModelTest : SysuiTestCase() {
var activityIn: Boolean? = null
val activityInJob = underTest
.home
.isActivityInViewVisible
.onEach { activityIn = it }
.launchIn(this)
var activityOut: Boolean? = null
val activityOutJob = underTest
.home
.isActivityOutViewVisible
.onEach { activityOut = it }
.launchIn(this)
var activityContainer: Boolean? = null
val activityContainerJob = underTest
.home
.isActivityContainerVisible
.onEach { activityContainer = it }
.launchIn(this)
@@ -324,6 +404,46 @@ class WifiViewModelTest : SysuiTestCase() {
activityContainerJob.cancel()
}
@Test
fun activity_allLocationViewModelsReceiveSameData() = runBlocking(IMMEDIATE) {
whenever(constants.shouldShowActivityConfig).thenReturn(true)
createAndSetViewModel()
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
var latestHome: Boolean? = null
val jobHome = underTest
.home
.isActivityInViewVisible
.onEach { latestHome = it }
.launchIn(this)
var latestKeyguard: Boolean? = null
val jobKeyguard = underTest
.keyguard
.isActivityInViewVisible
.onEach { latestKeyguard = it }
.launchIn(this)
var latestQs: Boolean? = null
val jobQs = underTest
.qs
.isActivityInViewVisible
.onEach { latestQs = it }
.launchIn(this)
val activity = WifiActivityModel(hasActivityIn = true, hasActivityOut = true)
wifiRepository.setWifiActivity(activity)
yield()
assertThat(latestHome).isTrue()
assertThat(latestKeyguard).isTrue()
assertThat(latestQs).isTrue()
jobHome.cancel()
jobKeyguard.cancel()
jobQs.cancel()
}
@Test
fun activityIn_hasActivityInTrue_outputsTrue() = runBlocking(IMMEDIATE) {
whenever(constants.shouldShowActivityConfig).thenReturn(true)
@@ -332,6 +452,7 @@ class WifiViewModelTest : SysuiTestCase() {
var latest: Boolean? = null
val job = underTest
.home
.isActivityInViewVisible
.onEach { latest = it }
.launchIn(this)
@@ -353,6 +474,7 @@ class WifiViewModelTest : SysuiTestCase() {
var latest: Boolean? = null
val job = underTest
.home
.isActivityInViewVisible
.onEach { latest = it }
.launchIn(this)
@@ -374,6 +496,7 @@ class WifiViewModelTest : SysuiTestCase() {
var latest: Boolean? = null
val job = underTest
.home
.isActivityOutViewVisible
.onEach { latest = it }
.launchIn(this)
@@ -395,6 +518,7 @@ class WifiViewModelTest : SysuiTestCase() {
var latest: Boolean? = null
val job = underTest
.home
.isActivityOutViewVisible
.onEach { latest = it }
.launchIn(this)
@@ -416,6 +540,7 @@ class WifiViewModelTest : SysuiTestCase() {
var latest: Boolean? = null
val job = underTest
.home
.isActivityContainerVisible
.onEach { latest = it }
.launchIn(this)
@@ -437,6 +562,7 @@ class WifiViewModelTest : SysuiTestCase() {
var latest: Boolean? = null
val job = underTest
.home
.isActivityContainerVisible
.onEach { latest = it }
.launchIn(this)
@@ -458,6 +584,7 @@ class WifiViewModelTest : SysuiTestCase() {
var latest: Boolean? = null
val job = underTest
.home
.isActivityContainerVisible
.onEach { latest = it }
.launchIn(this)
@@ -479,6 +606,7 @@ class WifiViewModelTest : SysuiTestCase() {
var latest: Boolean? = null
val job = underTest
.home
.isActivityContainerVisible
.onEach { latest = it }
.launchIn(this)
@@ -501,6 +629,7 @@ class WifiViewModelTest : SysuiTestCase() {
context,
logger,
interactor,
scope,
statusBarPipelineFlags,
)
}