Merge changes I723cfc04,I1eba794e,I9d421a4a,Icd9c2b14,Idee192ff, ... into tm-qpr-dev

* changes:
  [SB Refactor] Only show wifi icon if (1) the network is active and validated; or (2) we're configured to always show it when enabled.
  [SB Refactor] Listen to the WIFI_STATE_CHANGED_ACTION broadcasts and re-fetch `isWifiEnabled` whenever it happens.
  [SB Refactor] Connect the old pipeline and new pipeline visibility calculations together for wifi.
  [SB Refactor] Add an annotation for the visibility states.
  [SB Refactor] Add tracking for wifi enabled state and pipe it through to the UI.
  [SB Refactor] Turn the wifi ViewModel into an @SysUISingleton and instead create separate view models per location.
  [SB Refactor] Display the activity in and out icons using the new pipeline.
  [SB Refactor] Expose the wifi repository flows as StateFlows.
This commit is contained in:
Caitlin Shkuratov
2022-09-29 14:58:38 +00:00
committed by Android (Google) Code Review
35 changed files with 1753 additions and 400 deletions

View File

@@ -131,6 +131,9 @@
<!-- For StatusIconContainer to tag its icon views --> <!-- For StatusIconContainer to tag its icon views -->
<item type="id" name="status_bar_view_state_tag" /> <item type="id" name="status_bar_view_state_tag" />
<!-- Status bar -->
<item type="id" name="status_bar_dot" />
<!-- Default display cutout on the physical top of screen --> <!-- Default display cutout on the physical top of screen -->
<item type="id" name="display_cutout" /> <item type="id" name="display_cutout" />
<item type="id" name="display_cutout_left" /> <item type="id" name="display_cutout_left" />

View File

@@ -30,6 +30,7 @@ import com.android.systemui.qs.carrier.QSCarrierGroupController;
import com.android.systemui.qs.dagger.QSScope; import com.android.systemui.qs.dagger.QSScope;
import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider; import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider;
import com.android.systemui.statusbar.phone.StatusBarIconController; 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.StatusIconContainer;
import com.android.systemui.statusbar.policy.Clock; import com.android.systemui.statusbar.policy.Clock;
import com.android.systemui.statusbar.policy.VariableDateViewController; import com.android.systemui.statusbar.policy.VariableDateViewController;
@@ -104,7 +105,7 @@ class QuickStatusBarHeaderController extends ViewController<QuickStatusBarHeader
mView.requireViewById(R.id.date_clock) mView.requireViewById(R.id.date_clock)
); );
mIconManager = tintedIconManagerFactory.create(mIconContainer); mIconManager = tintedIconManagerFactory.create(mIconContainer, StatusBarLocation.QS);
mDemoModeReceiver = new ClockDemoModeReceiver(mClockView); mDemoModeReceiver = new ClockDemoModeReceiver(mClockView);
mColorExtractor = colorExtractor; mColorExtractor = colorExtractor;
mOnColorsChangedListener = (extractor, which) -> { 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.shade.LargeScreenShadeHeaderController.Companion.QS_HEADER_CONSTRAINT
import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
import com.android.systemui.statusbar.phone.StatusBarIconController 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.StatusIconContainer
import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent.CentralSurfacesScope import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent.CentralSurfacesScope
import com.android.systemui.statusbar.phone.dagger.StatusBarViewModule.LARGE_SCREEN_BATTERY_CONTROLLER import com.android.systemui.statusbar.phone.dagger.StatusBarViewModule.LARGE_SCREEN_BATTERY_CONTROLLER
@@ -261,7 +262,7 @@ class LargeScreenShadeHeaderController @Inject constructor(
batteryMeterViewController.ignoreTunerUpdates() batteryMeterViewController.ignoreTunerUpdates()
batteryIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE) batteryIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
iconManager = tintedIconManagerFactory.create(iconContainer) iconManager = tintedIconManagerFactory.create(iconContainer, StatusBarLocation.QS)
iconManager.setTint( iconManager.setTint(
Utils.getColorAttrDefaultColor(header.context, android.R.attr.textColorPrimary) Utils.getColorAttrDefaultColor(header.context, android.R.attr.textColorPrimary)
) )

View File

@@ -22,6 +22,7 @@ import android.animation.Animator;
import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator; import android.animation.ObjectAnimator;
import android.animation.ValueAnimator; import android.animation.ValueAnimator;
import android.annotation.IntDef;
import android.app.ActivityManager; import android.app.ActivityManager;
import android.app.Notification; import android.app.Notification;
import android.content.Context; import android.content.Context;
@@ -59,6 +60,8 @@ import com.android.systemui.statusbar.notification.NotificationIconDozeHelper;
import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.statusbar.notification.NotificationUtils;
import com.android.systemui.util.drawable.DrawableSize; import com.android.systemui.util.drawable.DrawableSize;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.text.NumberFormat; import java.text.NumberFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@@ -86,6 +89,10 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi
public static final int STATE_DOT = 1; public static final int STATE_DOT = 1;
public static final int STATE_HIDDEN = 2; public static final int STATE_HIDDEN = 2;
@Retention(RetentionPolicy.SOURCE)
@IntDef({STATE_ICON, STATE_DOT, STATE_HIDDEN})
public @interface VisibleState { }
private static final String TAG = "StatusBarIconView"; private static final String TAG = "StatusBarIconView";
private static final Property<StatusBarIconView, Float> ICON_APPEAR_AMOUNT private static final Property<StatusBarIconView, Float> ICON_APPEAR_AMOUNT
= new FloatProperty<StatusBarIconView>("iconAppearAmount") { = new FloatProperty<StatusBarIconView>("iconAppearAmount") {
@@ -133,6 +140,7 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi
private final Paint mDotPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint mDotPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private float mDotRadius; private float mDotRadius;
private int mStaticDotRadius; private int mStaticDotRadius;
@StatusBarIconView.VisibleState
private int mVisibleState = STATE_ICON; private int mVisibleState = STATE_ICON;
private float mIconAppearAmount = 1.0f; private float mIconAppearAmount = 1.0f;
private ObjectAnimator mIconAppearAnimator; private ObjectAnimator mIconAppearAnimator;
@@ -746,11 +754,12 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi
} }
@Override @Override
public void setVisibleState(int state) { public void setVisibleState(@StatusBarIconView.VisibleState int state) {
setVisibleState(state, true /* animate */, null /* endRunnable */); setVisibleState(state, true /* animate */, null /* endRunnable */);
} }
public void setVisibleState(int state, boolean animate) { @Override
public void setVisibleState(@StatusBarIconView.VisibleState int state, boolean animate) {
setVisibleState(state, animate, null); setVisibleState(state, animate, null);
} }
@@ -862,6 +871,7 @@ public class StatusBarIconView extends AnimatedImageView implements StatusIconDi
return mIconAppearAmount; return mIconAppearAmount;
} }
@StatusBarIconView.VisibleState
public int getVisibleState() { public int getVisibleState() {
return mVisibleState; return mVisibleState;
} }

View File

@@ -59,7 +59,8 @@ public class StatusBarMobileView extends FrameLayout implements DarkReceiver,
private ImageView mOut; private ImageView mOut;
private ImageView mMobile, mMobileType, mMobileRoaming; private ImageView mMobile, mMobileType, mMobileRoaming;
private View mMobileRoamingSpace; private View mMobileRoamingSpace;
private int mVisibleState = -1; @StatusBarIconView.VisibleState
private int mVisibleState = STATE_HIDDEN;
private DualToneHandler mDualToneHandler; private DualToneHandler mDualToneHandler;
private boolean mForceHidden; private boolean mForceHidden;
@@ -271,7 +272,7 @@ public class StatusBarMobileView extends FrameLayout implements DarkReceiver,
} }
@Override @Override
public void setVisibleState(int state, boolean animate) { public void setVisibleState(@StatusBarIconView.VisibleState int state, boolean animate) {
if (state == mVisibleState) { if (state == mVisibleState) {
return; return;
} }
@@ -312,6 +313,7 @@ public class StatusBarMobileView extends FrameLayout implements DarkReceiver,
} }
@Override @Override
@StatusBarIconView.VisibleState
public int getVisibleState() { public int getVisibleState() {
return mVisibleState; return mVisibleState;
} }

View File

@@ -55,7 +55,8 @@ public class StatusBarWifiView extends BaseStatusBarWifiView implements DarkRece
private View mAirplaneSpacer; private View mAirplaneSpacer;
private WifiIconState mState; private WifiIconState mState;
private String mSlot; private String mSlot;
private int mVisibleState = -1; @StatusBarIconView.VisibleState
private int mVisibleState = STATE_HIDDEN;
public static StatusBarWifiView fromContext(Context context, String slot) { public static StatusBarWifiView fromContext(Context context, String slot) {
LayoutInflater inflater = LayoutInflater.from(context); LayoutInflater inflater = LayoutInflater.from(context);
@@ -107,7 +108,7 @@ public class StatusBarWifiView extends BaseStatusBarWifiView implements DarkRece
} }
@Override @Override
public void setVisibleState(int state, boolean animate) { public void setVisibleState(@StatusBarIconView.VisibleState int state, boolean animate) {
if (state == mVisibleState) { if (state == mVisibleState) {
return; return;
} }
@@ -131,6 +132,7 @@ public class StatusBarWifiView extends BaseStatusBarWifiView implements DarkRece
} }
@Override @Override
@StatusBarIconView.VisibleState
public int getVisibleState() { public int getVisibleState() {
return mVisibleState; return mVisibleState;
} }

View File

@@ -22,14 +22,32 @@ public interface StatusIconDisplayable extends DarkReceiver {
String getSlot(); String getSlot();
void setStaticDrawableColor(int color); void setStaticDrawableColor(int color);
void setDecorColor(int color); void setDecorColor(int color);
default void setVisibleState(int state) {
/** Sets the visible state that this displayable should be. */
default void setVisibleState(@StatusBarIconView.VisibleState int state) {
setVisibleState(state, false); setVisibleState(state, false);
} }
void setVisibleState(int state, boolean animate);
/**
* Sets the visible state that this displayable should be, and whether the change should
* animate.
*/
void setVisibleState(@StatusBarIconView.VisibleState int state, boolean animate);
/** Returns the current visible state of this displayable. */
@StatusBarIconView.VisibleState
int getVisibleState(); int getVisibleState();
/**
* Returns true if this icon should be visible if there's space, and false otherwise.
*
* Note that this doesn't necessarily mean it *will* be visible. It's possible that there are
* more icons than space, in which case this icon might just show a dot or might be completely
* hidden. {@link #getVisibleState} will return the icon's actual visible status.
*/
boolean isIconVisible(); boolean isIconVisible();
default boolean isIconBlocked() { default boolean isIconBlocked() {
return false; return false;
} }
} }

View File

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

View File

