Merge changes I723cfc04,I1eba794e,I9d421a4a,Icd9c2b14,Idee192ff, ... into tm-qpr-dev am: d547dfa98e am: ea4878748f

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/20067345

Change-Id: I9faa24e536c3bc43a44f47cdfd69de954d962736
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml
index 8084254..f22e797 100644
--- a/packages/SystemUI/res/values/ids.xml
+++ b/packages/SystemUI/res/values/ids.xml
@@ -131,6 +131,9 @@
     <!-- For StatusIconContainer to tag its icon views -->
     <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 -->
     <item type="id" name="display_cutout" />
     <item type="id" name="display_cutout_left" />
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java
index b585961..ccaab1a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java
@@ -30,6 +30,7 @@
 import com.android.systemui.qs.dagger.QSScope;
 import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider;
 import com.android.systemui.statusbar.phone.StatusBarIconController;
+import com.android.systemui.statusbar.phone.StatusBarLocation;
 import com.android.systemui.statusbar.phone.StatusIconContainer;
 import com.android.systemui.statusbar.policy.Clock;
 import com.android.systemui.statusbar.policy.VariableDateViewController;
@@ -104,7 +105,7 @@
                 mView.requireViewById(R.id.date_clock)
         );
 
-        mIconManager = tintedIconManagerFactory.create(mIconContainer);
+        mIconManager = tintedIconManagerFactory.create(mIconContainer, StatusBarLocation.QS);
         mDemoModeReceiver = new ClockDemoModeReceiver(mClockView);
         mColorExtractor = colorExtractor;
         mOnColorsChangedListener = (extractor, which) -> {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
index fe40d4c..d3ed474 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/LargeScreenShadeHeaderController.kt
@@ -48,6 +48,7 @@
 import com.android.systemui.shade.LargeScreenShadeHeaderController.Companion.QS_HEADER_CONSTRAINT
 import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
 import com.android.systemui.statusbar.phone.StatusBarIconController
+import com.android.systemui.statusbar.phone.StatusBarLocation
 import com.android.systemui.statusbar.phone.StatusIconContainer
 import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent.CentralSurfacesScope
 import com.android.systemui.statusbar.phone.dagger.StatusBarViewModule.LARGE_SCREEN_BATTERY_CONTROLLER
@@ -261,7 +262,7 @@
         batteryMeterViewController.ignoreTunerUpdates()
         batteryIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE)
 
-        iconManager = tintedIconManagerFactory.create(iconContainer)
+        iconManager = tintedIconManagerFactory.create(iconContainer, StatusBarLocation.QS)
         iconManager.setTint(
             Utils.getColorAttrDefaultColor(header.context, android.R.attr.textColorPrimary)
         )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
index 827d0d0f..c35c5c5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
@@ -22,6 +22,7 @@
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator;
+import android.annotation.IntDef;
 import android.app.ActivityManager;
 import android.app.Notification;
 import android.content.Context;
@@ -59,6 +60,8 @@
 import com.android.systemui.statusbar.notification.NotificationUtils;
 import com.android.systemui.util.drawable.DrawableSize;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.text.NumberFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -86,6 +89,10 @@
     public static final int STATE_DOT = 1;
     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 Property<StatusBarIconView, Float> ICON_APPEAR_AMOUNT
             = new FloatProperty<StatusBarIconView>("iconAppearAmount") {
@@ -133,6 +140,7 @@
     private final Paint mDotPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
     private float mDotRadius;
     private int mStaticDotRadius;
+    @StatusBarIconView.VisibleState
     private int mVisibleState = STATE_ICON;
     private float mIconAppearAmount = 1.0f;
     private ObjectAnimator mIconAppearAnimator;
@@ -746,11 +754,12 @@
     }
 
     @Override
-    public void setVisibleState(int state) {
+    public void setVisibleState(@StatusBarIconView.VisibleState int state) {
         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);
     }
 
@@ -862,6 +871,7 @@
         return mIconAppearAmount;
     }
 
+    @StatusBarIconView.VisibleState
     public int getVisibleState() {
         return mVisibleState;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarMobileView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarMobileView.java
index 25c6dce..48c6e27 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarMobileView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarMobileView.java
@@ -59,7 +59,8 @@
     private ImageView mOut;
     private ImageView mMobile, mMobileType, mMobileRoaming;
     private View mMobileRoamingSpace;
-    private int mVisibleState = -1;
+    @StatusBarIconView.VisibleState
+    private int mVisibleState = STATE_HIDDEN;
     private DualToneHandler mDualToneHandler;
     private boolean mForceHidden;
 
@@ -271,7 +272,7 @@
     }
 
     @Override