@@ -56,7 +56,6 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Provider;
public interface StatusBarIconController { public interface StatusBarIconController {
@@ -139,13 +138,15 @@ public interface StatusBarIconController {
public DarkIconManager( public DarkIconManager(
LinearLayout linearLayout, LinearLayout linearLayout,
StatusBarLocation location,
StatusBarPipelineFlags statusBarPipelineFlags, StatusBarPipelineFlags statusBarPipelineFlags,
Provider<WifiViewModel> wifiViewModelProvider, WifiViewModel wifiViewModel,
MobileContextProvider mobileContextProvider, MobileContextProvider mobileContextProvider,
DarkIconDispatcher darkIconDispatcher) { DarkIconDispatcher darkIconDispatcher) {
super(linearLayout, super(linearLayout,
location,
statusBarPipelineFlags, statusBarPipelineFlags,
wifiViewModelProvider, wifiViewModel,
mobileContextProvider); mobileContextProvider);
mIconHPadding = mContext.getResources().getDimensionPixelSize( mIconHPadding = mContext.getResources().getDimensionPixelSize(
R.dimen.status_bar_icon_padding); R.dimen.status_bar_icon_padding);
@@ -204,27 +205,28 @@ public interface StatusBarIconController {
@SysUISingleton @SysUISingleton
public static class Factory { public static class Factory {
private final StatusBarPipelineFlags mStatusBarPipelineFlags; private final StatusBarPipelineFlags mStatusBarPipelineFlags;
private final Provider<WifiViewModel> mWifiViewModelProvider; private final WifiViewModel mWifiViewModel;
private final MobileContextProvider mMobileContextProvider; private final MobileContextProvider mMobileContextProvider;
private final DarkIconDispatcher mDarkIconDispatcher; private final DarkIconDispatcher mDarkIconDispatcher;
@Inject @Inject
public Factory( public Factory(
StatusBarPipelineFlags statusBarPipelineFlags, StatusBarPipelineFlags statusBarPipelineFlags,
Provider<WifiViewModel> wifiViewModelProvider, WifiViewModel wifiViewModel,
MobileContextProvider mobileContextProvider, MobileContextProvider mobileContextProvider,
DarkIconDispatcher darkIconDispatcher) { DarkIconDispatcher darkIconDispatcher) {
mStatusBarPipelineFlags = statusBarPipelineFlags; mStatusBarPipelineFlags = statusBarPipelineFlags;
mWifiViewModelProvider = wifiViewModelProvider; mWifiViewModel = wifiViewModel;
mMobileContextProvider = mobileContextProvider; mMobileContextProvider = mobileContextProvider;
mDarkIconDispatcher = darkIconDispatcher; mDarkIconDispatcher = darkIconDispatcher;
} }
public DarkIconManager create(LinearLayout group) { public DarkIconManager create(LinearLayout group, StatusBarLocation location) {
return new DarkIconManager( return new DarkIconManager(
group, group,
location,
mStatusBarPipelineFlags, mStatusBarPipelineFlags,
mWifiViewModelProvider, mWifiViewModel,
mMobileContextProvider, mMobileContextProvider,
mDarkIconDispatcher); mDarkIconDispatcher);
} }
@@ -239,12 +241,14 @@ public interface StatusBarIconController {
public TintedIconManager( public TintedIconManager(
ViewGroup group, ViewGroup group,
StatusBarLocation location,
StatusBarPipelineFlags statusBarPipelineFlags, StatusBarPipelineFlags statusBarPipelineFlags,
Provider<WifiViewModel> wifiViewModelProvider, WifiViewModel wifiViewModel,
MobileContextProvider mobileContextProvider) { MobileContextProvider mobileContextProvider) {
super(group, super(group,
location,
statusBarPipelineFlags, statusBarPipelineFlags,
wifiViewModelProvider, wifiViewModel,
mobileContextProvider); mobileContextProvider);
} }
@@ -278,24 +282,25 @@ public interface StatusBarIconController {
@SysUISingleton @SysUISingleton
public static class Factory { public static class Factory {
private final StatusBarPipelineFlags mStatusBarPipelineFlags; private final StatusBarPipelineFlags mStatusBarPipelineFlags;
private final Provider<WifiViewModel> mWifiViewModelProvider; private final WifiViewModel mWifiViewModel;
private final MobileContextProvider mMobileContextProvider; private final MobileContextProvider mMobileContextProvider;
@Inject @Inject
public Factory( public Factory(
StatusBarPipelineFlags statusBarPipelineFlags, StatusBarPipelineFlags statusBarPipelineFlags,
Provider<WifiViewModel> wifiViewModelProvider, WifiViewModel wifiViewModel,
MobileContextProvider mobileContextProvider) { MobileContextProvider mobileContextProvider) {
mStatusBarPipelineFlags = statusBarPipelineFlags; mStatusBarPipelineFlags = statusBarPipelineFlags;
mWifiViewModelProvider = wifiViewModelProvider; mWifiViewModel = wifiViewModel;
mMobileContextProvider = mobileContextProvider; mMobileContextProvider = mobileContextProvider;
} }
public TintedIconManager create(ViewGroup group) { public TintedIconManager create(ViewGroup group, StatusBarLocation location) {
return new TintedIconManager( return new TintedIconManager(
group, group,
location,
mStatusBarPipelineFlags, mStatusBarPipelineFlags,
mWifiViewModelProvider, mWifiViewModel,
mMobileContextProvider); mMobileContextProvider);
} }
} }
@@ -306,8 +311,9 @@ public interface StatusBarIconController {
*/ */
class IconManager implements DemoModeCommandReceiver { class IconManager implements DemoModeCommandReceiver {
protected final ViewGroup mGroup; protected final ViewGroup mGroup;
private final StatusBarLocation mLocation;
private final StatusBarPipelineFlags mStatusBarPipelineFlags; private final StatusBarPipelineFlags mStatusBarPipelineFlags;
private final Provider<WifiViewModel> mWifiViewModelProvider; private final WifiViewModel mWifiViewModel;
private final MobileContextProvider mMobileContextProvider; private final MobileContextProvider mMobileContextProvider;
protected final Context mContext; protected final Context mContext;
protected final int mIconSize; protected final int mIconSize;
@@ -324,12 +330,14 @@ public interface StatusBarIconController {
public IconManager( public IconManager(
ViewGroup group, ViewGroup group,
StatusBarLocation location,
StatusBarPipelineFlags statusBarPipelineFlags, StatusBarPipelineFlags statusBarPipelineFlags,
Provider<WifiViewModel> wifiViewModelProvider, WifiViewModel wifiViewModel,
MobileContextProvider mobileContextProvider) { MobileContextProvider mobileContextProvider) {
mGroup = group; mGroup = group;
mLocation = location;
mStatusBarPipelineFlags = statusBarPipelineFlags; mStatusBarPipelineFlags = statusBarPipelineFlags;
mWifiViewModelProvider = wifiViewModelProvider; mWifiViewModel = wifiViewModel;
mMobileContextProvider = mobileContextProvider; mMobileContextProvider = mobileContextProvider;
mContext = group.getContext(); mContext = group.getContext();
mIconSize = mContext.getResources().getDimensionPixelSize( mIconSize = mContext.getResources().getDimensionPixelSize(
@@ -446,7 +454,7 @@ public interface StatusBarIconController {
private ModernStatusBarWifiView onCreateModernStatusBarWifiView(String slot) { private ModernStatusBarWifiView onCreateModernStatusBarWifiView(String slot) {
return ModernStatusBarWifiView.constructAndBind( return ModernStatusBarWifiView.constructAndBind(
mContext, slot, mWifiViewModelProvider.get()); mContext, slot, mWifiViewModel, mLocation);
} }
private StatusBarMobileView onCreateStatusBarMobileView(int subId, String slot) { 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.StatusBarHideIconsForBouncerManager;
import com.android.systemui.statusbar.phone.StatusBarIconController; import com.android.systemui.statusbar.phone.StatusBarIconController;
import com.android.systemui.statusbar.phone.StatusBarIconController.DarkIconManager; 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.StatusBarLocationPublisher;
import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent; import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent;
import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent.Startable; import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent.Startable;
@@ -250,7 +251,8 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue
mStatusBar.restoreHierarchyState( mStatusBar.restoreHierarchyState(
savedInstanceState.getSparseParcelableArray(EXTRA_PANEL_STATE)); 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); mDarkIconManager.setShouldLog(true);
updateBlockedIcons(); updateBlockedIcons();
mStatusBarIconController.addIconGroup(mDarkIconManager); mStatusBarIconController.addIconGroup(mDarkIconManager);

View File

@@ -31,6 +31,20 @@ import kotlinx.coroutines.flow.onEach
class ConnectivityPipelineLogger @Inject constructor( class ConnectivityPipelineLogger @Inject constructor(
@StatusBarConnectivityLog private val buffer: LogBuffer, @StatusBarConnectivityLog private val buffer: LogBuffer,
) { ) {
/**
* Logs a change in one of the **raw inputs** to the connectivity pipeline.
*
* Use this method for inputs that don't have any extra information besides their callback name.
*/
fun logInputChange(callbackName: String) {
buffer.log(
SB_LOGGING_TAG,
LogLevel.INFO,
{ str1 = callbackName },
{ "Input: $str1" }
)
}
/** /**
* Logs a change in one of the **raw inputs** to the connectivity pipeline. * Logs a change in one of the **raw inputs** to the connectivity pipeline.
*/ */
@@ -127,13 +141,37 @@ class ConnectivityPipelineLogger @Inject constructor(
companion object { companion object {
const val SB_LOGGING_TAG = "SbConnectivity" const val SB_LOGGING_TAG = "SbConnectivity"
/**
* Log a change in one of the **inputs** to the connectivity pipeline.
*/
fun Flow<Unit>.logInputChange(
logger: ConnectivityPipelineLogger,
inputParamName: String,
): Flow<Unit> {
return this.onEach { logger.logInputChange(inputParamName) }
}
/**
* Log a change in one of the **inputs** to the connectivity pipeline.
*
* @param prettyPrint an optional function to transform the value into a readable string.
* [toString] is used if no custom function is provided.
*/
fun <T> Flow<T>.logInputChange(
logger: ConnectivityPipelineLogger,
inputParamName: String,
prettyPrint: (T) -> String = { it.toString() }
): Flow<T> {
return this.onEach {logger.logInputChange(inputParamName, prettyPrint(it)) }
}
/** /**
* Log a change in one of the **outputs** to the connectivity pipeline. * Log a change in one of the **outputs** to the connectivity pipeline.
* *
* @param prettyPrint an optional function to transform the value into a readable string. * @param prettyPrint an optional function to transform the value into a readable string.
* [toString] is used if no custom function is provided. * [toString] is used if no custom function is provided.
*/ */
fun <T : Any> Flow<T>.logOutputChange( fun <T> Flow<T>.logOutputChange(
logger: ConnectivityPipelineLogger, logger: ConnectivityPipelineLogger,
outputParamName: String, outputParamName: String,
prettyPrint: (T) -> String = { it.toString() } prettyPrint: (T) -> String = { it.toString() }

View File

@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.pipeline.wifi.data.repository package com.android.systemui.statusbar.pipeline.wifi.data.repository
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.IntentFilter
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.Network import android.net.Network
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
@@ -30,51 +31,87 @@ import android.net.wifi.WifiManager
import android.net.wifi.WifiManager.TrafficStateCallback import android.net.wifi.WifiManager.TrafficStateCallback
import android.util.Log import android.util.Log
import com.android.settingslib.Utils import com.android.settingslib.Utils
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.SB_LOGGING_TAG import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.SB_LOGGING_TAG
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiActivityModel import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel
import java.util.concurrent.Executor import java.util.concurrent.Executor
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
/** /** Provides data related to the wifi state. */
* Provides data related to the wifi state.
*/
interface WifiRepository { interface WifiRepository {
/** /** Observable for the current wifi enabled status. */
* Observable for the current wifi network. val isWifiEnabled: StateFlow<Boolean>
*/
val wifiNetwork: Flow<WifiNetworkModel>
/** /** Observable for the current wifi network. */
* Observable for the current wifi network activity. val wifiNetwork: StateFlow<WifiNetworkModel>
*/
val wifiActivity: Flow<WifiActivityModel> /** Observable for the current wifi network activity. */
val wifiActivity: StateFlow<WifiActivityModel>
} }
/** Real implementation of [WifiRepository]. */ /** Real implementation of [WifiRepository]. */
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton @SysUISingleton
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
class WifiRepositoryImpl @Inject constructor( class WifiRepositoryImpl @Inject constructor(
broadcastDispatcher: BroadcastDispatcher,
connectivityManager: ConnectivityManager, connectivityManager: ConnectivityManager,
logger: ConnectivityPipelineLogger, logger: ConnectivityPipelineLogger,
@Main mainExecutor: Executor, @Main mainExecutor: Executor,
@Application scope: CoroutineScope, @Application scope: CoroutineScope,
wifiManager: WifiManager?, wifiManager: WifiManager?,
) : WifiRepository { ) : WifiRepository {
override val wifiNetwork: Flow<WifiNetworkModel> = conflatedCallbackFlow {
private val wifiStateChangeEvents: Flow<Unit> = broadcastDispatcher.broadcastFlow(
IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION)
)
.logInputChange(logger, "WIFI_STATE_CHANGED_ACTION intent")
private val wifiNetworkChangeEvents: MutableSharedFlow<Unit> =
MutableSharedFlow(extraBufferCapacity = 1)
override val isWifiEnabled: StateFlow<Boolean> =
if (wifiManager == null) {
MutableStateFlow(false).asStateFlow()
} else {
// Because [WifiManager] doesn't expose a wifi enabled change listener, we do it
// internally by fetching [WifiManager.isWifiEnabled] whenever we think the state may
// have changed.
merge(wifiNetworkChangeEvents, wifiStateChangeEvents)
.mapLatest { wifiManager.isWifiEnabled }
.distinctUntilChanged()
.logOutputChange(logger, "enabled")
.stateIn(
scope = scope,
started = SharingStarted.WhileSubscribed(),
initialValue = wifiManager.isWifiEnabled
)
}
override val wifiNetwork: StateFlow<WifiNetworkModel> = conflatedCallbackFlow {
var currentWifi: WifiNetworkModel = WIFI_NETWORK_DEFAULT var currentWifi: WifiNetworkModel = WIFI_NETWORK_DEFAULT
val callback = object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) { val callback = object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
@@ -84,6 +121,8 @@ class WifiRepositoryImpl @Inject constructor(
) { ) {
logger.logOnCapabilitiesChanged(network, networkCapabilities) logger.logOnCapabilitiesChanged(network, networkCapabilities)
wifiNetworkChangeEvents.tryEmit(Unit)
val wifiInfo = networkCapabilitiesToWifiInfo(networkCapabilities) val wifiInfo = networkCapabilitiesToWifiInfo(networkCapabilities)
if (wifiInfo?.isPrimary == true) { if (wifiInfo?.isPrimary == true) {
val wifiNetworkModel = createWifiNetworkModel( val wifiNetworkModel = createWifiNetworkModel(
@@ -104,6 +143,9 @@ class WifiRepositoryImpl @Inject constructor(
override fun onLost(network: Network) { override fun onLost(network: Network) {
logger.logOnLost(network) logger.logOnLost(network)
wifiNetworkChangeEvents.tryEmit(Unit)
val wifi = currentWifi val wifi = currentWifi
if (wifi is WifiNetworkModel.Active && wifi.networkId == network.getNetId()) { if (wifi is WifiNetworkModel.Active && wifi.networkId == network.getNetId()) {
val newNetworkModel = WifiNetworkModel.Inactive val newNetworkModel = WifiNetworkModel.Inactive
@@ -132,7 +174,7 @@ class WifiRepositoryImpl @Inject constructor(
initialValue = WIFI_NETWORK_DEFAULT initialValue = WIFI_NETWORK_DEFAULT
) )
override val wifiActivity: Flow<WifiActivityModel> = override val wifiActivity: StateFlow<WifiActivityModel> =
if (wifiManager == null) { if (wifiManager == null) {
Log.w(SB_LOGGING_TAG, "Null WifiManager; skipping activity callback") Log.w(SB_LOGGING_TAG, "Null WifiManager; skipping activity callback")
flowOf(ACTIVITY_DEFAULT) flowOf(ACTIVITY_DEFAULT)
@@ -142,13 +184,15 @@ class WifiRepositoryImpl @Inject constructor(
logger.logInputChange("onTrafficStateChange", prettyPrintActivity(state)) logger.logInputChange("onTrafficStateChange", prettyPrintActivity(state))
trySend(trafficStateToWifiActivityModel(state)) trySend(trafficStateToWifiActivityModel(state))
} }
trySend(ACTIVITY_DEFAULT)
wifiManager.registerTrafficStateCallback(mainExecutor, callback) wifiManager.registerTrafficStateCallback(mainExecutor, callback)
awaitClose { wifiManager.unregisterTrafficStateCallback(callback) } awaitClose { wifiManager.unregisterTrafficStateCallback(callback) }
} }
} }
.stateIn(
scope,
started = SharingStarted.WhileSubscribed(),
initialValue = ACTIVITY_DEFAULT
)
companion object { companion object {
val ACTIVITY_DEFAULT = WifiActivityModel(hasActivityIn = false, hasActivityOut = false) val ACTIVITY_DEFAULT = WifiActivityModel(hasActivityIn = false, hasActivityOut = false)

View File

@@ -22,9 +22,10 @@ import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlo
import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
/** /**
@@ -38,7 +39,11 @@ class WifiInteractor @Inject constructor(
connectivityRepository: ConnectivityRepository, connectivityRepository: ConnectivityRepository,
wifiRepository: WifiRepository, wifiRepository: WifiRepository,
) { ) {
private val ssid: Flow<String?> = wifiRepository.wifiNetwork.map { info -> /**
* The SSID (service set identifier) of the wifi network. Null if we don't have a network, or
* have a network but no valid SSID.
*/
val ssid: Flow<String?> = wifiRepository.wifiNetwork.map { info ->
when (info) { when (info) {
is WifiNetworkModel.Inactive -> null is WifiNetworkModel.Inactive -> null
is WifiNetworkModel.CarrierMerged -> null is WifiNetworkModel.CarrierMerged -> null
@@ -51,17 +56,17 @@ class WifiInteractor @Inject constructor(
} }
} }
/** Our current enabled status. */
val isEnabled: Flow<Boolean> = wifiRepository.isWifiEnabled
/** Our current wifi network. See [WifiNetworkModel]. */ /** Our current wifi network. See [WifiNetworkModel]. */
val wifiNetwork: Flow<WifiNetworkModel> = wifiRepository.wifiNetwork val wifiNetwork: Flow<WifiNetworkModel> = wifiRepository.wifiNetwork
/** Our current wifi activity. See [WifiActivityModel]. */
val activity: StateFlow<WifiActivityModel> = wifiRepository.wifiActivity
/** True if we're configured to force-hide the wifi icon and false otherwise. */ /** True if we're configured to force-hide the wifi icon and false otherwise. */
val isForceHidden: Flow<Boolean> = connectivityRepository.forceHiddenSlots.map { val isForceHidden: Flow<Boolean> = connectivityRepository.forceHiddenSlots.map {
it.contains(ConnectivitySlot.WIFI) it.contains(ConnectivitySlot.WIFI)
} }
/** True if our wifi network has activity in (download), and false otherwise. */
val hasActivityIn: Flow<Boolean> =
combine(wifiRepository.wifiActivity, ssid) { activity, ssid ->
activity.hasActivityIn && ssid != null
}
} }

View File

@@ -41,9 +41,14 @@ class WifiConstants @Inject constructor(
/** True if we should show the activityIn/activityOut icons and false otherwise. */ /** True if we should show the activityIn/activityOut icons and false otherwise. */
val shouldShowActivityConfig = context.resources.getBoolean(R.bool.config_showActivity) val shouldShowActivityConfig = context.resources.getBoolean(R.bool.config_showActivity)
/** True if we should always show the wifi icon when wifi is enabled and false otherwise. */
val alwaysShowIconIfEnabled =
context.resources.getBoolean(R.bool.config_showWifiIndicatorWhenEnabled)
override fun dump(pw: PrintWriter, args: Array<out String>) { override fun dump(pw: PrintWriter, args: Array<out String>) {
pw.apply { pw.apply {
println("shouldShowActivityConfig=$shouldShowActivityConfig") println("shouldShowActivityConfig=$shouldShowActivityConfig")
println("alwaysShowIconIfEnabled=$alwaysShowIconIfEnabled")
} }
} }
} }

View File

@@ -14,11 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
package com.android.systemui.statusbar.pipeline.wifi.data.model package com.android.systemui.statusbar.pipeline.wifi.shared.model
/** /** Provides information on the current wifi activity. */
* Provides information on the current wifi activity.
*/
data class WifiActivityModel( data class WifiActivityModel(
/** True if the wifi has activity in (download). */ /** True if the wifi has activity in (download). */
val hasActivityIn: Boolean, val hasActivityIn: Boolean,

View File

@@ -26,8 +26,15 @@ import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.R import com.android.systemui.R
import com.android.systemui.common.ui.binder.IconViewBinder import com.android.systemui.common.ui.binder.IconViewBinder
import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT
import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON
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 com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel
import kotlinx.coroutines.InternalCoroutinesApi import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -41,40 +48,111 @@ import kotlinx.coroutines.launch
*/ */
@OptIn(InternalCoroutinesApi::class) @OptIn(InternalCoroutinesApi::class)
object WifiViewBinder { object WifiViewBinder {
/** Binds the view to the view-model, continuing to update the former based on the latter. */
/**
* Defines interface for an object that acts as the binding between the view and its view-model.
*
* Users of the [WifiViewBinder] class should use this to control the binder after it is bound.
*/
interface Binding {
/** Returns true if the wifi icon should be visible and false otherwise. */
fun getShouldIconBeVisible(): Boolean
/** Notifies that the visibility state has changed. */
fun onVisibilityStateChanged(@StatusBarIconView.VisibleState state: Int)
}
/**
* 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 @JvmStatic
fun bind( fun bind(
view: ViewGroup, view: ViewGroup,
viewModel: WifiViewModel, wifiViewModel: WifiViewModel,
) { location: StatusBarLocation,
): Binding {
return 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,
): Binding {
val groupView = view.requireViewById<ViewGroup>(R.id.wifi_group)
val iconView = view.requireViewById<ImageView>(R.id.wifi_signal) val iconView = view.requireViewById<ImageView>(R.id.wifi_signal)
val dotView = view.requireViewById<StatusBarIconView>(R.id.status_bar_dot)
val activityInView = view.requireViewById<ImageView>(R.id.wifi_in)
val activityOutView = view.requireViewById<ImageView>(R.id.wifi_out)
val activityContainerView = view.requireViewById<View>(R.id.inout_container)
view.isVisible = true view.isVisible = true
iconView.isVisible = true iconView.isVisible = true
// TODO(b/238425913): We should log this visibility state.
@StatusBarIconView.VisibleState
val visibilityState: MutableStateFlow<Int> = MutableStateFlow(STATE_HIDDEN)
view.repeatWhenAttached { view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) { repeatOnLifecycle(Lifecycle.State.STARTED) {
launch { launch {
viewModel.wifiIcon.distinctUntilChanged().collect { wifiIcon -> visibilityState.collect { visibilityState ->
// TODO(b/238425913): Right now, if !isVisible, there's just an empty space groupView.isVisible = visibilityState == STATE_ICON
// where the wifi icon would be. We need to pipe isVisible through to dotView.isVisible = visibilityState == STATE_DOT
// [ModernStatusBarWifiView.isIconVisible], which is what actually makes }
// the view GONE. }
launch {
viewModel.wifiIcon.collect { wifiIcon ->
view.isVisible = wifiIcon != null view.isVisible = wifiIcon != null
wifiIcon?.let { wifiIcon?.let { IconViewBinder.bind(wifiIcon, iconView) }
IconViewBinder.bind(wifiIcon, iconView)
}
} }
} }
launch { launch {
viewModel.tint.collect { tint -> viewModel.tint.collect { tint ->
iconView.imageTintList = ColorStateList.valueOf(tint) val tintList = ColorStateList.valueOf(tint)
iconView.imageTintList = tintList
activityInView.imageTintList = tintList
activityOutView.imageTintList = tintList
dotView.setDecorColor(tint)
}
}
launch {
viewModel.isActivityInViewVisible.distinctUntilChanged().collect { visible ->
activityInView.isVisible = visible
}
}
launch {
viewModel.isActivityOutViewVisible.distinctUntilChanged().collect { visible ->
activityOutView.isVisible = visible
}
}
launch {
viewModel.isActivityContainerVisible.distinctUntilChanged().collect { visible ->
activityContainerView.isVisible = visible
} }
} }
} }
} }
// TODO(b/238425913): Hook up to [viewModel] to render actual changes to the wifi icon. return object : Binding {
override fun getShouldIconBeVisible(): Boolean {
return viewModel.wifiIcon.value != null
}
override fun onVisibilityStateChanged(@StatusBarIconView.VisibleState state: Int) {
visibilityState.value = state
}
}
} }
} }

View File

@@ -19,10 +19,14 @@ package com.android.systemui.statusbar.pipeline.wifi.ui.view
import android.content.Context import android.content.Context
import android.graphics.Rect import android.graphics.Rect
import android.util.AttributeSet import android.util.AttributeSet
import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import com.android.systemui.R import com.android.systemui.R
import com.android.systemui.statusbar.BaseStatusBarWifiView import com.android.systemui.statusbar.BaseStatusBarWifiView
import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON import com.android.systemui.statusbar.StatusBarIconView
import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT
import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
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.binder.WifiViewBinder
import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel
@@ -36,6 +40,17 @@ class ModernStatusBarWifiView(
) : BaseStatusBarWifiView(context, attrs) { ) : BaseStatusBarWifiView(context, attrs) {
private lateinit var slot: String private lateinit var slot: String
private lateinit var binding: WifiViewBinder.Binding
@StatusBarIconView.VisibleState
private var iconVisibleState: Int = STATE_HIDDEN
set(value) {
if (field == value) {
return
}
field = value
binding.onVisibilityStateChanged(value)
}
override fun onDarkChanged(areas: ArrayList<Rect>?, darkIntensity: Float, tint: Int) { override fun onDarkChanged(areas: ArrayList<Rect>?, darkIntensity: Float, tint: Int) {
// TODO(b/238425913) // TODO(b/238425913)
@@ -51,42 +66,64 @@ class ModernStatusBarWifiView(
// TODO(b/238425913) // TODO(b/238425913)
} }
override fun setVisibleState(state: Int, animate: Boolean) { override fun setVisibleState(@StatusBarIconView.VisibleState state: Int, animate: Boolean) {
// TODO(b/238425913) iconVisibleState = state
} }
@StatusBarIconView.VisibleState
override fun getVisibleState(): Int { override fun getVisibleState(): Int {
// TODO(b/238425913) return iconVisibleState
return STATE_ICON
} }
override fun isIconVisible(): Boolean { override fun isIconVisible(): Boolean {
// TODO(b/238425913) return binding.getShouldIconBeVisible()
return true
} }
/** Set the slot name for this view. */ private fun initView(
private fun setSlot(slotName: String) { slotName: String,
this.slot = slotName wifiViewModel: WifiViewModel,
location: StatusBarLocation,
) {
slot = slotName
initDotView()
binding = WifiViewBinder.bind(this, wifiViewModel, location)
}
// Mostly duplicated from [com.android.systemui.statusbar.StatusBarWifiView].
private fun initDotView() {
// TODO(b/238425913): Could we just have this dot view be part of
// R.layout.new_status_bar_wifi_group with a dot drawable so we don't need to inflate it
// manually? Would that not work with animations?
val dotView = StatusBarIconView(mContext, slot, null).also {
it.id = R.id.status_bar_dot
// Hard-code this view to always be in the DOT state so that whenever it's visible it
// will show a dot
it.visibleState = STATE_DOT
}
val width = mContext.resources.getDimensionPixelSize(R.dimen.status_bar_icon_size)
val lp = LayoutParams(width, width)
lp.gravity = Gravity.CENTER_VERTICAL or Gravity.START
addView(dotView, lp)
} }
companion object { 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. * returns it.
*/ */
@JvmStatic @JvmStatic
fun constructAndBind( fun constructAndBind(
context: Context, context: Context,
slot: String, slot: String,
viewModel: WifiViewModel, wifiViewModel: WifiViewModel,
location: StatusBarLocation,
): ModernStatusBarWifiView { ): ModernStatusBarWifiView {
return ( return (
LayoutInflater.from(context).inflate(R.layout.new_status_bar_wifi_group, null) LayoutInflater.from(context).inflate(R.layout.new_status_bar_wifi_group, null)
as ModernStatusBarWifiView as ModernStatusBarWifiView
).also { ).also {
it.setSlot(slot) it.initView(slot, wifiViewModel, location)
WifiViewBinder.bind(it, viewModel)
} }
} }
} }

View File

@@ -0,0 +1,43 @@
/*
* 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.StateFlow
/**
* 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: StateFlow<Icon?>,
isActivityInViewVisible: Flow<Boolean>,
isActivityOutViewVisible: Flow<Boolean>,
isActivityContainerVisible: Flow<Boolean>,
) :
LocationBasedWifiViewModel(
statusBarPipelineFlags,
debugTint = Color.CYAN,
wifiIcon,
isActivityInViewVisible,
isActivityOutViewVisible,
isActivityContainerVisible,
)

View File

@@ -0,0 +1,40 @@
/*
* 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.StateFlow
/** A view model for the wifi icon shown on keyguard (lockscreen). */
class KeyguardWifiViewModel(
statusBarPipelineFlags: StatusBarPipelineFlags,
wifiIcon: StateFlow<Icon?>,
isActivityInViewVisible: Flow<Boolean>,
isActivityOutViewVisible: Flow<Boolean>,
isActivityContainerVisible: Flow<Boolean>,
) :
LocationBasedWifiViewModel(
statusBarPipelineFlags,
debugTint = Color.MAGENTA,
wifiIcon,
isActivityInViewVisible,
isActivityOutViewVisible,
isActivityContainerVisible,
)

View File

@@ -0,0 +1,68 @@
/*
* 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.StateFlow
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: StateFlow<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,40 @@
/*
* 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.StateFlow
/** A view model for the wifi icon shown in quick settings (when the shade is pulled down). */
class QsWifiViewModel(
statusBarPipelineFlags: StatusBarPipelineFlags,
wifiIcon: StateFlow<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 package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel
import android.content.Context import android.content.Context
import android.graphics.Color
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.annotation.VisibleForTesting 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.R
import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon 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_FULL_ICONS
import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_INTERNET_ICONS import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_INTERNET_ICONS
import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_NETWORK import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_NETWORK
@@ -35,98 +36,171 @@ import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants
import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
/** /**
* Models the UI state for the status bar wifi icon. * 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
statusBarPipelineFlags: StatusBarPipelineFlags, class WifiViewModel
private val constants: WifiConstants, @Inject
constructor(
constants: WifiConstants,
private val context: Context, private val context: Context,
private val logger: ConnectivityPipelineLogger, logger: ConnectivityPipelineLogger,
private val interactor: WifiInteractor, 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. * Returns the drawable resource ID to use for the wifi icon based on the given network.
* Null if we can't compute the icon.
*/ */
@DrawableRes @DrawableRes
private val iconResId: Flow<Int?> = interactor.wifiNetwork.map { private fun WifiNetworkModel.iconResId(): Int? {
when (it) { return when (this) {
is WifiNetworkModel.CarrierMerged -> null is WifiNetworkModel.CarrierMerged -> null
is WifiNetworkModel.Inactive -> WIFI_NO_NETWORK is WifiNetworkModel.Inactive -> WIFI_NO_NETWORK
is WifiNetworkModel.Active -> is WifiNetworkModel.Active ->
when { when {
it.level == null -> null this.level == null -> null
it.isValidated -> WIFI_FULL_ICONS[it.level] this.isValidated -> WIFI_FULL_ICONS[this.level]
else -> WIFI_NO_INTERNET_ICONS[it.level] else -> WIFI_NO_INTERNET_ICONS[this.level]
} }
} }
} }
/** The content description for the wifi icon. */ /**
private val contentDescription: Flow<ContentDescription?> = interactor.wifiNetwork.map { * Returns the content description for the wifi icon based on the given network.
when (it) { * Null if we can't compute the content description.
*/
private fun WifiNetworkModel.contentDescription(): ContentDescription? {
return when (this) {
is WifiNetworkModel.CarrierMerged -> null is WifiNetworkModel.CarrierMerged -> null
is WifiNetworkModel.Inactive -> is WifiNetworkModel.Inactive ->
ContentDescription.Loaded( ContentDescription.Loaded(
"${context.getString(WIFI_NO_CONNECTION)},${context.getString(NO_INTERNET)}" "${context.getString(WIFI_NO_CONNECTION)},${context.getString(NO_INTERNET)}"
) )
is WifiNetworkModel.Active -> is WifiNetworkModel.Active ->
when (it.level) { when (this.level) {
null -> null null -> null
else -> { else -> {
val levelDesc = context.getString(WIFI_CONNECTION_STRENGTH[it.level]) val levelDesc = context.getString(WIFI_CONNECTION_STRENGTH[this.level])
when { when {
it.isValidated -> ContentDescription.Loaded(levelDesc) this.isValidated -> ContentDescription.Loaded(levelDesc)
else -> ContentDescription.Loaded( else ->
"$levelDesc,${context.getString(NO_INTERNET)}" ContentDescription.Loaded(
) "$levelDesc,${context.getString(NO_INTERNET)}"
)
} }
} }
} }
} }
} }
/** /** The wifi icon that should be displayed. Null if we shouldn't display any icon. */
* The wifi icon that should be displayed. Null if we shouldn't display any icon. private val wifiIcon: StateFlow<Icon?> =
*/ combine(
val wifiIcon: Flow<Icon?> = combine( interactor.isEnabled,
interactor.isForceHidden, interactor.isForceHidden,
iconResId, interactor.wifiNetwork,
contentDescription, ) { isEnabled, isForceHidden, wifiNetwork ->
) { isForceHidden, iconResId, contentDescription -> if (!isEnabled || isForceHidden || wifiNetwork is WifiNetworkModel.CarrierMerged) {
when { return@combine null
isForceHidden || }
iconResId == null ||
iconResId <= 0 -> null val iconResId = wifiNetwork.iconResId() ?: return@combine null
else -> Icon.Resource(iconResId, contentDescription) val icon = Icon.Resource(iconResId, wifiNetwork.contentDescription())
return@combine when {
constants.alwaysShowIconIfEnabled -> icon
wifiNetwork is WifiNetworkModel.Active && wifiNetwork.isValidated -> icon
else -> null
} }
} }
.stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = null)
/** /** The wifi activity status. Null if we shouldn't display the activity status. */
* True if the activity in icon should be displayed and false otherwise. private val activity: Flow<WifiActivityModel?> =
*/ if (!constants.shouldShowActivityConfig) {
val isActivityInVisible: Flow<Boolean> flowOf(null)
get() = } else {
if (!constants.shouldShowActivityConfig) { combine(interactor.activity, interactor.ssid) { activity, ssid ->
flowOf(false) when (ssid) {
} else { null -> null
interactor.hasActivityIn else -> activity
}
} }
.logOutputChange(logger, "activityInVisible") }
.distinctUntilChanged()
.logOutputChange(logger, "activity")
.stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = null)
/** The tint that should be applied to the icon. */ private val isActivityInViewVisible: Flow<Boolean> =
val tint: Flow<Int> = if (!statusBarPipelineFlags.useNewPipelineDebugColoring()) { activity
emptyFlow() .map { it?.hasActivityIn == true }
} else { .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false)
flowOf(Color.CYAN)
} private val isActivityOutViewVisible: Flow<Boolean> =
activity
.map { it?.hasActivityOut == true }
.stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false)
private val isActivityContainerVisible: Flow<Boolean> =
combine(isActivityInViewVisible, isActivityOutViewVisible) { activityIn, activityOut ->
activityIn || activityOut
}
.stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false)
/** A view model for the status bar on the home screen. */
val home: HomeWifiViewModel =
HomeWifiViewModel(
statusBarPipelineFlags,
wifiIcon,
isActivityInViewVisible,
isActivityOutViewVisible,
isActivityContainerVisible,
)
/** 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 { companion object {
@StringRes @StringRes

View File

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

View File

@@ -176,7 +176,7 @@ class LargeScreenShadeHeaderControllerCombinedTest : SysuiTestCase() {
} }
whenever(view.visibility).thenAnswer { _ -> viewVisibility } 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.COMBINED_QS_HEADERS)).thenReturn(true)
whenever(featureFlags.isEnabled(Flags.NEW_HEADER)).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(view.visibility).thenAnswer { _ -> viewVisibility }
whenever(variableDateViewControllerFactory.create(any())) whenever(variableDateViewControllerFactory.create(any()))
.thenReturn(variableDateViewController) .thenReturn(variableDateViewController)
whenever(iconManagerFactory.create(any())).thenReturn(iconManager) whenever(iconManagerFactory.create(any(), any())).thenReturn(iconManager)
whenever(featureFlags.isEnabled(Flags.COMBINED_QS_HEADERS)).thenReturn(false) whenever(featureFlags.isEnabled(Flags.COMBINED_QS_HEADERS)).thenReturn(false)
mLargeScreenShadeHeaderController = LargeScreenShadeHeaderController( mLargeScreenShadeHeaderController = LargeScreenShadeHeaderController(
view, view,

View File

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

View File

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

View File

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

View File

@@ -23,9 +23,16 @@ import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpManager import com.android.systemui.dump.DumpManager
import com.android.systemui.log.LogBufferFactory import com.android.systemui.log.LogBufferFactory
import com.android.systemui.log.LogcatEchoTracker import com.android.systemui.log.LogcatEchoTracker
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logInputChange
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger.Companion.logOutputChange
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import java.io.PrintWriter import java.io.PrintWriter
import java.io.StringWriter import java.io.StringWriter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.runBlocking
import org.junit.Test import org.junit.Test
import org.mockito.Mockito import org.mockito.Mockito
import org.mockito.Mockito.mock import org.mockito.Mockito.mock
@@ -64,12 +71,70 @@ class ConnectivityPipelineLoggerTest : SysuiTestCase() {
assertThat(actualString).contains(expectedNetId) assertThat(actualString).contains(expectedNetId)
} }
private val NET_1_ID = 100 @Test
private val NET_1 = com.android.systemui.util.mockito.mock<Network>().also { fun logOutputChange_printsValuesAndNulls() = runBlocking(IMMEDIATE) {
Mockito.`when`(it.getNetId()).thenReturn(NET_1_ID) val flow: Flow<Int?> = flowOf(1, null, 3)
val job = flow
.logOutputChange(logger, "testInts")
.launchIn(this)
val stringWriter = StringWriter()
buffer.dump(PrintWriter(stringWriter), tailLength = 0)
val actualString = stringWriter.toString()
assertThat(actualString).contains("1")
assertThat(actualString).contains("null")
assertThat(actualString).contains("3")
job.cancel()
}
@Test
fun logInputChange_unit_printsInputName() = runBlocking(IMMEDIATE) {
val flow: Flow<Unit> = flowOf(Unit, Unit)
val job = flow
.logInputChange(logger, "testInputs")
.launchIn(this)
val stringWriter = StringWriter()
buffer.dump(PrintWriter(stringWriter), tailLength = 0)
val actualString = stringWriter.toString()
assertThat(actualString).contains("testInputs")
job.cancel()
}
@Test
fun logInputChange_any_printsValuesAndNulls() = runBlocking(IMMEDIATE) {
val flow: Flow<Any?> = flowOf(null, 2, "threeString")
val job = flow
.logInputChange(logger, "testInputs")
.launchIn(this)
val stringWriter = StringWriter()
buffer.dump(PrintWriter(stringWriter), tailLength = 0)
val actualString = stringWriter.toString()
assertThat(actualString).contains("null")
assertThat(actualString).contains("2")
assertThat(actualString).contains("threeString")
job.cancel()
}
companion object {
private const val NET_1_ID = 100
private val NET_1 = com.android.systemui.util.mockito.mock<Network>().also {
Mockito.`when`(it.getNetId()).thenReturn(NET_1_ID)
}
private val NET_1_CAPS = NetworkCapabilities.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
.build()
private val IMMEDIATE = Dispatchers.Main.immediate
} }
private val NET_1_CAPS = NetworkCapabilities.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
.build()
} }

View File

@@ -16,20 +16,27 @@
package com.android.systemui.statusbar.pipeline.wifi.data.repository package com.android.systemui.statusbar.pipeline.wifi.data.repository
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiActivityModel
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl.Companion.ACTIVITY_DEFAULT import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl.Companion.ACTIVITY_DEFAULT
import kotlinx.coroutines.flow.Flow import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
/** Fake implementation of [WifiRepository] exposing set methods for all the flows. */ /** Fake implementation of [WifiRepository] exposing set methods for all the flows. */
class FakeWifiRepository : WifiRepository { class FakeWifiRepository : WifiRepository {
private val _isWifiEnabled: MutableStateFlow<Boolean> = MutableStateFlow(false)
override val isWifiEnabled: StateFlow<Boolean> = _isWifiEnabled
private val _wifiNetwork: MutableStateFlow<WifiNetworkModel> = private val _wifiNetwork: MutableStateFlow<WifiNetworkModel> =
MutableStateFlow(WifiNetworkModel.Inactive) MutableStateFlow(WifiNetworkModel.Inactive)
override val wifiNetwork: Flow<WifiNetworkModel> = _wifiNetwork override val wifiNetwork: StateFlow<WifiNetworkModel> = _wifiNetwork
private val _wifiActivity = MutableStateFlow(ACTIVITY_DEFAULT) private val _wifiActivity = MutableStateFlow(ACTIVITY_DEFAULT)
override val wifiActivity: Flow<WifiActivityModel> = _wifiActivity override val wifiActivity: StateFlow<WifiActivityModel> = _wifiActivity
fun setIsWifiEnabled(enabled: Boolean) {
_isWifiEnabled.value = enabled
}
fun setWifiNetwork(wifiNetworkModel: WifiNetworkModel) { fun setWifiNetwork(wifiNetworkModel: WifiNetworkModel) {
_wifiNetwork.value = wifiNetworkModel _wifiNetwork.value = wifiNetworkModel

View File

@@ -28,15 +28,17 @@ import android.net.wifi.WifiManager
import android.net.wifi.WifiManager.TrafficStateCallback import android.net.wifi.WifiManager.TrafficStateCallback
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiActivityModel
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl.Companion.ACTIVITY_DEFAULT import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl.Companion.ACTIVITY_DEFAULT
import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl.Companion.WIFI_NETWORK_DEFAULT import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepositoryImpl.Companion.WIFI_NETWORK_DEFAULT
import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel
import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.nullable
import com.android.systemui.util.time.FakeSystemClock import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import java.util.concurrent.Executor import java.util.concurrent.Executor
@@ -44,23 +46,28 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@SmallTest @SmallTest
class WifiRepositoryImplTest : SysuiTestCase() { class WifiRepositoryImplTest : SysuiTestCase() {
private lateinit var underTest: WifiRepositoryImpl private lateinit var underTest: WifiRepositoryImpl
@Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
@Mock private lateinit var logger: ConnectivityPipelineLogger @Mock private lateinit var logger: ConnectivityPipelineLogger
@Mock private lateinit var connectivityManager: ConnectivityManager @Mock private lateinit var connectivityManager: ConnectivityManager
@Mock private lateinit var wifiManager: WifiManager @Mock private lateinit var wifiManager: WifiManager
@@ -70,16 +77,17 @@ class WifiRepositoryImplTest : SysuiTestCase() {
@Before @Before
fun setUp() { fun setUp() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
whenever(
broadcastDispatcher.broadcastFlow(
any(),
nullable(),
anyInt(),
nullable(),
)
).thenReturn(flowOf(Unit))
executor = FakeExecutor(FakeSystemClock()) executor = FakeExecutor(FakeSystemClock())
scope = CoroutineScope(IMMEDIATE) scope = CoroutineScope(IMMEDIATE)
underTest = createRepo()
underTest = WifiRepositoryImpl(
connectivityManager,
logger,
executor,
scope,
wifiManager,
)
} }
@After @After
@@ -87,6 +95,132 @@ class WifiRepositoryImplTest : SysuiTestCase() {
scope.cancel() scope.cancel()
} }
@Test
fun isWifiEnabled_nullWifiManager_getsFalse() = runBlocking(IMMEDIATE) {
underTest = createRepo(wifiManagerToUse = null)
assertThat(underTest.isWifiEnabled.value).isFalse()
}
@Test
fun isWifiEnabled_initiallyGetsWifiManagerValue() = runBlocking(IMMEDIATE) {
whenever(wifiManager.isWifiEnabled).thenReturn(true)
underTest = createRepo()
assertThat(underTest.isWifiEnabled.value).isTrue()
}
@Test
fun isWifiEnabled_networkCapabilitiesChanged_valueUpdated() = runBlocking(IMMEDIATE) {
// We need to call launch on the flows so that they start updating
val networkJob = underTest.wifiNetwork.launchIn(this)
val enabledJob = underTest.isWifiEnabled.launchIn(this)
whenever(wifiManager.isWifiEnabled).thenReturn(true)
getNetworkCallback().onCapabilitiesChanged(
NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO)
)
assertThat(underTest.isWifiEnabled.value).isTrue()
whenever(wifiManager.isWifiEnabled).thenReturn(false)
getNetworkCallback().onCapabilitiesChanged(
NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO)
)
assertThat(underTest.isWifiEnabled.value).isFalse()
networkJob.cancel()
enabledJob.cancel()
}
@Test
fun isWifiEnabled_networkLost_valueUpdated() = runBlocking(IMMEDIATE) {
// We need to call launch on the flows so that they start updating
val networkJob = underTest.wifiNetwork.launchIn(this)
val enabledJob = underTest.isWifiEnabled.launchIn(this)
whenever(wifiManager.isWifiEnabled).thenReturn(true)
getNetworkCallback().onLost(NETWORK)
assertThat(underTest.isWifiEnabled.value).isTrue()
whenever(wifiManager.isWifiEnabled).thenReturn(false)
getNetworkCallback().onLost(NETWORK)
assertThat(underTest.isWifiEnabled.value).isFalse()
networkJob.cancel()
enabledJob.cancel()
}
@Test
fun isWifiEnabled_intentsReceived_valueUpdated() = runBlocking(IMMEDIATE) {
val intentFlow = MutableSharedFlow<Unit>()
whenever(
broadcastDispatcher.broadcastFlow(
any(),
nullable(),
anyInt(),
nullable(),
)
).thenReturn(intentFlow)
underTest = createRepo()
val job = underTest.isWifiEnabled.launchIn(this)
whenever(wifiManager.isWifiEnabled).thenReturn(true)
intentFlow.emit(Unit)
assertThat(underTest.isWifiEnabled.value).isTrue()
whenever(wifiManager.isWifiEnabled).thenReturn(false)
intentFlow.emit(Unit)
assertThat(underTest.isWifiEnabled.value).isFalse()
job.cancel()
}
@Test
fun isWifiEnabled_bothIntentAndNetworkUpdates_valueAlwaysUpdated() = runBlocking(IMMEDIATE) {
val intentFlow = MutableSharedFlow<Unit>()
whenever(
broadcastDispatcher.broadcastFlow(
any(),
nullable(),
anyInt(),
nullable(),
)
).thenReturn(intentFlow)
underTest = createRepo()
val networkJob = underTest.wifiNetwork.launchIn(this)
val enabledJob = underTest.isWifiEnabled.launchIn(this)
whenever(wifiManager.isWifiEnabled).thenReturn(false)
intentFlow.emit(Unit)
assertThat(underTest.isWifiEnabled.value).isFalse()
whenever(wifiManager.isWifiEnabled).thenReturn(true)
getNetworkCallback().onLost(NETWORK)
assertThat(underTest.isWifiEnabled.value).isTrue()
whenever(wifiManager.isWifiEnabled).thenReturn(false)
getNetworkCallback().onCapabilitiesChanged(
NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO)
)
assertThat(underTest.isWifiEnabled.value).isFalse()
whenever(wifiManager.isWifiEnabled).thenReturn(true)
intentFlow.emit(Unit)
assertThat(underTest.isWifiEnabled.value).isTrue()
networkJob.cancel()
enabledJob.cancel()
}
@Test @Test
fun wifiNetwork_initiallyGetsDefault() = runBlocking(IMMEDIATE) { fun wifiNetwork_initiallyGetsDefault() = runBlocking(IMMEDIATE) {
var latest: WifiNetworkModel? = null var latest: WifiNetworkModel? = null
@@ -509,13 +643,7 @@ class WifiRepositoryImplTest : SysuiTestCase() {
@Test @Test
fun wifiActivity_nullWifiManager_receivesDefault() = runBlocking(IMMEDIATE) { fun wifiActivity_nullWifiManager_receivesDefault() = runBlocking(IMMEDIATE) {
underTest = WifiRepositoryImpl( underTest = createRepo(wifiManagerToUse = null)
connectivityManager,
logger,
executor,
scope,
wifiManager = null,
)
var latest: WifiActivityModel? = null var latest: WifiActivityModel? = null
val job = underTest val job = underTest
@@ -594,6 +722,17 @@ class WifiRepositoryImplTest : SysuiTestCase() {
job.cancel() job.cancel()
} }
private fun createRepo(wifiManagerToUse: WifiManager? = wifiManager): WifiRepositoryImpl {
return WifiRepositoryImpl(
broadcastDispatcher,
connectivityManager,
logger,
executor,
scope,
wifiManagerToUse,
)
}
private fun getTrafficStateCallback(): TrafficStateCallback { private fun getTrafficStateCallback(): TrafficStateCallback {
val callbackCaptor = argumentCaptor<TrafficStateCallback>() val callbackCaptor = argumentCaptor<TrafficStateCallback>()
verify(wifiManager).registerTrafficStateCallback(any(), callbackCaptor.capture()) verify(wifiManager).registerTrafficStateCallback(any(), callbackCaptor.capture())

View File

@@ -16,13 +16,14 @@
package com.android.systemui.statusbar.pipeline.wifi.domain.interactor package com.android.systemui.statusbar.pipeline.wifi.domain.interactor
import android.net.wifi.WifiManager
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiActivityModel
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -50,172 +51,129 @@ class WifiInteractorTest : SysuiTestCase() {
} }
@Test @Test
fun hasActivityIn_noInOrOut_outputsFalse() = runBlocking(IMMEDIATE) { fun ssid_inactiveNetwork_outputsNull() = runBlocking(IMMEDIATE) {
wifiRepository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL)
wifiRepository.setWifiActivity(
WifiActivityModel(hasActivityIn = false, hasActivityOut = false)
)
var latest: Boolean? = null
val job = underTest
.hasActivityIn
.onEach { latest = it }
.launchIn(this)
assertThat(latest).isFalse()
job.cancel()
}
@Test
fun hasActivityIn_onlyOut_outputsFalse() = runBlocking(IMMEDIATE) {
wifiRepository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL)
wifiRepository.setWifiActivity(
WifiActivityModel(hasActivityIn = false, hasActivityOut = true)
)
var latest: Boolean? = null
val job = underTest
.hasActivityIn
.onEach { latest = it }
.launchIn(this)
assertThat(latest).isFalse()
job.cancel()
}
@Test
fun hasActivityIn_onlyIn_outputsTrue() = runBlocking(IMMEDIATE) {
wifiRepository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL)
wifiRepository.setWifiActivity(
WifiActivityModel(hasActivityIn = true, hasActivityOut = false)
)
var latest: Boolean? = null
val job = underTest
.hasActivityIn
.onEach { latest = it }
.launchIn(this)
assertThat(latest).isTrue()
job.cancel()
}
@Test
fun hasActivityIn_inAndOut_outputsTrue() = runBlocking(IMMEDIATE) {
wifiRepository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL)
wifiRepository.setWifiActivity(
WifiActivityModel(hasActivityIn = true, hasActivityOut = true)
)
var latest: Boolean? = null
val job = underTest
.hasActivityIn
.onEach { latest = it }
.launchIn(this)
assertThat(latest).isTrue()
job.cancel()
}
@Test
fun hasActivityIn_ssidNull_outputsFalse() = runBlocking(IMMEDIATE) {
wifiRepository.setWifiNetwork(WifiNetworkModel.Active(networkId = 1, ssid = null))
wifiRepository.setWifiActivity(
WifiActivityModel(hasActivityIn = true, hasActivityOut = true)
)
var latest: Boolean? = null
val job = underTest
.hasActivityIn
.onEach { latest = it }
.launchIn(this)
assertThat(latest).isFalse()
job.cancel()
}
@Test
fun hasActivityIn_inactiveNetwork_outputsFalse() = runBlocking(IMMEDIATE) {
wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive) wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive)
wifiRepository.setWifiActivity(
WifiActivityModel(hasActivityIn = true, hasActivityOut = true)
)
var latest: Boolean? = null var latest: String? = "default"
val job = underTest val job = underTest
.hasActivityIn .ssid
.onEach { latest = it } .onEach { latest = it }
.launchIn(this) .launchIn(this)
assertThat(latest).isFalse() assertThat(latest).isNull()
job.cancel() job.cancel()
} }
@Test @Test
fun hasActivityIn_carrierMergedNetwork_outputsFalse() = runBlocking(IMMEDIATE) { fun ssid_carrierMergedNetwork_outputsNull() = runBlocking(IMMEDIATE) {
wifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged) wifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged)
wifiRepository.setWifiActivity(
WifiActivityModel(hasActivityIn = true, hasActivityOut = true)
)
var latest: Boolean? = null var latest: String? = "default"
val job = underTest val job = underTest
.hasActivityIn .ssid
.onEach { latest = it } .onEach { latest = it }
.launchIn(this) .launchIn(this)
assertThat(latest).isFalse() assertThat(latest).isNull()
job.cancel() job.cancel()
} }
@Test @Test
fun hasActivityIn_multipleChanges_multipleOutputChanges() = runBlocking(IMMEDIATE) { fun ssid_isPasspointAccessPoint_outputsPasspointName() = runBlocking(IMMEDIATE) {
wifiRepository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL) wifiRepository.setWifiNetwork(WifiNetworkModel.Active(
networkId = 1,
isPasspointAccessPoint = true,
passpointProviderFriendlyName = "friendly",
))
var latest: String? = null
val job = underTest
.ssid
.onEach { latest = it }
.launchIn(this)
assertThat(latest).isEqualTo("friendly")
job.cancel()
}
@Test
fun ssid_isOnlineSignUpForPasspoint_outputsPasspointName() = runBlocking(IMMEDIATE) {
wifiRepository.setWifiNetwork(WifiNetworkModel.Active(
networkId = 1,
isOnlineSignUpForPasspointAccessPoint = true,
passpointProviderFriendlyName = "friendly",
))
var latest: String? = null
val job = underTest
.ssid
.onEach { latest = it }
.launchIn(this)
assertThat(latest).isEqualTo("friendly")
job.cancel()
}
@Test
fun ssid_unknownSsid_outputsNull() = runBlocking(IMMEDIATE) {
wifiRepository.setWifiNetwork(WifiNetworkModel.Active(
networkId = 1,
ssid = WifiManager.UNKNOWN_SSID,
))
var latest: String? = "default"
val job = underTest
.ssid
.onEach { latest = it }
.launchIn(this)
assertThat(latest).isNull()
job.cancel()
}
@Test
fun ssid_validSsid_outputsSsid() = runBlocking(IMMEDIATE) {
wifiRepository.setWifiNetwork(WifiNetworkModel.Active(
networkId = 1,
ssid = "MyAwesomeWifiNetwork",
))
var latest: String? = null
val job = underTest
.ssid
.onEach { latest = it }
.launchIn(this)
assertThat(latest).isEqualTo("MyAwesomeWifiNetwork")
job.cancel()
}
@Test
fun isEnabled_matchesRepoIsEnabled() = runBlocking(IMMEDIATE) {
var latest: Boolean? = null var latest: Boolean? = null
val job = underTest val job = underTest
.hasActivityIn .isEnabled
.onEach { latest = it } .onEach { latest = it }
.launchIn(this) .launchIn(this)
// Conduct a series of changes and verify we catch each of them in succession wifiRepository.setIsWifiEnabled(true)
wifiRepository.setWifiActivity(
WifiActivityModel(hasActivityIn = true, hasActivityOut = false)
)
yield() yield()
assertThat(latest).isTrue() assertThat(latest).isTrue()
wifiRepository.setWifiActivity( wifiRepository.setIsWifiEnabled(false)
WifiActivityModel(hasActivityIn = false, hasActivityOut = true)
)
yield() yield()
assertThat(latest).isFalse() assertThat(latest).isFalse()
wifiRepository.setWifiActivity( wifiRepository.setIsWifiEnabled(true)
WifiActivityModel(hasActivityIn = true, hasActivityOut = true)
)
yield() yield()
assertThat(latest).isTrue() assertThat(latest).isTrue()
wifiRepository.setWifiActivity(
WifiActivityModel(hasActivityIn = true, hasActivityOut = false)
)
yield()
assertThat(latest).isTrue()
wifiRepository.setWifiActivity(
WifiActivityModel(hasActivityIn = false, hasActivityOut = false)
)
yield()
assertThat(latest).isFalse()
job.cancel() job.cancel()
} }
@@ -241,6 +199,32 @@ class WifiInteractorTest : SysuiTestCase() {
job.cancel() job.cancel()
} }
@Test
fun activity_matchesRepoWifiActivity() = runBlocking(IMMEDIATE) {
var latest: WifiActivityModel? = null
val job = underTest
.activity
.onEach { latest = it }
.launchIn(this)
val activity1 = WifiActivityModel(hasActivityIn = true, hasActivityOut = true)
wifiRepository.setWifiActivity(activity1)
yield()
assertThat(latest).isEqualTo(activity1)
val activity2 = WifiActivityModel(hasActivityIn = false, hasActivityOut = false)
wifiRepository.setWifiActivity(activity2)
yield()
assertThat(latest).isEqualTo(activity2)
val activity3 = WifiActivityModel(hasActivityIn = true, hasActivityOut = false)
wifiRepository.setWifiActivity(activity3)
yield()
assertThat(latest).isEqualTo(activity3)
job.cancel()
}
@Test @Test
fun isForceHidden_repoHasWifiHidden_outputsTrue() = runBlocking(IMMEDIATE) { fun isForceHidden_repoHasWifiHidden_outputsTrue() = runBlocking(IMMEDIATE) {
connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.WIFI)) connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.WIFI))
@@ -270,10 +254,6 @@ class WifiInteractorTest : SysuiTestCase() {
job.cancel() job.cancel()
} }
companion object {
val VALID_WIFI_NETWORK_MODEL = WifiNetworkModel.Active(networkId = 1, ssid = "AB")
}
} }
private val IMMEDIATE = Dispatchers.Main.immediate private val IMMEDIATE = Dispatchers.Main.immediate

View File

@@ -16,38 +16,216 @@
package com.android.systemui.statusbar.pipeline.wifi.ui.view package com.android.systemui.statusbar.pipeline.wifi.ui.view
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.testing.TestableLooper.RunWithLooper import android.testing.TestableLooper.RunWithLooper
import android.testing.ViewUtils
import android.view.View
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import com.android.systemui.R
import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestCase
import com.android.systemui.lifecycle.InstantTaskExecutorRule import com.android.systemui.lifecycle.InstantTaskExecutorRule
import com.android.systemui.util.Assert import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT
import com.android.systemui.util.mockito.mock import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON
import com.android.systemui.statusbar.phone.StatusBarLocation
import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants
import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.JUnit4 import org.mockito.Mock
import org.mockito.MockitoAnnotations
@SmallTest @SmallTest
@RunWith(JUnit4::class) @RunWith(AndroidTestingRunner::class)
@RunWithLooper @RunWithLooper(setAsMainLooper = true)
class ModernStatusBarWifiViewTest : SysuiTestCase() { class ModernStatusBarWifiViewTest : SysuiTestCase() {
private lateinit var testableLooper: TestableLooper
@Mock
private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags
@Mock
private lateinit var logger: ConnectivityPipelineLogger
@Mock
private lateinit var constants: WifiConstants
private lateinit var connectivityRepository: FakeConnectivityRepository
private lateinit var wifiRepository: FakeWifiRepository
private lateinit var interactor: WifiInteractor
private lateinit var viewModel: WifiViewModel
private lateinit var scope: CoroutineScope
@JvmField @Rule @JvmField @Rule
val instantTaskExecutor = InstantTaskExecutorRule() val instantTaskExecutor = InstantTaskExecutorRule()
@Before @Before
fun setUp() { fun setUp() {
Assert.setTestThread(Thread.currentThread()) MockitoAnnotations.initMocks(this)
testableLooper = TestableLooper.get(this)
connectivityRepository = FakeConnectivityRepository()
wifiRepository = FakeWifiRepository()
wifiRepository.setIsWifiEnabled(true)
interactor = WifiInteractor(connectivityRepository, wifiRepository)
scope = CoroutineScope(Dispatchers.Unconfined)
viewModel = WifiViewModel(
constants, context, logger, interactor, scope, statusBarPipelineFlags
)
} }
@Test @Test
fun constructAndBind_hasCorrectSlot() { fun constructAndBind_hasCorrectSlot() {
val view = ModernStatusBarWifiView.constructAndBind( val view = ModernStatusBarWifiView.constructAndBind(
context, "slotName", mock() context, "slotName", viewModel, StatusBarLocation.HOME
) )
assertThat(view.slot).isEqualTo("slotName") assertThat(view.slot).isEqualTo("slotName")
} }
@Test
fun getVisibleState_icon_returnsIcon() {
val view = ModernStatusBarWifiView.constructAndBind(
context, SLOT_NAME, viewModel, StatusBarLocation.HOME
)
view.setVisibleState(STATE_ICON, /* animate= */ false)
assertThat(view.visibleState).isEqualTo(STATE_ICON)
}
@Test
fun getVisibleState_dot_returnsDot() {
val view = ModernStatusBarWifiView.constructAndBind(
context, SLOT_NAME, viewModel, StatusBarLocation.HOME
)
view.setVisibleState(STATE_DOT, /* animate= */ false)
assertThat(view.visibleState).isEqualTo(STATE_DOT)
}
@Test
fun getVisibleState_hidden_returnsHidden() {
val view = ModernStatusBarWifiView.constructAndBind(
context, SLOT_NAME, viewModel, StatusBarLocation.HOME
)
view.setVisibleState(STATE_HIDDEN, /* animate= */ false)
assertThat(view.visibleState).isEqualTo(STATE_HIDDEN)
}
// Note: The following tests are more like integration tests, since they stand up a full
// [WifiViewModel] and test the interactions between the view, view-binder, and view-model.
@Test
fun setVisibleState_icon_iconShownDotHidden() {
val view = ModernStatusBarWifiView.constructAndBind(
context, SLOT_NAME, viewModel, StatusBarLocation.HOME
)
view.setVisibleState(STATE_ICON, /* animate= */ false)
ViewUtils.attachView(view)
testableLooper.processAllMessages()
assertThat(view.getIconGroupView().visibility).isEqualTo(View.VISIBLE)
assertThat(view.getDotView().visibility).isEqualTo(View.GONE)
ViewUtils.detachView(view)
}
@Test
fun setVisibleState_dot_iconHiddenDotShown() {
val view = ModernStatusBarWifiView.constructAndBind(
context, SLOT_NAME, viewModel, StatusBarLocation.HOME
)
view.setVisibleState(STATE_DOT, /* animate= */ false)
ViewUtils.attachView(view)
testableLooper.processAllMessages()
assertThat(view.getIconGroupView().visibility).isEqualTo(View.GONE)
assertThat(view.getDotView().visibility).isEqualTo(View.VISIBLE)
ViewUtils.detachView(view)
}
@Test
fun setVisibleState_hidden_iconAndDotHidden() {
val view = ModernStatusBarWifiView.constructAndBind(
context, SLOT_NAME, viewModel, StatusBarLocation.HOME
)
view.setVisibleState(STATE_HIDDEN, /* animate= */ false)
ViewUtils.attachView(view)
testableLooper.processAllMessages()
assertThat(view.getIconGroupView().visibility).isEqualTo(View.GONE)
assertThat(view.getDotView().visibility).isEqualTo(View.GONE)
ViewUtils.detachView(view)
}
@Test
fun isIconVisible_notEnabled_outputsFalse() {
wifiRepository.setIsWifiEnabled(false)
wifiRepository.setWifiNetwork(
WifiNetworkModel.Active(NETWORK_ID, isValidated = true, level = 2)
)
val view = ModernStatusBarWifiView.constructAndBind(
context, SLOT_NAME, viewModel, StatusBarLocation.HOME
)
ViewUtils.attachView(view)
testableLooper.processAllMessages()
assertThat(view.isIconVisible).isFalse()
ViewUtils.detachView(view)
}
@Test
fun isIconVisible_enabled_outputsTrue() {
wifiRepository.setIsWifiEnabled(true)
wifiRepository.setWifiNetwork(
WifiNetworkModel.Active(NETWORK_ID, isValidated = true, level = 2)
)
val view = ModernStatusBarWifiView.constructAndBind(
context, SLOT_NAME, viewModel, StatusBarLocation.HOME
)
ViewUtils.attachView(view)
testableLooper.processAllMessages()
assertThat(view.isIconVisible).isTrue()
ViewUtils.detachView(view)
}
private fun View.getIconGroupView(): View {
return this.requireViewById(R.id.wifi_group)
}
private fun View.getDotView(): View {
return this.requireViewById(R.id.status_bar_dot)
}
} }
private const val SLOT_NAME = "TestSlotName"
private const val NETWORK_ID = 200

View File

@@ -29,25 +29,29 @@ import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiActivityModel
import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants 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.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel.Companion.NO_INTERNET
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito.`when` as whenever import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@SmallTest @SmallTest
class WifiViewModelTest : SysuiTestCase() { class WifiViewModelTest : SysuiTestCase() {
@@ -60,34 +64,64 @@ class WifiViewModelTest : SysuiTestCase() {
private lateinit var connectivityRepository: FakeConnectivityRepository private lateinit var connectivityRepository: FakeConnectivityRepository
private lateinit var wifiRepository: FakeWifiRepository private lateinit var wifiRepository: FakeWifiRepository
private lateinit var interactor: WifiInteractor private lateinit var interactor: WifiInteractor
private lateinit var scope: CoroutineScope
@Before @Before
fun setUp() { fun setUp() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
connectivityRepository = FakeConnectivityRepository() connectivityRepository = FakeConnectivityRepository()
wifiRepository = FakeWifiRepository() wifiRepository = FakeWifiRepository()
wifiRepository.setIsWifiEnabled(true)
interactor = WifiInteractor(connectivityRepository, wifiRepository) interactor = WifiInteractor(connectivityRepository, wifiRepository)
scope = CoroutineScope(IMMEDIATE)
createAndSetViewModel()
}
underTest = WifiViewModel( @After
statusBarPipelineFlags, fun tearDown() {
constants, scope.cancel()
context, }
logger,
interactor // 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_notEnabled_outputsNull() = runBlocking(IMMEDIATE) {
wifiRepository.setIsWifiEnabled(false)
// 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()
} }
@Test @Test
fun wifiIcon_forceHidden_outputsNull() = runBlocking(IMMEDIATE) { fun wifiIcon_forceHidden_outputsNull() = runBlocking(IMMEDIATE) {
connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.WIFI)) 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 val job = underTest
.home
.wifiIcon .wifiIcon
.onEach { latest = it } .onEach { latest = it }
.launchIn(this) .launchIn(this)
wifiRepository.setWifiNetwork(WifiNetworkModel.Active(NETWORK_ID, level = 2))
yield()
assertThat(latest).isNull() assertThat(latest).isNull()
job.cancel() job.cancel()
@@ -96,28 +130,59 @@ class WifiViewModelTest : SysuiTestCase() {
@Test @Test
fun wifiIcon_notForceHidden_outputsVisible() = runBlocking(IMMEDIATE) { fun wifiIcon_notForceHidden_outputsVisible() = runBlocking(IMMEDIATE) {
connectivityRepository.setForceHiddenIcons(setOf()) connectivityRepository.setForceHiddenIcons(setOf())
wifiRepository.setWifiNetwork(WifiNetworkModel.Active(NETWORK_ID, level = 2))
var latest: Icon? = null var latest: Icon? = null
val job = underTest val job = underTest
.home
.wifiIcon .wifiIcon
.onEach { latest = it } .onEach { latest = it }
.launchIn(this) .launchIn(this)
wifiRepository.setWifiNetwork(
WifiNetworkModel.Active(NETWORK_ID, isValidated = true, level = 2)
)
yield()
assertThat(latest).isInstanceOf(Icon.Resource::class.java) assertThat(latest).isInstanceOf(Icon.Resource::class.java)
job.cancel() job.cancel()
} }
@Test @Test
fun wifiIcon_inactiveNetwork_outputsNoNetworkIcon() = runBlocking(IMMEDIATE) { fun wifiIcon_inactiveNetwork_alwaysShowFalse_outputsNull() = runBlocking(IMMEDIATE) {
whenever(constants.alwaysShowIconIfEnabled).thenReturn(false)
createAndSetViewModel()
// 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.Inactive) wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive)
yield()
assertThat(latest).isNull()
job.cancel()
}
@Test
fun wifiIcon_inactiveNetwork_alwaysShowTrue_outputsNoNetworkIcon() = runBlocking(IMMEDIATE) {
whenever(constants.alwaysShowIconIfEnabled).thenReturn(true)
createAndSetViewModel()
var latest: Icon? = null var latest: Icon? = null
val job = underTest val job = underTest
.wifiIcon .home
.onEach { latest = it } .wifiIcon
.launchIn(this) .onEach { latest = it }
.launchIn(this)
wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive)
yield()
assertThat(latest).isInstanceOf(Icon.Resource::class.java) assertThat(latest).isInstanceOf(Icon.Resource::class.java)
val icon = latest as Icon.Resource val icon = latest as Icon.Resource
@@ -132,14 +197,22 @@ class WifiViewModelTest : SysuiTestCase() {
@Test @Test
fun wifiIcon_carrierMergedNetwork_outputsNull() = runBlocking(IMMEDIATE) { fun wifiIcon_carrierMergedNetwork_outputsNull() = runBlocking(IMMEDIATE) {
wifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged) // Even when we should always show the icon
whenever(constants.alwaysShowIconIfEnabled).thenReturn(true)
createAndSetViewModel()
var latest: Icon? = null var latest: Icon? = Icon.Resource(0, null)
val job = underTest val job = underTest
.home
.wifiIcon .wifiIcon
.onEach { latest = it } .onEach { latest = it }
.launchIn(this) .launchIn(this)
// WHEN we have a carrier merged network
wifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged)
yield()
// THEN we override the alwaysShow boolean and still don't show the icon
assertThat(latest).isNull() assertThat(latest).isNull()
job.cancel() job.cancel()
@@ -147,14 +220,22 @@ class WifiViewModelTest : SysuiTestCase() {
@Test @Test
fun wifiIcon_isActiveNullLevel_outputsNull() = runBlocking(IMMEDIATE) { fun wifiIcon_isActiveNullLevel_outputsNull() = runBlocking(IMMEDIATE) {
wifiRepository.setWifiNetwork(WifiNetworkModel.Active(NETWORK_ID, level = null)) // Even when we should always show the icon
whenever(constants.alwaysShowIconIfEnabled).thenReturn(true)
createAndSetViewModel()
var latest: Icon? = null var latest: Icon? = Icon.Resource(0, null)
val job = underTest val job = underTest
.home
.wifiIcon .wifiIcon
.onEach { latest = it } .onEach { latest = it }
.launchIn(this) .launchIn(this)
// WHEN we have a null level
wifiRepository.setWifiNetwork(WifiNetworkModel.Active(NETWORK_ID, level = null))
yield()
// THEN we override the alwaysShow boolean and still don't show the icon
assertThat(latest).isNull() assertThat(latest).isNull()
job.cancel() job.cancel()
@@ -162,22 +243,23 @@ class WifiViewModelTest : SysuiTestCase() {
@Test @Test
fun wifiIcon_isActiveAndValidated_level1_outputsFull1Icon() = runBlocking(IMMEDIATE) { fun wifiIcon_isActiveAndValidated_level1_outputsFull1Icon() = runBlocking(IMMEDIATE) {
val level = 1
wifiRepository.setWifiNetwork(
WifiNetworkModel.Active(
NETWORK_ID,
isValidated = true,
level = level
)
)
var latest: Icon? = null var latest: Icon? = null
val job = underTest val job = underTest
.home
.wifiIcon .wifiIcon
.onEach { latest = it } .onEach { latest = it }
.launchIn(this) .launchIn(this)
val level = 1
wifiRepository.setWifiNetwork(
WifiNetworkModel.Active(
NETWORK_ID,
isValidated = true,
level,
)
)
yield()
assertThat(latest).isInstanceOf(Icon.Resource::class.java) assertThat(latest).isInstanceOf(Icon.Resource::class.java)
val icon = latest as Icon.Resource val icon = latest as Icon.Resource
assertThat(icon.res).isEqualTo(WIFI_FULL_ICONS[level]) assertThat(icon.res).isEqualTo(WIFI_FULL_ICONS[level])
@@ -190,23 +272,49 @@ class WifiViewModelTest : SysuiTestCase() {
} }
@Test @Test
fun wifiIcon_isActiveAndNotValidated_level4_outputsEmpty4Icon() = runBlocking(IMMEDIATE) { fun wifiIcon_isActiveAndNotValidated_alwaysShowFalse_outputsNull() = runBlocking(IMMEDIATE) {
val level = 4 whenever(constants.alwaysShowIconIfEnabled).thenReturn(false)
createAndSetViewModel()
wifiRepository.setWifiNetwork( var latest: Icon? = Icon.Resource(0, null)
WifiNetworkModel.Active(
NETWORK_ID,
isValidated = false,
level = level
)
)
var latest: Icon? = null
val job = underTest val job = underTest
.home
.wifiIcon .wifiIcon
.onEach { latest = it } .onEach { latest = it }
.launchIn(this) .launchIn(this)
wifiRepository.setWifiNetwork(
WifiNetworkModel.Active(NETWORK_ID, isValidated = false, level = 4,)
)
yield()
assertThat(latest).isNull()
job.cancel()
}
@Test
fun wifiIcon_isActiveAndNotValidated_alwaysShowTrue_outputsIcon() = runBlocking(IMMEDIATE) {
whenever(constants.alwaysShowIconIfEnabled).thenReturn(true)
createAndSetViewModel()
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) assertThat(latest).isInstanceOf(Icon.Resource::class.java)
val icon = latest as Icon.Resource val icon = latest as Icon.Resource
assertThat(icon.res).isEqualTo(WIFI_NO_INTERNET_ICONS[level]) assertThat(icon.res).isEqualTo(WIFI_NO_INTERNET_ICONS[level])
@@ -219,68 +327,398 @@ class WifiViewModelTest : SysuiTestCase() {
} }
@Test @Test
fun activityInVisible_showActivityConfigFalse_outputsFalse() = runBlocking(IMMEDIATE) { fun wifiIcon_allLocationViewModelsReceiveSameData() = runBlocking(IMMEDIATE) {
whenever(constants.shouldShowActivityConfig).thenReturn(false) var latestHome: Icon? = null
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) val jobHome = underTest
.home
.wifiIcon
.onEach { latestHome = it }
.launchIn(this)
var latest: Boolean? = null var latestKeyguard: Icon? = null
val job = underTest val jobKeyguard = underTest
.isActivityInVisible .keyguard
.onEach { latest = it } .wifiIcon
.launchIn(this) .onEach { latestKeyguard = it }
.launchIn(this)
// Verify that on launch, we receive a false. var latestQs: Icon? = null
assertThat(latest).isFalse() val jobQs = underTest
.qs
.wifiIcon
.onEach { latestQs = it }
.launchIn(this)
job.cancel() wifiRepository.setWifiNetwork(
} WifiNetworkModel.Active(
NETWORK_ID,
@Test isValidated = true,
fun activityInVisible_showActivityConfigFalse_noUpdatesReceived() = runBlocking(IMMEDIATE) { level = 1
whenever(constants.shouldShowActivityConfig).thenReturn(false) )
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
var latest: Boolean? = null
val job = underTest
.isActivityInVisible
.onEach { latest = it }
.launchIn(this)
// Update the repo to have activityIn
wifiRepository.setWifiActivity(
WifiActivityModel(hasActivityIn = true, hasActivityOut = false)
) )
yield() yield()
// Verify that we didn't update to activityIn=true (because our config is false) assertThat(latestHome).isInstanceOf(Icon.Resource::class.java)
assertThat(latest).isFalse() assertThat(latestHome).isEqualTo(latestKeyguard)
assertThat(latestKeyguard).isEqualTo(latestQs)
job.cancel() jobHome.cancel()
jobKeyguard.cancel()
jobQs.cancel()
} }
@Test @Test
fun activityInVisible_showActivityConfigTrue_outputsUpdate() = runBlocking(IMMEDIATE) { fun activity_showActivityConfigFalse_outputsFalse() = runBlocking(IMMEDIATE) {
whenever(constants.shouldShowActivityConfig).thenReturn(false)
createAndSetViewModel()
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
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)
// Verify that on launch, we receive false.
assertThat(activityIn).isFalse()
assertThat(activityOut).isFalse()
assertThat(activityContainer).isFalse()
activityInJob.cancel()
activityOutJob.cancel()
activityContainerJob.cancel()
}
@Test
fun activity_showActivityConfigFalse_noUpdatesReceived() = runBlocking(IMMEDIATE) {
whenever(constants.shouldShowActivityConfig).thenReturn(false)
createAndSetViewModel()
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
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)
// WHEN we update the repo to have activity
val activity = WifiActivityModel(hasActivityIn = true, hasActivityOut = true)
wifiRepository.setWifiActivity(activity)
yield()
// THEN we didn't update to the new activity (because our config is false)
assertThat(activityIn).isFalse()
assertThat(activityOut).isFalse()
assertThat(activityContainer).isFalse()
activityInJob.cancel()
activityOutJob.cancel()
activityContainerJob.cancel()
}
@Test
fun activity_nullSsid_outputsFalse() = runBlocking(IMMEDIATE) {
whenever(constants.shouldShowActivityConfig).thenReturn(true) whenever(constants.shouldShowActivityConfig).thenReturn(true)
createAndSetViewModel()
wifiRepository.setWifiNetwork(WifiNetworkModel.Active(NETWORK_ID, ssid = null))
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)
// WHEN we update the repo to have activity
val activity = WifiActivityModel(hasActivityIn = true, hasActivityOut = true)
wifiRepository.setWifiActivity(activity)
yield()
// THEN we still output false because our network's SSID is null
assertThat(activityIn).isFalse()
assertThat(activityOut).isFalse()
assertThat(activityContainer).isFalse()
activityInJob.cancel()
activityOutJob.cancel()
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)
createAndSetViewModel()
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK) wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
var latest: Boolean? = null var latest: Boolean? = null
val job = underTest val job = underTest
.isActivityInVisible .home
.onEach { latest = it } .isActivityInViewVisible
.launchIn(this) .onEach { latest = it }
.launchIn(this)
// Update the repo to have activityIn val activity = WifiActivityModel(hasActivityIn = true, hasActivityOut = false)
wifiRepository.setWifiActivity( wifiRepository.setWifiActivity(activity)
WifiActivityModel(hasActivityIn = true, hasActivityOut = false)
)
yield() yield()
// Verify that we updated to activityIn=true
assertThat(latest).isTrue() assertThat(latest).isTrue()
job.cancel() job.cancel()
} }
@Test
fun activityIn_hasActivityInFalse_outputsFalse() = runBlocking(IMMEDIATE) {
whenever(constants.shouldShowActivityConfig).thenReturn(true)
createAndSetViewModel()
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
var latest: Boolean? = null
val job = underTest
.home
.isActivityInViewVisible
.onEach { latest = it }
.launchIn(this)
val activity = WifiActivityModel(hasActivityIn = false, hasActivityOut = true)
wifiRepository.setWifiActivity(activity)
yield()
assertThat(latest).isFalse()
job.cancel()
}
@Test
fun activityOut_hasActivityOutTrue_outputsTrue() = runBlocking(IMMEDIATE) {
whenever(constants.shouldShowActivityConfig).thenReturn(true)
createAndSetViewModel()
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
var latest: Boolean? = null
val job = underTest
.home
.isActivityOutViewVisible
.onEach { latest = it }
.launchIn(this)
val activity = WifiActivityModel(hasActivityIn = false, hasActivityOut = true)
wifiRepository.setWifiActivity(activity)
yield()
assertThat(latest).isTrue()
job.cancel()
}
@Test
fun activityOut_hasActivityOutFalse_outputsFalse() = runBlocking(IMMEDIATE) {
whenever(constants.shouldShowActivityConfig).thenReturn(true)
createAndSetViewModel()
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
var latest: Boolean? = null
val job = underTest
.home
.isActivityOutViewVisible
.onEach { latest = it }
.launchIn(this)
val activity = WifiActivityModel(hasActivityIn = true, hasActivityOut = false)
wifiRepository.setWifiActivity(activity)
yield()
assertThat(latest).isFalse()
job.cancel()
}
@Test
fun activityContainer_hasActivityInTrue_outputsTrue() = runBlocking(IMMEDIATE) {
whenever(constants.shouldShowActivityConfig).thenReturn(true)
createAndSetViewModel()
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
var latest: Boolean? = null
val job = underTest
.home
.isActivityContainerVisible
.onEach { latest = it }
.launchIn(this)
val activity = WifiActivityModel(hasActivityIn = true, hasActivityOut = false)
wifiRepository.setWifiActivity(activity)
yield()
assertThat(latest).isTrue()
job.cancel()
}
@Test
fun activityContainer_hasActivityOutTrue_outputsTrue() = runBlocking(IMMEDIATE) {
whenever(constants.shouldShowActivityConfig).thenReturn(true)
createAndSetViewModel()
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
var latest: Boolean? = null
val job = underTest
.home
.isActivityContainerVisible
.onEach { latest = it }
.launchIn(this)
val activity = WifiActivityModel(hasActivityIn = false, hasActivityOut = true)
wifiRepository.setWifiActivity(activity)
yield()
assertThat(latest).isTrue()
job.cancel()
}
@Test
fun activityContainer_inAndOutTrue_outputsTrue() = runBlocking(IMMEDIATE) {
whenever(constants.shouldShowActivityConfig).thenReturn(true)
createAndSetViewModel()
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
var latest: Boolean? = null
val job = underTest
.home
.isActivityContainerVisible
.onEach { latest = it }
.launchIn(this)
val activity = WifiActivityModel(hasActivityIn = true, hasActivityOut = true)
wifiRepository.setWifiActivity(activity)
yield()
assertThat(latest).isTrue()
job.cancel()
}
@Test
fun activityContainer_inAndOutFalse_outputsFalse() = runBlocking(IMMEDIATE) {
whenever(constants.shouldShowActivityConfig).thenReturn(true)
createAndSetViewModel()
wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
var latest: Boolean? = null
val job = underTest
.home
.isActivityContainerVisible
.onEach { latest = it }
.launchIn(this)
val activity = WifiActivityModel(hasActivityIn = false, hasActivityOut = false)
wifiRepository.setWifiActivity(activity)
yield()
assertThat(latest).isFalse()
job.cancel()
}
private fun createAndSetViewModel() {
// [WifiViewModel] creates its flows as soon as it's instantiated, and some of those flow
// creations rely on certain config values that we mock out in individual tests. This method
// allows tests to create the view model only after those configs are correctly set up.
underTest = WifiViewModel(
constants,
context,
logger,
interactor,
scope,
statusBarPipelineFlags,
)
}
private fun ContentDescription.getAsString(): String? { private fun ContentDescription.getAsString(): String? {
return when (this) { return when (this) {
is ContentDescription.Loaded -> this.description is ContentDescription.Loaded -> this.description