-    public void setVisibleState(int state, boolean animate) {
+    public void setVisibleState(@StatusBarIconView.VisibleState int state, boolean animate) {
         if (state == mVisibleState) {
             return;
         }
@@ -312,6 +313,7 @@
     }
 
     @Override
+    @StatusBarIconView.VisibleState
     public int getVisibleState() {
         return mVisibleState;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarWifiView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarWifiView.java
index 5aee62e..f3e74d9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarWifiView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarWifiView.java
@@ -55,7 +55,8 @@
     private View mAirplaneSpacer;
     private WifiIconState mState;
     private String mSlot;
-    private int mVisibleState = -1;
+    @StatusBarIconView.VisibleState
+    private int mVisibleState = STATE_HIDDEN;
 
     public static StatusBarWifiView fromContext(Context context, String slot) {
         LayoutInflater inflater = LayoutInflater.from(context);
@@ -107,7 +108,7 @@
     }
 
     @Override
-    public void setVisibleState(int state, boolean animate) {
+    public void setVisibleState(@StatusBarIconView.VisibleState int state, boolean animate) {
         if (state == mVisibleState) {
             return;
         }
@@ -131,6 +132,7 @@
     }
 
     @Override
+    @StatusBarIconView.VisibleState
     public int getVisibleState() {
         return mVisibleState;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusIconDisplayable.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusIconDisplayable.java
index d541fae..1196211 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusIconDisplayable.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusIconDisplayable.java
@@ -22,14 +22,32 @@
     String getSlot();
     void setStaticDrawableColor(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);
     }
-    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();
+
+    /**
+     * 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();
+
     default boolean isIconBlocked() {
         return false;
     }
 }
-
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
index ce2c9c2..0026b71 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
@@ -352,8 +352,8 @@
         mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback);
         mDisableStateTracker.startTracking(mCommandQueue, mView.getDisplay().getDisplayId());
         if (mTintedIconManager == null) {
-            mTintedIconManager =
-                    mTintedIconManagerFactory.create(mView.findViewById(R.id.statusIcons));
+            mTintedIconManager = mTintedIconManagerFactory.create(
+                    mView.findViewById(R.id.statusIcons), StatusBarLocation.KEYGUARD);
             mTintedIconManager.setBlockList(getBlockedIcons());
             mStatusBarIconController.addIconGroup(mTintedIconManager);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
index bd99713..d6d021f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
@@ -56,7 +56,6 @@
 import java.util.List;
 
 import javax.inject.Inject;
-import javax.inject.Provider;
 
 public interface StatusBarIconController {
 
@@ -139,13 +138,15 @@
 
         public DarkIconManager(
                 LinearLayout linearLayout,
+                StatusBarLocation location,
                 StatusBarPipelineFlags statusBarPipelineFlags,
-                Provider<WifiViewModel> wifiViewModelProvider,
+                WifiViewModel wifiViewModel,
                 MobileContextProvider mobileContextProvider,
                 DarkIconDispatcher darkIconDispatcher) {
             super(linearLayout,
+                    location,
                     statusBarPipelineFlags,
-                    wifiViewModelProvider,
+                    wifiViewModel,
                     mobileContextProvider);
             mIconHPadding = mContext.getResources().getDimensionPixelSize(
                     R.dimen.status_bar_icon_padding);
@@ -204,27 +205,28 @@
         @SysUISingleton
         public static class Factory {
             private final StatusBarPipelineFlags mStatusBarPipelineFlags;
-            private final Provider<WifiViewModel> mWifiViewModelProvider;
+            private final WifiViewModel mWifiViewModel;
             private final MobileContextProvider mMobileContextProvider;
             private final DarkIconDispatcher mDarkIconDispatcher;
 
             @Inject
             public Factory(
                     StatusBarPipelineFlags statusBarPipelineFlags,
-                    Provider<WifiViewModel> wifiViewModelProvider,
+                    WifiViewModel wifiViewModel,
                     MobileContextProvider mobileContextProvider,
                     DarkIconDispatcher darkIconDispatcher) {
                 mStatusBarPipelineFlags = statusBarPipelineFlags;
-                mWifiViewModelProvider = wifiViewModelProvider;
+                mWifiViewModel = wifiViewModel;
                 mMobileContextProvider = mobileContextProvider;
                 mDarkIconDispatcher = darkIconDispatcher;
             }
 
-            public DarkIconManager create(LinearLayout group) {
+            public DarkIconManager create(LinearLayout group, StatusBarLocation location) {
                 return new DarkIconManager(
                         group,
+                        location,
                         mStatusBarPipelineFlags,
-                        mWifiViewModelProvider,
+                        mWifiViewModel,
                         mMobileContextProvider,
                         mDarkIconDispatcher);
             }
@@ -239,12 +241,14 @@
 
         public TintedIconManager(
                 ViewGroup group,
+                StatusBarLocation location,
                 StatusBarPipelineFlags statusBarPipelineFlags,
-                Provider<WifiViewModel> wifiViewModelProvider,
+                WifiViewModel wifiViewModel,
                 MobileContextProvider mobileContextProvider) {
             super(group,
+                    location,
                     statusBarPipelineFlags,
-                    wifiViewModelProvider,
+                    wifiViewModel,
                     mobileContextProvider);
         }
 
@@ -278,24 +282,25 @@
         @SysUISingleton
         public static class Factory {
             private final StatusBarPipelineFlags mStatusBarPipelineFlags;
-            private final Provider<WifiViewModel> mWifiViewModelProvider;
+            private final WifiViewModel mWifiViewModel;
             private final MobileContextProvider mMobileContextProvider;
 
             @Inject
             public Factory(
                     StatusBarPipelineFlags statusBarPipelineFlags,
-                    Provider<WifiViewModel> wifiViewModelProvider,
+                    WifiViewModel wifiViewModel,
                     MobileContextProvider mobileContextProvider) {
                 mStatusBarPipelineFlags = statusBarPipelineFlags;
-                mWifiViewModelProvider = wifiViewModelProvider;
+                mWifiViewModel = wifiViewModel;
                 mMobileContextProvider = mobileContextProvider;
             }
 
-            public TintedIconManager create(ViewGroup group) {
+            public TintedIconManager create(ViewGroup group, StatusBarLocation location) {
                 return new TintedIconManager(
                         group,
+                        location,
                         mStatusBarPipelineFlags,
-                        mWifiViewModelProvider,
+                        mWifiViewModel,
                         mMobileContextProvider);
             }
         }
@@ -306,8 +311,9 @@
      */
     class IconManager implements DemoModeCommandReceiver {
         protected final ViewGroup mGroup;
+        private final StatusBarLocation mLocation;
         private final StatusBarPipelineFlags mStatusBarPipelineFlags;
-        private final Provider<WifiViewModel> mWifiViewModelProvider;
+        private final WifiViewModel mWifiViewModel;
         private final MobileContextProvider mMobileContextProvider;
         protected final Context mContext;
         protected final int mIconSize;
@@ -324,12 +330,14 @@
 
         public IconManager(
                 ViewGroup group,
+                StatusBarLocation location,
                 StatusBarPipelineFlags statusBarPipelineFlags,
-                Provider<WifiViewModel> wifiViewModelProvider,
+                WifiViewModel wifiViewModel,
                 MobileContextProvider mobileContextProvider) {
             mGroup = group;
+            mLocation = location;
             mStatusBarPipelineFlags = statusBarPipelineFlags;
-            mWifiViewModelProvider = wifiViewModelProvider;
+            mWifiViewModel = wifiViewModel;
             mMobileContextProvider = mobileContextProvider;
             mContext = group.getContext();
             mIconSize = mContext.getResources().getDimensionPixelSize(
@@ -446,7 +454,7 @@
 
         private ModernStatusBarWifiView onCreateModernStatusBarWifiView(String slot) {
             return ModernStatusBarWifiView.constructAndBind(
-                    mContext, slot, mWifiViewModelProvider.get());
+                    mContext, slot, mWifiViewModel, mLocation);
         }
 
         private StatusBarMobileView onCreateStatusBarMobileView(int subId, String slot) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiActivityModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLocation.kt
similarity index 64%
copy from packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiActivityModel.kt
copy to packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLocation.kt
index 44c0496..5ace226 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiActivityModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLocation.kt
@@ -14,14 +14,14 @@
  * limitations under the License.
  */
 
-package com.android.systemui.statusbar.pipeline.wifi.data.model
+package com.android.systemui.statusbar.phone
 
-/**
- * Provides information on the current wifi activity.
- */
-data class WifiActivityModel(
-    /** True if the wifi has activity in (download). */
-    val hasActivityIn: Boolean,
-    /** True if the wifi has activity out (upload). */
-    val hasActivityOut: Boolean,
-)
+/** 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,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
index 891f657..b8bdc7d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
@@ -64,6 +64,7 @@
 import com.android.systemui.statusbar.phone.StatusBarHideIconsForBouncerManager;
 import com.android.systemui.statusbar.phone.StatusBarIconController;
 import com.android.systemui.statusbar.phone.StatusBarIconController.DarkIconManager;
+import com.android.systemui.statusbar.phone.StatusBarLocation;
 import com.android.systemui.statusbar.phone.StatusBarLocationPublisher;
 import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent;
 import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent.Startable;
@@ -235,7 +236,8 @@
             mStatusBar.restoreHierarchyState(
                     savedInstanceState.getSparseParcelableArray(EXTRA_PANEL_STATE));
         }
-        mDarkIconManager = mDarkIconManagerFactory.create(view.findViewById(R.id.statusIcons));
+        mDarkIconManager = mDarkIconManagerFactory.create(
+                view.findViewById(R.id.statusIcons), StatusBarLocation.HOME);
         mDarkIconManager.setShouldLog(true);
         updateBlockedIcons();
         mStatusBarIconController.addIconGroup(mDarkIconManager);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
index 88d8a86..dbb1aa5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
@@ -33,6 +33,20 @@
 ) {
     /**
      * 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.
      */
     fun logInputChange(callbackName: String, changeInfo: String?) {
         buffer.log(
@@ -128,12 +142,36 @@
         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.
          *
          * @param prettyPrint an optional function to transform the value into a readable string.
          *   [toString] is used if no custom function is provided.
          */
-        fun <T : Any> Flow<T>.logOutputChange(
+        fun <T> Flow<T>.logOutputChange(
                 logger: ConnectivityPipelineLogger,
                 outputParamName: String,
                 prettyPrint: (T) -> String = { it.toString() }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
index 103f3fc..681cf72 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepository.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.pipeline.wifi.data.repository
 
 import android.annotation.SuppressLint
+import android.content.IntentFilter
 import android.net.ConnectivityManager
 import android.net.Network
 import android.net.NetworkCapabilities
@@ -30,51 +31,87 @@
 import android.net.wifi.WifiManager.TrafficStateCallback
 import android.util.Log
 import com.android.settingslib.Utils
+import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Main
 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.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.shared.model.WifiActivityModel
 import java.util.concurrent.Executor
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
 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.mapLatest
+import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.stateIn
 
-/**
- * Provides data related to the wifi state.
- */
+/** Provides data related to the wifi state. */
 interface WifiRepository {
-    /**
-     * Observable for the current wifi network.
-     */
-    val wifiNetwork: Flow<WifiNetworkModel>
+    /** Observable for the current wifi enabled status. */
+    val isWifiEnabled: StateFlow<Boolean>
 
-    /**
-     * Observable for the current wifi network activity.
-     */
-    val wifiActivity: Flow<WifiActivityModel>
+    /** Observable for the current wifi network. */
+    val wifiNetwork: StateFlow<WifiNetworkModel>
+
+    /** Observable for the current wifi network activity. */
+    val wifiActivity: StateFlow<WifiActivityModel>
 }
 
 /** Real implementation of [WifiRepository]. */
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
 @OptIn(ExperimentalCoroutinesApi::class)
 @SysUISingleton
 @SuppressLint("MissingPermission")
 class WifiRepositoryImpl @Inject constructor(
+    broadcastDispatcher: BroadcastDispatcher,
     connectivityManager: ConnectivityManager,
     logger: ConnectivityPipelineLogger,
     @Main mainExecutor: Executor,
     @Application scope: CoroutineScope,
     wifiManager: WifiManager?,
 ) : 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
 
         val callback = object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
@@ -84,6 +121,8 @@
             ) {
                 logger.logOnCapabilitiesChanged(network, networkCapabilities)
 
+                wifiNetworkChangeEvents.tryEmit(Unit)
+
                 val wifiInfo = networkCapabilitiesToWifiInfo(networkCapabilities)
                 if (wifiInfo?.isPrimary == true) {
                     val wifiNetworkModel = createWifiNetworkModel(
@@ -104,6 +143,9 @@
 
             override fun onLost(network: Network) {
                 logger.logOnLost(network)
+
+                wifiNetworkChangeEvents.tryEmit(Unit)
+
                 val wifi = currentWifi
                 if (wifi is WifiNetworkModel.Active && wifi.networkId == network.getNetId()) {
                     val newNetworkModel = WifiNetworkModel.Inactive
@@ -132,7 +174,7 @@
             initialValue = WIFI_NETWORK_DEFAULT
         )
 
-    override val wifiActivity: Flow<WifiActivityModel> =
+    override val wifiActivity: StateFlow<WifiActivityModel> =
             if (wifiManager == null) {
                 Log.w(SB_LOGGING_TAG, "Null WifiManager; skipping activity callback")
                 flowOf(ACTIVITY_DEFAULT)
@@ -142,13 +184,15 @@
                         logger.logInputChange("onTrafficStateChange", prettyPrintActivity(state))
                         trySend(trafficStateToWifiActivityModel(state))
                     }
-
-                    trySend(ACTIVITY_DEFAULT)
                     wifiManager.registerTrafficStateCallback(mainExecutor, callback)
-
                     awaitClose { wifiManager.unregisterTrafficStateCallback(callback) }
                 }
             }
+                .stateIn(
+                    scope,
+                    started = SharingStarted.WhileSubscribed(),
+                    initialValue = ACTIVITY_DEFAULT
+                )
 
     companion object {
         val ACTIVITY_DEFAULT = WifiActivityModel(hasActivityIn = false, hasActivityOut = false)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
index 952525d..04b17ed 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractor.kt
@@ -22,9 +22,10 @@
 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.repository.WifiRepository
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.map
 
 /**
@@ -38,7 +39,11 @@
     connectivityRepository: ConnectivityRepository,
     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) {
             is WifiNetworkModel.Inactive -> null
             is WifiNetworkModel.CarrierMerged -> null
@@ -51,17 +56,17 @@
         }
     }
 
+    /** Our current enabled status. */
+    val isEnabled: Flow<Boolean> = wifiRepository.isWifiEnabled
+
     /** Our current wifi network. See [WifiNetworkModel]. */
     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. */
     val isForceHidden: Flow<Boolean> = connectivityRepository.forceHiddenSlots.map {
         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
-        }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt
index a19d1bd..0eb4b0d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/WifiConstants.kt
@@ -41,9 +41,14 @@
     /** True if we should show the activityIn/activityOut icons and false otherwise. */
     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>) {
         pw.apply {
             println("shouldShowActivityConfig=$shouldShowActivityConfig")
+            println("alwaysShowIconIfEnabled=$alwaysShowIconIfEnabled")
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiActivityModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiActivityModel.kt
similarity index 86%
rename from packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiActivityModel.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiActivityModel.kt
index 44c0496..5746106 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiActivityModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/shared/model/WifiActivityModel.kt
@@ -14,11 +14,9 @@
  * 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(
     /** True if the wifi has activity in (download). */
     val hasActivityIn: Boolean,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt
index 4fad327..273be63 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt
@@ -26,8 +26,15 @@
 import com.android.systemui.R
 import com.android.systemui.common.ui.binder.IconViewBinder
 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 kotlinx.coroutines.InternalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.launch
@@ -41,40 +48,111 @@
  */
 @OptIn(InternalCoroutinesApi::class)
 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
     fun bind(
         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 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
         iconView.isVisible = true
 
+        // TODO(b/238425913): We should log this visibility state.
+        @StatusBarIconView.VisibleState
+        val visibilityState: MutableStateFlow<Int> = MutableStateFlow(STATE_HIDDEN)
+
         view.repeatWhenAttached {
             repeatOnLifecycle(Lifecycle.State.STARTED) {
                 launch {
-                    viewModel.wifiIcon.distinctUntilChanged().collect { wifiIcon ->
-                        // TODO(b/238425913): Right now, if !isVisible, there's just an empty space
-                        //  where the wifi icon would be. We need to pipe isVisible through to
-                        //   [ModernStatusBarWifiView.isIconVisible], which is what actually makes
-                        //   the view GONE.
+                    visibilityState.collect { visibilityState ->
+                        groupView.isVisible = visibilityState == STATE_ICON
+                        dotView.isVisible = visibilityState == STATE_DOT
+                    }
+                }
+
+                launch {
+                    viewModel.wifiIcon.collect { wifiIcon ->
                         view.isVisible = wifiIcon != null
-                        wifiIcon?.let {
-                            IconViewBinder.bind(wifiIcon, iconView)
-                        }
+                        wifiIcon?.let { IconViewBinder.bind(wifiIcon, iconView) }
                     }
                 }
 
                 launch {
                     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
+            }
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt
index c14a897..6c616ac 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt
@@ -19,10 +19,14 @@
 import android.content.Context
 import android.graphics.Rect
 import android.util.AttributeSet
+import android.view.Gravity
 import android.view.LayoutInflater
 import com.android.systemui.R
 import com.android.systemui.statusbar.BaseStatusBarWifiView
-import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON
+import com.android.systemui.statusbar.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.viewmodel.WifiViewModel
 
@@ -36,6 +40,17 @@
 ) : BaseStatusBarWifiView(context, attrs) {
 
     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) {
         // TODO(b/238425913)
@@ -51,42 +66,64 @@
         // TODO(b/238425913)
     }
 
-    override fun setVisibleState(state: Int, animate: Boolean) {
-        // TODO(b/238425913)
+    override fun setVisibleState(@StatusBarIconView.VisibleState state: Int, animate: Boolean) {
+        iconVisibleState = state
     }
 
+    @StatusBarIconView.VisibleState
     override fun getVisibleState(): Int {
-        // TODO(b/238425913)
-        return STATE_ICON
+        return iconVisibleState
     }
 
     override fun isIconVisible(): Boolean {
-        // TODO(b/238425913)
-        return true
+        return binding.getShouldIconBeVisible()
     }
 
-    /** Set the slot name for this view. */
-    private fun setSlot(slotName: String) {
-        this.slot = slotName
+    private fun initView(
+        slotName: String,
+        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 {
         /**
-         * Inflates a new instance of [ModernStatusBarWifiView], binds it to [viewModel], and
+         * Inflates a new instance of [ModernStatusBarWifiView], binds it to a view model, and
          * returns it.
          */
         @JvmStatic
         fun constructAndBind(
             context: Context,
             slot: String,
-            viewModel: WifiViewModel,
+            wifiViewModel: WifiViewModel,
+            location: StatusBarLocation,
         ): ModernStatusBarWifiView {
             return (
                 LayoutInflater.from(context).inflate(R.layout.new_status_bar_wifi_group, null)
                     as ModernStatusBarWifiView
                 ).also {
-                    it.setSlot(slot)
-                    WifiViewBinder.bind(it, viewModel)
+                    it.initView(slot, wifiViewModel, location)
                 }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt
new file mode 100644
index 0000000..871b395
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/HomeWifiViewModel.kt
@@ -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,
+    )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt
new file mode 100644
index 0000000..be1f3f2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/KeyguardWifiViewModel.kt
@@ -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,
+    )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt
new file mode 100644
index 0000000..7243acf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/LocationBasedWifiViewModel.kt
@@ -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
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt
new file mode 100644
index 0000000..d640d33
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/QsWifiViewModel.kt
@@ -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,
+    )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
index 3c243ac..47347a2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel
 
 import android.content.Context
-import android.graphics.Color
 import androidx.annotation.DrawableRes
 import androidx.annotation.StringRes
 import androidx.annotation.VisibleForTesting
@@ -26,6 +25,8 @@
 import com.android.systemui.R
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS
 import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_INTERNET_ICONS
 import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_NO_NETWORK
@@ -35,98 +36,171 @@
 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.shared.WifiConstants
+import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiActivityModel
 import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
 
 /**
  * Models the UI state for the status bar wifi icon.
+ *
+ * This class exposes three view models, one per status bar location:
+ *  - [home]
+ *  - [keyguard]
+ *  - [qs]
+ *  In order to get the UI state for the wifi icon, you must use one of those view models (whichever
+ *  is correct for your location).
+ *
+ * Internally, this class maintains the current state of the wifi icon and notifies those three
+ * view models of any changes.
  */
-class WifiViewModel @Inject constructor(
-    statusBarPipelineFlags: StatusBarPipelineFlags,
-    private val constants: WifiConstants,
+@SysUISingleton
+class WifiViewModel
+@Inject
+constructor(
+    constants: WifiConstants,
     private val context: Context,
-    private val logger: ConnectivityPipelineLogger,
-    private val interactor: WifiInteractor,
+    logger: ConnectivityPipelineLogger,
+    interactor: WifiInteractor,
+    @Application private val scope: CoroutineScope,
+    statusBarPipelineFlags: StatusBarPipelineFlags,
 ) {
     /**
-     * The drawable resource ID to use for the wifi icon. Null if we shouldn't display any icon.
+     * 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
-    private val iconResId: Flow<Int?> = interactor.wifiNetwork.map {
-        when (it) {
+    private fun WifiNetworkModel.iconResId(): Int? {
+        return when (this) {
             is WifiNetworkModel.CarrierMerged -> null
             is WifiNetworkModel.Inactive -> WIFI_NO_NETWORK
             is WifiNetworkModel.Active ->
                 when {
-                    it.level == null -> null
-                    it.isValidated -> WIFI_FULL_ICONS[it.level]
-                    else -> WIFI_NO_INTERNET_ICONS[it.level]
+                    this.level == null -> null
+                    this.isValidated -> WIFI_FULL_ICONS[this.level]
+                    else -> WIFI_NO_INTERNET_ICONS[this.level]
                 }
         }
     }
 
-    /** The content description for the wifi icon. */
-    private val contentDescription: Flow<ContentDescription?> = interactor.wifiNetwork.map {
-        when (it) {
+    /**
+     * Returns the content description for the wifi icon based on the given network.
+     * Null if we can't compute the content description.
+     */
+    private fun WifiNetworkModel.contentDescription(): ContentDescription? {
+        return when (this) {
             is WifiNetworkModel.CarrierMerged -> null
             is WifiNetworkModel.Inactive ->
                 ContentDescription.Loaded(
                     "${context.getString(WIFI_NO_CONNECTION)},${context.getString(NO_INTERNET)}"
                 )
             is WifiNetworkModel.Active ->
-                when (it.level) {
+                when (this.level) {
                     null -> null
                     else -> {
-                        val levelDesc = context.getString(WIFI_CONNECTION_STRENGTH[it.level])
+                        val levelDesc = context.getString(WIFI_CONNECTION_STRENGTH[this.level])
                         when {
-                            it.isValidated -> ContentDescription.Loaded(levelDesc)
-                            else -> ContentDescription.Loaded(
-                                "$levelDesc,${context.getString(NO_INTERNET)}"
-                            )
+                            this.isValidated -> ContentDescription.Loaded(levelDesc)
+                            else ->
+                                ContentDescription.Loaded(
+                                    "$levelDesc,${context.getString(NO_INTERNET)}"
+                                )
                         }
                     }
                 }
         }
     }
 
-    /**
-     * The wifi icon that should be displayed. Null if we shouldn't display any icon.
-     */
-    val wifiIcon: Flow<Icon?> = combine(
+    /** The wifi icon that should be displayed. Null if we shouldn't display any icon. */
+    private val wifiIcon: StateFlow<Icon?> =
+        combine(
+            interactor.isEnabled,
             interactor.isForceHidden,
-            iconResId,
-            contentDescription,
-        ) { isForceHidden, iconResId, contentDescription ->
-            when {
-                isForceHidden ||
-                    iconResId == null ||
-                    iconResId <= 0 -> null
-                else -> Icon.Resource(iconResId, contentDescription)
+            interactor.wifiNetwork,
+        ) { isEnabled, isForceHidden, wifiNetwork ->
+            if (!isEnabled || isForceHidden || wifiNetwork is WifiNetworkModel.CarrierMerged) {
+                return@combine null
+            }
+
+            val iconResId = wifiNetwork.iconResId() ?: return@combine null
+            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)
 
-    /**
-     * True if the activity in icon should be displayed and false otherwise.
-     */
-    val isActivityInVisible: Flow<Boolean>
-        get() =
-            if (!constants.shouldShowActivityConfig) {
-                flowOf(false)
-            } else {
-                interactor.hasActivityIn
+    /** The wifi activity status. Null if we shouldn't display the activity status. */
+    private val activity: Flow<WifiActivityModel?> =
+        if (!constants.shouldShowActivityConfig) {
+            flowOf(null)
+        } else {
+            combine(interactor.activity, interactor.ssid) { activity, ssid ->
+                when (ssid) {
+                    null -> null
+                    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. */
-    val tint: Flow<Int> = if (!statusBarPipelineFlags.useNewPipelineDebugColoring()) {
-        emptyFlow()
-    } else {
-        flowOf(Color.CYAN)
-    }
+    private val isActivityInViewVisible: Flow<Boolean> =
+         activity
+             .map { it?.hasActivityIn == true }
+             .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = false)
+
+    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 {
         @StringRes
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt
index eb907bd..39d89bf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt
@@ -110,7 +110,7 @@
         `when`(qsCarrierGroupControllerBuilder.build()).thenReturn(qsCarrierGroupController)
         `when`(variableDateViewControllerFactory.create(any()))
                 .thenReturn(variableDateViewController)
-        `when`(iconManagerFactory.create(any())).thenReturn(iconManager)
+        `when`(iconManagerFactory.create(any(), any())).thenReturn(iconManager)
         `when`(view.resources).thenReturn(mContext.resources)
         `when`(view.isAttachedToWindow).thenReturn(true)
         `when`(view.context).thenReturn(context)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
index c448538..c76d9e7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerCombinedTest.kt
@@ -176,7 +176,7 @@
         }
         whenever(view.visibility).thenAnswer { _ -> viewVisibility }
 
-        whenever(iconManagerFactory.create(any())).thenReturn(iconManager)
+        whenever(iconManagerFactory.create(any(), any())).thenReturn(iconManager)
 
         whenever(featureFlags.isEnabled(Flags.COMBINED_QS_HEADERS)).thenReturn(true)
         whenever(featureFlags.isEnabled(Flags.NEW_HEADER)).thenReturn(true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt
index 5ecfc8eb..90ae693 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/LargeScreenShadeHeaderControllerTest.kt
@@ -97,7 +97,7 @@
         whenever(view.visibility).thenAnswer { _ -> viewVisibility }
         whenever(variableDateViewControllerFactory.create(any()))
             .thenReturn(variableDateViewController)
-        whenever(iconManagerFactory.create(any())).thenReturn(iconManager)
+        whenever(iconManagerFactory.create(any(), any())).thenReturn(iconManager)
         whenever(featureFlags.isEnabled(Flags.COMBINED_QS_HEADERS)).thenReturn(false)
         mLargeScreenShadeHeaderController = LargeScreenShadeHeaderController(
                 view,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
index ba5f503..cfaa470 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
@@ -135,7 +135,7 @@
 
         MockitoAnnotations.initMocks(this);
 
-        when(mIconManagerFactory.create(any())).thenReturn(mIconManager);
+        when(mIconManagerFactory.create(any(), any())).thenReturn(mIconManager);
 
         allowTestableLooperAsMainThread();
         TestableLooper.get(this).runWithLooper(() -> {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java
index de7db74..34399b8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java
@@ -51,8 +51,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import javax.inject.Provider;
-
 @RunWith(AndroidTestingRunner.class)
 @RunWithLooper
 @SmallTest
@@ -79,8 +77,9 @@
         LinearLayout layout = new LinearLayout(mContext);
         TestDarkIconManager manager = new TestDarkIconManager(
                 layout,
+                StatusBarLocation.HOME,
                 mock(StatusBarPipelineFlags.class),
-                () -> mock(WifiViewModel.class),
+                mock(WifiViewModel.class),
                 mMobileContextProvider,
                 mock(DarkIconDispatcher.class));
         testCallOnAdd_forManager(manager);
@@ -121,13 +120,15 @@
 
         TestDarkIconManager(
                 LinearLayout group,
+                StatusBarLocation location,
                 StatusBarPipelineFlags statusBarPipelineFlags,
-                Provider<WifiViewModel> wifiViewModelProvider,
+                WifiViewModel wifiViewModel,
                 MobileContextProvider contextProvider,
                 DarkIconDispatcher darkIconDispatcher) {
             super(group,
+                    location,
                     statusBarPipelineFlags,
-                    wifiViewModelProvider,
+                    wifiViewModel,
                     contextProvider,
                     darkIconDispatcher);
         }
@@ -165,8 +166,9 @@
     private static class TestIconManager extends IconManager implements TestableIconManager {
         TestIconManager(ViewGroup group, MobileContextProvider contextProvider) {
             super(group,
+                    StatusBarLocation.HOME,
                     mock(StatusBarPipelineFlags.class),
-                    () -> mock(WifiViewModel.class),
+                    mock(WifiViewModel.class),
                     contextProvider);
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
index faadd24..1ce4d61 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
@@ -428,7 +428,7 @@
         mOperatorNameViewControllerFactory = mock(OperatorNameViewController.Factory.class);
         when(mOperatorNameViewControllerFactory.create(any()))
                 .thenReturn(mOperatorNameViewController);
-        when(mIconManagerFactory.create(any())).thenReturn(mIconManager);
+        when(mIconManagerFactory.create(any(), any())).thenReturn(mIconManager);
         mSecureSettings = mock(SecureSettings.class);
 
         setUpNotificationIconAreaController();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt
index 36be1be..0e75c74 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt
@@ -23,9 +23,16 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.log.LogBufferFactory
 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 java.io.PrintWriter
 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.mockito.Mockito
 import org.mockito.Mockito.mock
@@ -64,12 +71,70 @@
         assertThat(actualString).contains(expectedNetId)
     }
 
-    private 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)
+    @Test
+    fun logOutputChange_printsValuesAndNulls() = runBlocking(IMMEDIATE) {
+        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()
     }
-    private val NET_1_CAPS = NetworkCapabilities.Builder()
-        .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
-        .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
-        .build()
+
+    @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
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
index 6b8d4aa..f751afc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt
@@ -16,20 +16,27 @@
 
 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.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.StateFlow
 
 /** Fake implementation of [WifiRepository] exposing set methods for all the flows. */
 class FakeWifiRepository : WifiRepository {
+    private val _isWifiEnabled: MutableStateFlow<Boolean> = MutableStateFlow(false)
+    override val isWifiEnabled: StateFlow<Boolean> = _isWifiEnabled
+
     private val _wifiNetwork: MutableStateFlow<WifiNetworkModel> =
         MutableStateFlow(WifiNetworkModel.Inactive)
-    override val wifiNetwork: Flow<WifiNetworkModel> = _wifiNetwork
+    override val wifiNetwork: StateFlow<WifiNetworkModel> = _wifiNetwork
 
     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) {
         _wifiNetwork.value = wifiNetworkModel
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
index d070ba0..0ba0bd6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
@@ -28,15 +28,17 @@
 import android.net.wifi.WifiManager.TrafficStateCallback
 import androidx.test.filters.SmallTest
 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.wifi.data.model.WifiActivityModel
 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.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.mockito.any
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.nullable
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import java.util.concurrent.Executor
@@ -44,23 +46,28 @@
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.runBlocking
 import org.junit.After
 import org.junit.Before
 import org.junit.Test
+import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mock
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
 
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 class WifiRepositoryImplTest : SysuiTestCase() {
 
     private lateinit var underTest: WifiRepositoryImpl
 
+    @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
     @Mock private lateinit var logger: ConnectivityPipelineLogger
     @Mock private lateinit var connectivityManager: ConnectivityManager
     @Mock private lateinit var wifiManager: WifiManager
@@ -70,16 +77,17 @@
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
+        whenever(
+            broadcastDispatcher.broadcastFlow(
+                any(),
+                nullable(),
+                anyInt(),
+                nullable(),
+            )
+        ).thenReturn(flowOf(Unit))
         executor = FakeExecutor(FakeSystemClock())
         scope = CoroutineScope(IMMEDIATE)
-
-        underTest = WifiRepositoryImpl(
-            connectivityManager,
-            logger,
-            executor,
-            scope,
-            wifiManager,
-        )
+        underTest = createRepo()
     }
 
     @After
@@ -88,6 +96,132 @@
     }
 
     @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
     fun wifiNetwork_initiallyGetsDefault() = runBlocking(IMMEDIATE) {
         var latest: WifiNetworkModel? = null
         val job = underTest
@@ -509,13 +643,7 @@
 
     @Test
     fun wifiActivity_nullWifiManager_receivesDefault() = runBlocking(IMMEDIATE) {
-        underTest = WifiRepositoryImpl(
-            connectivityManager,
-            logger,
-            executor,
-            scope,
-            wifiManager = null,
-        )
+        underTest = createRepo(wifiManagerToUse = null)
 
         var latest: WifiActivityModel? = null
         val job = underTest
@@ -594,6 +722,17 @@
         job.cancel()
     }
 
+    private fun createRepo(wifiManagerToUse: WifiManager? = wifiManager): WifiRepositoryImpl {
+        return WifiRepositoryImpl(
+            broadcastDispatcher,
+            connectivityManager,
+            logger,
+            executor,
+            scope,
+            wifiManagerToUse,
+        )
+    }
+
     private fun getTrafficStateCallback(): TrafficStateCallback {
         val callbackCaptor = argumentCaptor<TrafficStateCallback>()
         verify(wifiManager).registerTrafficStateCallback(any(), callbackCaptor.capture())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt
index e896749..39b886a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt
@@ -16,13 +16,14 @@
 
 package com.android.systemui.statusbar.pipeline.wifi.domain.interactor
 
+import android.net.wifi.WifiManager
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 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.wifi.data.model.WifiActivityModel
 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.shared.model.WifiActivityModel
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -50,172 +51,129 @@
     }
 
     @Test
-    fun hasActivityIn_noInOrOut_outputsFalse() = 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) {
+    fun ssid_inactiveNetwork_outputsNull() = runBlocking(IMMEDIATE) {
         wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive)
-        wifiRepository.setWifiActivity(
-            WifiActivityModel(hasActivityIn = true, hasActivityOut = true)
-        )
 
-        var latest: Boolean? = null
+        var latest: String? = "default"
         val job = underTest
-            .hasActivityIn
+            .ssid
             .onEach { latest = it }
             .launchIn(this)
 
-        assertThat(latest).isFalse()
+        assertThat(latest).isNull()
 
         job.cancel()
     }
 
     @Test
-    fun hasActivityIn_carrierMergedNetwork_outputsFalse() = runBlocking(IMMEDIATE) {
+    fun ssid_carrierMergedNetwork_outputsNull() = runBlocking(IMMEDIATE) {
         wifiRepository.setWifiNetwork(WifiNetworkModel.CarrierMerged)
-        wifiRepository.setWifiActivity(
-            WifiActivityModel(hasActivityIn = true, hasActivityOut = true)
-        )
 
-        var latest: Boolean? = null
+        var latest: String? = "default"
         val job = underTest
-            .hasActivityIn
+            .ssid
             .onEach { latest = it }
             .launchIn(this)
 
-        assertThat(latest).isFalse()
+        assertThat(latest).isNull()
 
         job.cancel()
     }
 
     @Test
-    fun hasActivityIn_multipleChanges_multipleOutputChanges() = runBlocking(IMMEDIATE) {
-        wifiRepository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL)
+    fun ssid_isPasspointAccessPoint_outputsPasspointName() = runBlocking(IMMEDIATE) {
+        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
         val job = underTest
-                .hasActivityIn
-                .onEach { latest = it }
-                .launchIn(this)
+            .isEnabled
+            .onEach { latest = it }
+            .launchIn(this)
 
-        // Conduct a series of changes and verify we catch each of them in succession
-        wifiRepository.setWifiActivity(
-            WifiActivityModel(hasActivityIn = true, hasActivityOut = false)
-        )
+        wifiRepository.setIsWifiEnabled(true)
         yield()
         assertThat(latest).isTrue()
 
-        wifiRepository.setWifiActivity(
-            WifiActivityModel(hasActivityIn = false, hasActivityOut = true)
-        )
+        wifiRepository.setIsWifiEnabled(false)
         yield()
         assertThat(latest).isFalse()
 
-        wifiRepository.setWifiActivity(
-            WifiActivityModel(hasActivityIn = true, hasActivityOut = true)
-        )
+        wifiRepository.setIsWifiEnabled(true)
         yield()
         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()
     }
 
@@ -242,6 +200,32 @@
     }
 
     @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
     fun isForceHidden_repoHasWifiHidden_outputsTrue() = runBlocking(IMMEDIATE) {
         connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.WIFI))
 
@@ -270,10 +254,6 @@
 
         job.cancel()
     }
-
-    companion object {
-        val VALID_WIFI_NETWORK_MODEL = WifiNetworkModel.Active(networkId = 1, ssid = "AB")
-    }
 }
 
 private val IMMEDIATE = Dispatchers.Main.immediate
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
index 3c200a5..c577db8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
@@ -16,38 +16,216 @@
 
 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.ViewUtils
+import android.view.View
 import androidx.test.filters.SmallTest
+import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.lifecycle.InstantTaskExecutorRule
-import com.android.systemui.util.Assert
-import com.android.systemui.util.mockito.mock
+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.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 kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
 
 @SmallTest
-@RunWith(JUnit4::class)
-@RunWithLooper
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper(setAsMainLooper = true)
 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
     val instantTaskExecutor = InstantTaskExecutorRule()
 
     @Before
     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
     fun constructAndBind_hasCorrectSlot() {
         val view = ModernStatusBarWifiView.constructAndBind(
-            context, "slotName", mock()
+            context, "slotName", viewModel, StatusBarLocation.HOME
         )
 
         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
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
index 43103a0..063072f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModelTest.kt
@@ -29,25 +29,29 @@
 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.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.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.shared.model.WifiActivityModel
 import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel.Companion.NO_INTERNET
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancel
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.yield
+import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.mockito.Mock
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
 
+@Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 class WifiViewModelTest : SysuiTestCase() {
@@ -60,34 +64,64 @@
     private lateinit var connectivityRepository: FakeConnectivityRepository
     private lateinit var wifiRepository: FakeWifiRepository
     private lateinit var interactor: WifiInteractor
+    private lateinit var scope: CoroutineScope
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
         connectivityRepository = FakeConnectivityRepository()
         wifiRepository = FakeWifiRepository()
+        wifiRepository.setIsWifiEnabled(true)
         interactor = WifiInteractor(connectivityRepository, wifiRepository)
+        scope = CoroutineScope(IMMEDIATE)
+        createAndSetViewModel()
+    }
 
-        underTest = WifiViewModel(
-            statusBarPipelineFlags,
-            constants,
-            context,
-            logger,
-            interactor
-        )
+    @After
+    fun tearDown() {
+        scope.cancel()
+    }
+
+    // Note on testing: [WifiViewModel] exposes 3 different instances of
+    // [LocationBasedWifiViewModel]. In practice, these 3 different instances will get the exact
+    // same data for icon, activity, etc. flows. So, most of these tests will test just one of the
+    // instances. There are also some tests that verify all 3 instances received the same data.
+
+    @Test
+    fun wifiIcon_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
     fun wifiIcon_forceHidden_outputsNull() = runBlocking(IMMEDIATE) {
         connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.WIFI))
-        wifiRepository.setWifiNetwork(WifiNetworkModel.Active(NETWORK_ID, level = 2))
 
-        var latest: Icon? = null
+        // Start as non-null so we can verify we got the update
+        var latest: Icon? = Icon.Resource(0, null)
         val job = underTest
+            .home
             .wifiIcon
             .onEach { latest = it }
             .launchIn(this)
 
+        wifiRepository.setWifiNetwork(WifiNetworkModel.Active(NETWORK_ID, level = 2))
+        yield()
+
         assertThat(latest).isNull()
 
         job.cancel()
@@ -96,28 +130,59 @@
     @Test
     fun wifiIcon_notForceHidden_outputsVisible() = runBlocking(IMMEDIATE) {
         connectivityRepository.setForceHiddenIcons(setOf())
-        wifiRepository.setWifiNetwork(WifiNetworkModel.Active(NETWORK_ID, level = 2))
 
         var latest: Icon? = null
         val job = underTest
+            .home
             .wifiIcon
             .onEach { latest = it }
             .launchIn(this)
 
+        wifiRepository.setWifiNetwork(
+            WifiNetworkModel.Active(NETWORK_ID, isValidated = true, level = 2)
+        )
+        yield()
+
         assertThat(latest).isInstanceOf(Icon.Resource::class.java)
 
         job.cancel()
     }
 
     @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)
+        yield()
+
+        assertThat(latest).isNull()
+
+        job.cancel()
+    }
+
+    @Test
+    fun wifiIcon_inactiveNetwork_alwaysShowTrue_outputsNoNetworkIcon() = runBlocking(IMMEDIATE) {
+        whenever(constants.alwaysShowIconIfEnabled).thenReturn(true)
+        createAndSetViewModel()
 
         var latest: Icon? = null
         val job = underTest
-                .wifiIcon
-                .onEach { latest = it }
-                .launchIn(this)
+            .home
+            .wifiIcon
+            .onEach { latest = it }
+            .launchIn(this)
+
+        wifiRepository.setWifiNetwork(WifiNetworkModel.Inactive)
+        yield()
 
         assertThat(latest).isInstanceOf(Icon.Resource::class.java)
         val icon = latest as Icon.Resource
@@ -132,14 +197,22 @@
 
     @Test
     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
+            .home
             .wifiIcon
             .onEach { latest = it }
             .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()
 
         job.cancel()
@@ -147,14 +220,22 @@
 
     @Test
     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
+            .home
             .wifiIcon
             .onEach { latest = it }
             .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()
 
         job.cancel()
@@ -162,22 +243,23 @@
 
     @Test
     fun wifiIcon_isActiveAndValidated_level1_outputsFull1Icon() = runBlocking(IMMEDIATE) {
-        val level = 1
-
-        wifiRepository.setWifiNetwork(
-                WifiNetworkModel.Active(
-                        NETWORK_ID,
-                        isValidated = true,
-                        level = level
-                )
-        )
-
         var latest: Icon? = null
         val job = underTest
+            .home
             .wifiIcon
             .onEach { latest = it }
             .launchIn(this)
 
+        val level = 1
+        wifiRepository.setWifiNetwork(
+            WifiNetworkModel.Active(
+                NETWORK_ID,
+                isValidated = true,
+                level,
+            )
+        )
+        yield()
+
         assertThat(latest).isInstanceOf(Icon.Resource::class.java)
         val icon = latest as Icon.Resource
         assertThat(icon.res).isEqualTo(WIFI_FULL_ICONS[level])
@@ -190,23 +272,49 @@
     }
 
     @Test
-    fun wifiIcon_isActiveAndNotValidated_level4_outputsEmpty4Icon() = runBlocking(IMMEDIATE) {
-        val level = 4
+    fun wifiIcon_isActiveAndNotValidated_alwaysShowFalse_outputsNull() = runBlocking(IMMEDIATE) {
+        whenever(constants.alwaysShowIconIfEnabled).thenReturn(false)
+        createAndSetViewModel()
 
-        wifiRepository.setWifiNetwork(
-                WifiNetworkModel.Active(
-                        NETWORK_ID,
-                        isValidated = false,
-                        level = level
-                )
-        )
-
-        var latest: Icon? = null
+        var latest: Icon? = Icon.Resource(0, null)
         val job = underTest
+            .home
             .wifiIcon
             .onEach { latest = it }
             .launchIn(this)
 
+        wifiRepository.setWifiNetwork(
+            WifiNetworkModel.Active(NETWORK_ID, 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)
         val icon = latest as Icon.Resource
         assertThat(icon.res).isEqualTo(WIFI_NO_INTERNET_ICONS[level])
@@ -219,68 +327,398 @@
     }
 
     @Test
-    fun activityInVisible_showActivityConfigFalse_outputsFalse() = runBlocking(IMMEDIATE) {
-        whenever(constants.shouldShowActivityConfig).thenReturn(false)
-        wifiRepository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
+    fun wifiIcon_allLocationViewModelsReceiveSameData() = runBlocking(IMMEDIATE) {
+        var latestHome: Icon? = null
+        val jobHome = underTest
+            .home
+            .wifiIcon
+            .onEach { latestHome = it }
+            .launchIn(this)
 
-        var latest: Boolean? = null
-        val job = underTest
-                .isActivityInVisible
-                .onEach { latest = it }
-                .launchIn(this)
+        var latestKeyguard: Icon? = null
+        val jobKeyguard = underTest
+            .keyguard
+            .wifiIcon
+            .onEach { latestKeyguard = it }
+            .launchIn(this)
 
-        // Verify that on launch, we receive a false.
-        assertThat(latest).isFalse()
+        var latestQs: Icon? = null
+        val jobQs = underTest
+            .qs
+            .wifiIcon
+            .onEach { latestQs = it }
+            .launchIn(this)
 
-        job.cancel()
-    }
-
-    @Test
-    fun activityInVisible_showActivityConfigFalse_noUpdatesReceived() = runBlocking(IMMEDIATE) {
-        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)
+        wifiRepository.setWifiNetwork(
+            WifiNetworkModel.Active(
+                NETWORK_ID,
+                isValidated = true,
+                level = 1
+            )
         )
         yield()
 
-        // Verify that we didn't update to activityIn=true (because our config is false)
-        assertThat(latest).isFalse()
+        assertThat(latestHome).isInstanceOf(Icon.Resource::class.java)
+        assertThat(latestHome).isEqualTo(latestKeyguard)
+        assertThat(latestKeyguard).isEqualTo(latestQs)
 
-        job.cancel()
+        jobHome.cancel()
+        jobKeyguard.cancel()
+        jobQs.cancel()
     }
 
     @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)
+        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)
 
         var latest: Boolean? = null
         val job = underTest
-                .isActivityInVisible
-                .onEach { latest = it }
-                .launchIn(this)
+            .home
+            .isActivityInViewVisible
+            .onEach { latest = it }
+            .launchIn(this)
 
-        // Update the repo to have activityIn
-        wifiRepository.setWifiActivity(
-            WifiActivityModel(hasActivityIn = true, hasActivityOut = false)
-        )
+        val activity = WifiActivityModel(hasActivityIn = true, hasActivityOut = false)
+        wifiRepository.setWifiActivity(activity)
         yield()
 
-        // Verify that we updated to activityIn=true
         assertThat(latest).isTrue()
 
         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? {
         return when (this) {
             is ContentDescription.Loaded -> this.description