Merge "Not to update recent tasks split pairs when shell transition enabled" into tm-qpr-dev
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 19de1a5..521793b 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -3403,6 +3403,7 @@
     method @NonNull public android.window.WindowContainerTransaction reparentActivityToTaskFragment(@NonNull android.os.IBinder, @NonNull android.os.IBinder);
     method @NonNull public android.window.WindowContainerTransaction reparentChildren(@NonNull android.window.WindowContainerToken, @Nullable android.window.WindowContainerToken);
     method @NonNull public android.window.WindowContainerTransaction reparentTasks(@Nullable android.window.WindowContainerToken, @Nullable android.window.WindowContainerToken, @Nullable int[], @Nullable int[], boolean);
+    method @NonNull public android.window.WindowContainerTransaction requestFocusOnTaskFragment(@NonNull android.os.IBinder);
     method @NonNull public android.window.WindowContainerTransaction scheduleFinishEnterPip(@NonNull android.window.WindowContainerToken, @NonNull android.graphics.Rect);
     method @NonNull public android.window.WindowContainerTransaction setActivityWindowingMode(@NonNull android.window.WindowContainerToken, int);
     method @NonNull public android.window.WindowContainerTransaction setAdjacentRoots(@NonNull android.window.WindowContainerToken, @NonNull android.window.WindowContainerToken);
diff --git a/core/java/android/util/FeatureFlagUtils.java b/core/java/android/util/FeatureFlagUtils.java
index e0e41d0..976a3e4 100644
--- a/core/java/android/util/FeatureFlagUtils.java
+++ b/core/java/android/util/FeatureFlagUtils.java
@@ -58,6 +58,12 @@
     public static final String SETTINGS_APP_LOCALE_OPT_IN_ENABLED =
             "settings_app_locale_opt_in_enabled";
 
+    /**
+     * Launch the Volume panel in SystemUI.
+     * @hide
+     */
+    public static final String SETTINGS_VOLUME_PANEL_IN_SYSTEMUI =
+            "settings_volume_panel_in_systemui";
 
     /** @hide */
     public static final String SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS =
@@ -105,6 +111,7 @@
         DEFAULT_FLAGS.put(SETTINGS_SUPPORT_LARGE_SCREEN, "true");
         DEFAULT_FLAGS.put("settings_search_always_expand", "true");
         DEFAULT_FLAGS.put(SETTINGS_APP_LOCALE_OPT_IN_ENABLED, "true");
+        DEFAULT_FLAGS.put(SETTINGS_VOLUME_PANEL_IN_SYSTEMUI, "false");
         DEFAULT_FLAGS.put(SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS, "true");
         DEFAULT_FLAGS.put(SETTINGS_APP_ALLOW_DARK_THEME_ACTIVATION_AT_BEDTIME, "true");
         DEFAULT_FLAGS.put(SETTINGS_HIDE_SECOND_LAYER_PAGE_NAVIGATE_UP_BUTTON_IN_TWO_PANE, "true");
diff --git a/core/java/android/widget/Switch.java b/core/java/android/widget/Switch.java
index 872e65a..07dfce7 100644
--- a/core/java/android/widget/Switch.java
+++ b/core/java/android/widget/Switch.java
@@ -61,22 +61,32 @@
 import com.android.internal.R;
 
 /**
- * A Switch is a two-state toggle switch widget that can select between two
- * options. The user may drag the "thumb" back and forth to choose the selected option,
- * or simply tap to toggle as if it were a checkbox. The {@link #setText(CharSequence) text}
- * property controls the text displayed in the label for the switch, whereas the
- * {@link #setTextOff(CharSequence) off} and {@link #setTextOn(CharSequence) on} text
- * controls the text on the thumb. Similarly, the
- * {@link #setTextAppearance(android.content.Context, int) textAppearance} and the related
- * setTypeface() methods control the typeface and style of label text, whereas the
- * {@link #setSwitchTextAppearance(android.content.Context, int) switchTextAppearance} and
- * the related setSwitchTypeface() methods control that of the thumb.
+ * A Switch is a two-state toggle widget. Users can drag the switch "thumb" back
+ * and forth to select either of two options or simply tap the switch to toggle
+ * between options.
  *
- * <p>{@link androidx.recyclerview.widget.RecyclerView} is a version of
- * the Switch widget which runs on devices back to API 7.</p>
+ * <p>The {@link #setText(CharSequence) text} property controls
+ * the text of the switch label. The {@link #setTextOn(CharSequence) textOn} and
+ * {@link #setTextOff(CharSequence) textOff} properties control the text of the
+ * thumb. The {@link #setTextAppearance(int) textAppearance} property and the
+ * related {@link #setTypeface(android.graphics.Typeface) setTypeface()} methods
+ * control the typeface and style of the switch label. The
+ * {@link #setSwitchTextAppearance(android.content.Context, int)
+ * switchTextAppearance} property and the related
+ * {@link #setSwitchTypeface(android.graphics.Typeface) setSwitchTypeface()}
+ * methods control the typeface and style of the thumb text.</p>
  *
- * <p>See the <a href="{@docRoot}guide/topics/ui/controls/togglebutton.html">Toggle Buttons</a>
- * guide.</p>
+ * <p class="note"><b>Note:</b> The thumb text is displayed only if the
+ * <a href="{@docRoot}reference/android/widget/Switch#attr_android:showText">
+ * <code>showText</code></a> attribute is set to {@code true}. See also
+ * {@link #setShowText(boolean)} and {@link #getShowText()}.</p>
+ *
+ * <p>{@link androidx.appcompat.widget.SwitchCompat} provides backward
+ * compatibility down to Android 4.0 (API level 14).</p>
+ *
+ * <p>For more information, see the
+ * <a href="{@docRoot}guide/topics/ui/controls/togglebutton.html">
+ * Toggle Buttons</a> guide.</p>
  *
  * @attr ref android.R.styleable#Switch_textOn
  * @attr ref android.R.styleable#Switch_textOff
diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java
index 567b164..d3949b2 100644
--- a/core/java/android/window/WindowContainerTransaction.java
+++ b/core/java/android/window/WindowContainerTransaction.java
@@ -682,7 +682,6 @@
      * TaskFragment.
      * @param fragmentToken client assigned unique token to create TaskFragment with specified in
      *                      {@link TaskFragmentCreationParams#getFragmentToken()}.
-     * @hide
      */
     @NonNull
     public WindowContainerTransaction requestFocusOnTaskFragment(@NonNull IBinder fragmentToken) {
diff --git a/data/etc/com.android.systemui.xml b/data/etc/com.android.systemui.xml
index f030d80..e0e13f5 100644
--- a/data/etc/com.android.systemui.xml
+++ b/data/etc/com.android.systemui.xml
@@ -81,5 +81,6 @@
         <permission name="android.permission.READ_DEVICE_CONFIG" />
         <permission name="android.permission.READ_SAFETY_CENTER_STATUS" />
         <permission name="android.permission.SET_UNRESTRICTED_KEEP_CLEAR_AREAS" />
+        <permission name="android.permission.READ_SEARCH_INDEXABLES" />
     </privapp-permissions>
 </permissions>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index dcbb272..d63c25d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -1387,9 +1387,6 @@
 
             if (update.selectionChanged && mStackView != null) {
                 mStackView.setSelectedBubble(update.selectedBubble);
-                if (update.selectedBubble != null) {
-                    mSysuiProxy.updateNotificationSuppression(update.selectedBubble.getKey());
-                }
             }
 
             // Expanding? Apply this last.
@@ -1448,7 +1445,6 @@
                     // in the shade, it is essentially removed.
                     Bubble bubbleChild = mBubbleData.getAnyBubbleWithkey(child.getKey());
                     if (bubbleChild != null) {
-                        mSysuiProxy.removeNotificationEntry(bubbleChild.getKey());
                         bubbleChild.setSuppressNotification(true);
                         bubbleChild.setShowDot(false /* show */);
                     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
index 0e97e9e..453b34e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
@@ -278,12 +278,8 @@
 
         void notifyMaybeCancelSummary(String key);
 
-        void removeNotificationEntry(String key);
-
         void updateNotificationBubbleButton(String key);
 
-        void updateNotificationSuppression(String key);
-
         void onStackExpandChanged(boolean shouldExpand);
 
         void onManageMenuExpandChanged(boolean menuExpanded);
diff --git a/packages/CompanionDeviceManager/res/drawable-night/ic_info.xml b/packages/CompanionDeviceManager/res/drawable-night/ic_info.xml
index 5689e34..a984938 100644
--- a/packages/CompanionDeviceManager/res/drawable-night/ic_info.xml
+++ b/packages/CompanionDeviceManager/res/drawable-night/ic_info.xml
@@ -19,7 +19,7 @@
         android:height="24dp"
         android:viewportWidth="24"
         android:viewportHeight="24"
-        android:tint="@android:color/system_accent1_200">
+        android:tint="@android:color/system_neutral1_200">
     <path android:fillColor="@android:color/white"
           android:pathData="M11,17H13V11H11ZM12,9Q12.425,9 12.713,8.712Q13,8.425 13,8Q13,7.575 12.713,7.287Q12.425,7 12,7Q11.575,7 11.288,7.287Q11,7.575 11,8Q11,8.425 11.288,8.712Q11.575,9 12,9ZM12,22Q9.925,22 8.1,21.212Q6.275,20.425 4.925,19.075Q3.575,17.725 2.788,15.9Q2,14.075 2,12Q2,9.925 2.788,8.1Q3.575,6.275 4.925,4.925Q6.275,3.575 8.1,2.787Q9.925,2 12,2Q14.075,2 15.9,2.787Q17.725,3.575 19.075,4.925Q20.425,6.275 21.212,8.1Q22,9.925 22,12Q22,14.075 21.212,15.9Q20.425,17.725 19.075,19.075Q17.725,20.425 15.9,21.212Q14.075,22 12,22ZM12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12ZM12,20Q15.325,20 17.663,17.663Q20,15.325 20,12Q20,8.675 17.663,6.337Q15.325,4 12,4Q8.675,4 6.338,6.337Q4,8.675 4,12Q4,15.325 6.338,17.663Q8.675,20 12,20Z"/>
 </vector>
\ No newline at end of file
diff --git a/packages/CompanionDeviceManager/res/drawable/ic_info.xml b/packages/CompanionDeviceManager/res/drawable/ic_info.xml
index d9f6a27..229960c 100644
--- a/packages/CompanionDeviceManager/res/drawable/ic_info.xml
+++ b/packages/CompanionDeviceManager/res/drawable/ic_info.xml
@@ -20,7 +20,7 @@
         android:height="24dp"
         android:viewportWidth="24"
         android:viewportHeight="24"
-        android:tint="@android:color/system_accent1_600">
+        android:tint="@android:color/system_neutral1_700">
     <path android:fillColor="@android:color/white"
           android:pathData="M11,17H13V11H11ZM12,9Q12.425,9 12.713,8.712Q13,8.425 13,8Q13,7.575 12.713,7.287Q12.425,7 12,7Q11.575,7 11.288,7.287Q11,7.575 11,8Q11,8.425 11.288,8.712Q11.575,9 12,9ZM12,22Q9.925,22 8.1,21.212Q6.275,20.425 4.925,19.075Q3.575,17.725 2.788,15.9Q2,14.075 2,12Q2,9.925 2.788,8.1Q3.575,6.275 4.925,4.925Q6.275,3.575 8.1,2.787Q9.925,2 12,2Q14.075,2 15.9,2.787Q17.725,3.575 19.075,4.925Q20.425,6.275 21.212,8.1Q22,9.925 22,12Q22,14.075 21.212,15.9Q20.425,17.725 19.075,19.075Q17.725,20.425 15.9,21.212Q14.075,22 12,22ZM12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12ZM12,20Q15.325,20 17.663,17.663Q20,15.325 20,12Q20,8.675 17.663,6.337Q15.325,4 12,4Q8.675,4 6.338,6.337Q4,8.675 4,12Q4,15.325 6.338,17.663Q8.675,20 12,20Z"/>
 </vector>
\ No newline at end of file
diff --git a/packages/CompanionDeviceManager/res/layout/helper_confirmation.xml b/packages/CompanionDeviceManager/res/layout/helper_confirmation.xml
index d0d46f7..1f922b9 100644
--- a/packages/CompanionDeviceManager/res/layout/helper_confirmation.xml
+++ b/packages/CompanionDeviceManager/res/layout/helper_confirmation.xml
@@ -35,7 +35,7 @@
             <ImageView
                 android:id="@+id/app_icon"
                 android:layout_width="match_parent"
-                android:layout_height="48dp"
+                android:layout_height="32dp"
                 android:gravity="center"
                 android:layout_marginTop="12dp"
                 android:layout_marginBottom="12dp"/>
diff --git a/packages/CompanionDeviceManager/res/layout/vendor_header.xml b/packages/CompanionDeviceManager/res/layout/vendor_header.xml
index 14e7431..16a87e5 100644
--- a/packages/CompanionDeviceManager/res/layout/vendor_header.xml
+++ b/packages/CompanionDeviceManager/res/layout/vendor_header.xml
@@ -22,7 +22,7 @@
     android:layout_height="wrap_content"
     android:orientation="horizontal"
     android:layout_gravity="center"
-    android:paddingTop="24dp"
+    android:paddingTop="12dp"
     android:paddingBottom="4dp"
     android:paddingStart="24dp"
     android:paddingEnd="24dp"
@@ -32,24 +32,26 @@
         android:id="@+id/vendor_header_image"
         android:layout_width="48dp"
         android:layout_height="48dp"
+        android:padding="12dp"
         android:contentDescription="@string/vendor_icon_description" />
 
     <TextView
         android:id="@+id/vendor_header_name"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:layout_marginStart="12dp"
         android:layout_marginTop="12dp"
-        android:textSize="16sp"
+        android:textSize="14sp"
         android:layout_toEndOf="@+id/vendor_header_image"
         android:textColor="?android:attr/textColorSecondary"/>
 
     <ImageButton
         android:id="@+id/vendor_header_button"
-        android:background="@drawable/ic_info"
+        android:src="@drawable/ic_info"
         android:layout_width="48dp"
         android:layout_height="48dp"
+        android:padding="12dp"
         android:contentDescription="@string/vendor_header_button_description"
-        android:layout_alignParentEnd="true" />
+        android:layout_alignParentEnd="true"
+        android:background="@android:color/transparent" />
 
 </RelativeLayout>
\ No newline at end of file
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/OWNERS b/packages/SettingsLib/src/com/android/settingslib/media/OWNERS
new file mode 100644
index 0000000..d40f322
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/media/OWNERS
@@ -0,0 +1,2 @@
+# Default reviewers for this and subdirectories.
+shaoweishen@google.com
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 78dea89..62def48 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -152,6 +152,9 @@
     <uses-permission android:name="android.permission.CONTROL_KEYGUARD_SECURE_NOTIFICATIONS" />
     <uses-permission android:name="android.permission.GET_RUNTIME_PERMISSIONS" />
 
+    <!-- For auto-grant the access to the Settings' slice preferences, e.g. volume slices. -->
+    <uses-permission android:name="android.permission.READ_SEARCH_INDEXABLES" />
+
     <!-- Needed for WallpaperManager.clear in ImageWallpaper.updateWallpaperLocked -->
     <uses-permission android:name="android.permission.SET_WALLPAPER"/>
 
@@ -956,5 +959,13 @@
             </intent-filter>
         </receiver>
 
+        <receiver android:name=".volume.VolumePanelDialogReceiver"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.settings.panel.action.VOLUME" />
+                <action android:name="com.android.systemui.action.LAUNCH_VOLUME_PANEL_DIALOG" />
+                <action android:name="com.android.systemui.action.DISMISS_VOLUME_PANEL_DIALOG" />
+            </intent-filter>
+        </receiver>
     </application>
 </manifest>
diff --git a/packages/SystemUI/OWNERS b/packages/SystemUI/OWNERS
index fa855ab..546f0d2 100644
--- a/packages/SystemUI/OWNERS
+++ b/packages/SystemUI/OWNERS
@@ -13,7 +13,7 @@
 brockman@google.com
 brzezinski@google.com
 brycelee@google.com
-ccassidy@google.com
+caitlinshk@google.com
 chrisgollner@google.com
 cinek@google.com
 cwren@google.com
diff --git a/packages/SystemUI/res/layout/people_strip.xml b/packages/SystemUI/res/layout/people_strip.xml
deleted file mode 100644
index ec00429..0000000
--- a/packages/SystemUI/res/layout/people_strip.xml
+++ /dev/null
@@ -1,102 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2019 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.
-  -->
-
-<com.android.systemui.statusbar.notification.stack.PeopleHubView
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    android:layout_width="match_parent"
-    android:layout_height="@dimen/notification_section_header_height"
-    android:paddingStart="4dp"
-    android:paddingEnd="4dp"
-    android:focusable="true"
-    android:clickable="true"
->
-
-    <LinearLayout
-        android:id="@+id/people_list"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:layout_marginEnd="8dp"
-        android:gravity="bottom"
-        android:orientation="horizontal"
-        android:forceHasOverlappingRendering="false"
-        android:clipChildren="false">
-
-        <FrameLayout
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:gravity="start|center_vertical"
-            android:layout_weight="1"
-            android:forceHasOverlappingRendering="false">
-
-            <TextView
-                android:id="@+id/header_label"
-                style="@style/TextAppearance.NotificationSectionHeaderButton"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:text="@string/notification_section_header_conversations"
-            />
-
-        </FrameLayout>
-
-        <ImageView
-            android:layout_width="48dp"
-            android:layout_height="48dp"
-            android:padding="8dp"
-            android:scaleType="fitCenter"
-            android:forceHasOverlappingRendering="false"
-            android:visibility="gone"
-        />
-
-        <ImageView
-            android:layout_width="48dp"
-            android:layout_height="48dp"
-            android:padding="8dp"
-            android:scaleType="fitCenter"
-            android:forceHasOverlappingRendering="false"
-            android:visibility="gone"
-        />
-
-        <ImageView
-            android:layout_width="48dp"
-            android:layout_height="48dp"
-            android:padding="8dp"
-            android:scaleType="fitCenter"
-            android:forceHasOverlappingRendering="false"
-            android:visibility="gone"
-        />
-
-        <ImageView
-            android:layout_width="48dp"
-            android:layout_height="48dp"
-            android:padding="8dp"
-            android:scaleType="fitCenter"
-            android:forceHasOverlappingRendering="false"
-            android:visibility="gone"
-        />
-
-        <ImageView
-            android:layout_width="48dp"
-            android:layout_height="48dp"
-            android:padding="8dp"
-            android:scaleType="fitCenter"
-            android:forceHasOverlappingRendering="false"
-            android:visibility="gone"
-        />
-
-    </LinearLayout>
-
-</com.android.systemui.statusbar.notification.stack.PeopleHubView>
diff --git a/packages/SystemUI/res/layout/volume_panel_dialog.xml b/packages/SystemUI/res/layout/volume_panel_dialog.xml
new file mode 100644
index 0000000..99a1b5c
--- /dev/null
+++ b/packages/SystemUI/res/layout/volume_panel_dialog.xml
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    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.
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/volume_panel_dialog"
+    android:layout_width="@dimen/large_dialog_width"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        style="@style/Widget.SliceView.Panel"
+        android:gravity="center_vertical|center_horizontal"
+        android:layout_marginTop="@dimen/dialog_top_padding"
+        android:layout_marginBottom="@dimen/dialog_bottom_padding"
+        android:orientation="vertical">
+
+        <TextView
+            android:id="@+id/volume_panel_dialog_title"
+            android:ellipsize="end"
+            android:gravity="center_vertical|center_horizontal"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/sound_settings"
+            android:textAppearance="@style/TextAppearance.Dialog.Title"/>
+    </LinearLayout>
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/volume_panel_parent_layout"
+        android:scrollbars="vertical"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:minHeight="304dp"
+        android:layout_weight="1"
+        android:overScrollMode="never"/>
+
+    <LinearLayout
+        android:id="@+id/button_layout"
+        android:orientation="horizontal"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/dialog_button_vertical_padding"
+        android:layout_marginStart="@dimen/dialog_side_padding"
+        android:layout_marginEnd="@dimen/dialog_side_padding"
+        android:layout_marginBottom="@dimen/dialog_bottom_padding"
+        android:baselineAligned="false"
+        android:clickable="false"
+        android:focusable="false">
+
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:layout_gravity="start|center_vertical"
+            android:orientation="vertical">
+            <Button
+                android:id="@+id/settings_button"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/volume_panel_dialog_settings_button"
+                android:ellipsize="end"
+                android:maxLines="1"
+                style="@style/Widget.Dialog.Button.BorderButton"
+                android:clickable="true"
+                android:focusable="true"/>
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="@dimen/dialog_button_horizontal_padding"
+            android:layout_gravity="end|center_vertical">
+            <Button
+                android:id="@+id/done_button"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:text="@string/inline_done_button"
+                style="@style/Widget.Dialog.Button"
+                android:maxLines="1"
+                android:ellipsize="end"
+                android:clickable="true"
+                android:focusable="true"/>
+        </LinearLayout>
+    </LinearLayout>
+</LinearLayout>
diff --git a/packages/SystemUI/res/layout/volume_panel_slice_slider_row.xml b/packages/SystemUI/res/layout/volume_panel_slice_slider_row.xml
new file mode 100644
index 0000000..d1303ed
--- /dev/null
+++ b/packages/SystemUI/res/layout/volume_panel_slice_slider_row.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/slice_slider_layout"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <androidx.slice.widget.SliceView
+        android:id="@+id/slice_view"
+        style="@style/Widget.SliceView.Panel.Slider"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingVertical="@dimen/volume_panel_slice_vertical_padding"
+        android:paddingHorizontal="@dimen/volume_panel_slice_horizontal_padding"/>
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index c9776dd..c939504 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -479,6 +479,10 @@
 
     <dimen name="volume_tool_tip_arrow_corner_radius">2dp</dimen>
 
+    <!-- Volume panel slices dimensions -->
+    <dimen name="volume_panel_slice_vertical_padding">8dp</dimen>
+    <dimen name="volume_panel_slice_horizontal_padding">24dp</dimen>
+
     <!-- Size of each item in the ringer selector drawer. -->
     <dimen name="volume_ringer_drawer_item_size">42dp</dimen>
     <dimen name="volume_ringer_drawer_item_size_half">21dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index e4fefc7..2d0fa53 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1139,6 +1139,11 @@
     <!-- Content description for accessibility: Hint if click will disable. [CHAR LIMIT=NONE] -->
     <string name="volume_odi_captions_hint_disable">disable</string>
 
+    <!-- Sound and vibration settings dialog title. [CHAR LIMIT=30] -->
+    <string name="sound_settings">Sound &amp; vibration</string>
+    <!-- Label for button to go to sound settings screen [CHAR_LIMIT=30] -->
+    <string name="volume_panel_dialog_settings_button">Settings</string>
+
     <!-- content description for audio output chooser [CHAR LIMIT=NONE]-->
 
     <!-- Screen pinning dialog title. -->
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 112d903..6b2ff37 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -928,6 +928,10 @@
         <item name="rowStyle">@style/SliceRow</item>
     </style>
 
+    <style name="Widget.SliceView.Panel.Slider">
+        <item name="rowStyle">@style/SliceRow.Slider</item>
+    </style>
+
     <style name="SliceRow">
         <!-- 2dp start padding for the start icon -->
         <item name="titleItemStartPadding">2dp</item>
@@ -949,6 +953,26 @@
         <item name="actionDividerHeight">32dp</item>
     </style>
 
+    <style name="SliceRow.Slider">
+        <!-- Padding between content and the start icon is 5dp -->
+        <item name="contentStartPadding">5dp</item>
+        <item name="contentEndPadding">0dp</item>
+
+        <!-- 0dp start padding for the end item -->
+        <item name="endItemStartPadding">0dp</item>
+        <!-- 8dp end padding for the end item -->
+        <item name="endItemEndPadding">8dp</item>
+
+        <item name="titleSize">20sp</item>
+        <!-- Align text with slider -->
+        <item name="titleStartPadding">11dp</item>
+        <item name="subContentStartPadding">11dp</item>
+
+        <!-- Padding for indeterminate progress bar -->
+        <item name="progressBarStartPadding">12dp</item>
+        <item name="progressBarEndPadding">16dp</item>
+    </style>
+
     <style name="TextAppearance.Dialog.Title" parent="@android:style/TextAppearance.DeviceDefault.Large">
         <item name="android:textColor">?android:attr/textColorPrimary</item>
         <item name="android:textSize">24sp</item>
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 6976e3e..167d8af 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -206,7 +206,8 @@
      */
     private static final int BIOMETRIC_STATE_CANCELLING_RESTARTING = 3;
 
-    private static final int BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED = -1;
+    @VisibleForTesting
+    public static final int BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED = -1;
     public static final int BIOMETRIC_HELP_FACE_NOT_RECOGNIZED = -2;
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt b/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt
new file mode 100644
index 0000000..109be40
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/ChooserSelector.kt
@@ -0,0 +1,67 @@
+package com.android.systemui
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.PackageManager
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.FlagListenable
+import com.android.systemui.flags.Flags
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
+
+@SysUISingleton
+class ChooserSelector @Inject constructor(
+        context: Context,
+        private val featureFlags: FeatureFlags,
+        @Application private val coroutineScope: CoroutineScope,
+        @Background private val bgDispatcher: CoroutineDispatcher
+) : CoreStartable(context) {
+
+    private val packageManager = context.packageManager
+    private val chooserComponent = ComponentName.unflattenFromString(
+            context.resources.getString(ChooserSelectorResourceHelper.CONFIG_CHOOSER_ACTIVITY))
+
+    override fun start() {
+        coroutineScope.launch {
+            val listener = FlagListenable.Listener { event ->
+                if (event.flagId == Flags.CHOOSER_UNBUNDLED.id) {
+                    launch { updateUnbundledChooserEnabled() }
+                    event.requestNoRestart()
+                }
+            }
+            featureFlags.addListener(Flags.CHOOSER_UNBUNDLED, listener)
+            updateUnbundledChooserEnabled()
+
+            awaitCancellationAndThen { featureFlags.removeListener(listener) }
+        }
+    }
+
+    private suspend fun updateUnbundledChooserEnabled() {
+        setUnbundledChooserEnabled(withContext(bgDispatcher) {
+            featureFlags.isEnabled(Flags.CHOOSER_UNBUNDLED)
+        })
+    }
+
+    private fun setUnbundledChooserEnabled(enabled: Boolean) {
+        val newState = if (enabled) {
+            PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+        } else {
+            PackageManager.COMPONENT_ENABLED_STATE_DISABLED
+        }
+        packageManager.setComponentEnabledSetting(chooserComponent, newState, /* flags = */ 0)
+    }
+
+    suspend inline fun awaitCancellation(): Nothing = suspendCancellableCoroutine { }
+    suspend inline fun awaitCancellationAndThen(block: () -> Unit): Nothing = try {
+        awaitCancellation()
+    } finally {
+        block()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/ChooserSelectorResourceHelper.java b/packages/SystemUI/src/com/android/systemui/ChooserSelectorResourceHelper.java
new file mode 100644
index 0000000..7a2de7b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/ChooserSelectorResourceHelper.java
@@ -0,0 +1,31 @@
+/*
+ * 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;
+
+import androidx.annotation.StringRes;
+
+import com.android.internal.R;
+
+/** Helper class for referencing resources */
+class ChooserSelectorResourceHelper {
+
+    private ChooserSelectorResourceHelper() {
+    }
+
+    @StringRes
+    static final int CONFIG_CHOOSER_ACTIVITY = R.string.config_chooserActivity;
+}
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index 614a87f..0cb4da4 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -87,8 +87,6 @@
 import com.android.systemui.statusbar.events.SystemStatusAnimationScheduler;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.notification.NotificationEntryManager.KeyguardEnvironment;
-import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy;
-import com.android.systemui.statusbar.notification.collection.legacy.VisualStabilityManager;
 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
 import com.android.systemui.statusbar.notification.logging.NotificationLogger;
@@ -311,8 +309,6 @@
     @Inject Lazy<AccessibilityFloatingMenuController> mAccessibilityFloatingMenuController;
     @Inject Lazy<StatusBarStateController> mStatusBarStateController;
     @Inject Lazy<NotificationLockscreenUserManager> mNotificationLockscreenUserManager;
-    @Inject Lazy<NotificationGroupManagerLegacy> mNotificationGroupManager;
-    @Inject Lazy<VisualStabilityManager> mVisualStabilityManager;
     @Inject Lazy<NotificationGutsManager> mNotificationGutsManager;
     @Inject Lazy<NotificationMediaManager> mNotificationMediaManager;
     @Inject Lazy<NotificationRemoteInputManager> mNotificationRemoteInputManager;
@@ -523,8 +519,6 @@
         mProviders.put(StatusBarStateController.class, mStatusBarStateController::get);
         mProviders.put(NotificationLockscreenUserManager.class,
                 mNotificationLockscreenUserManager::get);
-        mProviders.put(VisualStabilityManager.class, mVisualStabilityManager::get);
-        mProviders.put(NotificationGroupManagerLegacy.class, mNotificationGroupManager::get);
         mProviders.put(NotificationMediaManager.class, mNotificationMediaManager::get);
         mProviders.put(NotificationGutsManager.class, mNotificationGutsManager::get);
         mProviders.put(NotificationRemoteInputManager.class,
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java
index 8ba6f1c..d60a222 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java
@@ -26,6 +26,7 @@
 import com.android.systemui.screenshot.ActionProxyReceiver;
 import com.android.systemui.screenshot.DeleteScreenshotReceiver;
 import com.android.systemui.screenshot.SmartActionsReceiver;
+import com.android.systemui.volume.VolumePanelDialogReceiver;
 
 import dagger.Binds;
 import dagger.Module;
@@ -78,6 +79,15 @@
      */
     @Binds
     @IntoMap
+    @ClassKey(VolumePanelDialogReceiver.class)
+    public abstract BroadcastReceiver bindVolumePanelDialogReceiver(
+            VolumePanelDialogReceiver broadcastReceiver);
+
+    /**
+     *
+     */
+    @Binds
+    @IntoMap
     @ClassKey(PeopleSpaceWidgetPinnedReceiver.class)
     public abstract BroadcastReceiver bindPeopleSpaceWidgetPinnedReceiver(
             PeopleSpaceWidgetPinnedReceiver broadcastReceiver);
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
index 6db3e82..8bb27a7 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.dagger
 
 import com.android.keyguard.KeyguardBiometricLockoutLogger
+import com.android.systemui.ChooserSelector
 import com.android.systemui.CoreStartable
 import com.android.systemui.LatencyTester
 import com.android.systemui.ScreenDecorations
@@ -60,6 +61,12 @@
     @ClassKey(AuthController::class)
     abstract fun bindAuthController(service: AuthController): CoreStartable
 
+    /** Inject into ChooserCoreStartable. */
+    @Binds
+    @IntoMap
+    @ClassKey(ChooserSelector::class)
+    abstract fun bindChooserSelector(sysui: ChooserSelector): CoreStartable
+
     /** Inject into ClipboardListener.  */
     @Binds
     @IntoMap
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 1b060e2..a0ecd22 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -65,7 +65,6 @@
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
 import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinder;
 import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl;
-import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy;
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider;
@@ -225,11 +224,9 @@
             NotificationInterruptStateProvider interruptionStateProvider,
             ZenModeController zenModeController,
             NotificationLockscreenUserManager notifUserManager,
-            NotificationGroupManagerLegacy groupManager,
             CommonNotifCollection notifCollection,
             NotifPipeline notifPipeline,
             SysUiState sysUiState,
-            DumpManager dumpManager,
             @Main Executor sysuiMainExecutor) {
         return Optional.ofNullable(BubblesManager.create(context,
                 bubblesOptional,
@@ -243,7 +240,6 @@
                 interruptionStateProvider,
                 zenModeController,
                 notifUserManager,
-                groupManager,
                 notifCollection,
                 notifPipeline,
                 sysUiState,
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
index d517aa3..1482303 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
@@ -60,8 +60,8 @@
     public static final ResourceBooleanFlag NOTIFICATION_DRAG_TO_CONTENTS =
             new ResourceBooleanFlag(108, R.bool.config_notificationToContents);
 
-    public static final UnreleasedFlag REMOVE_UNRANKED_NOTIFICATIONS =
-            new UnreleasedFlag(109, true);
+    public static final ReleasedFlag REMOVE_UNRANKED_NOTIFICATIONS =
+            new ReleasedFlag(109);
 
     public static final UnreleasedFlag FSI_REQUIRES_KEYGUARD =
             new UnreleasedFlag(110, true);
@@ -253,6 +253,9 @@
     // 1400 - columbus, b/242800729
     public static final UnreleasedFlag QUICK_TAP_IN_PCC = new UnreleasedFlag(1400);
 
+    // 1500 - chooser
+    public static final UnreleasedFlag CHOOSER_UNBUNDLED = new UnreleasedFlag(1500);
+
     // Pay no attention to the reflection behind the curtain.
     // ========================== Curtain ==========================
     // |                                                           |
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
index 012d766..b02393b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
@@ -488,8 +488,8 @@
         TextView deviceName = mMediaViewHolder.getSeamlessText();
         final MediaDeviceData device = data.getDevice();
 
-        final boolean enabled;
-        final boolean seamlessDisabled;
+        final boolean isTapEnabled;
+        final boolean useDisabledAlpha;
         final int iconResource;
         CharSequence deviceString;
         if (showBroadcastButton) {
@@ -499,21 +499,25 @@
                     && TextUtils.equals(device.getName(),
                     MediaDataUtils.getAppLabel(mContext, mPackageName, mContext.getString(
                             R.string.bt_le_audio_broadcast_dialog_unknown_name)));
-            seamlessDisabled = !mIsCurrentBroadcastedApp;
+            useDisabledAlpha = !mIsCurrentBroadcastedApp;
             // Always be enabled if the broadcast button is shown
-            enabled = true;
+            isTapEnabled = true;
+
+            // Defaults for broadcasting state
             deviceString = mContext.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name);
             iconResource = R.drawable.settings_input_antenna;
         } else {
             // Disable clicking on output switcher for invalid devices and resumption controls
-            seamlessDisabled = (device != null && !device.getEnabled()) || data.getResumption();
-            enabled = !seamlessDisabled;
+            useDisabledAlpha = (device != null && !device.getEnabled()) || data.getResumption();
+            isTapEnabled = !useDisabledAlpha;
+
+            // Defaults for non-broadcasting state
             deviceString = mContext.getString(R.string.media_seamless_other_device);
             iconResource = R.drawable.ic_media_home_devices;
         }
 
-        mMediaViewHolder.getSeamlessButton().setAlpha(seamlessDisabled ? DISABLED_ALPHA : 1.0f);
-        seamlessView.setEnabled(enabled);
+        mMediaViewHolder.getSeamlessButton().setAlpha(useDisabledAlpha ? DISABLED_ALPHA : 1.0f);
+        seamlessView.setEnabled(isTapEnabled);
 
         if (device != null) {
             Drawable icon = device.getIcon();
@@ -524,7 +528,9 @@
             } else {
                 iconView.setImageDrawable(icon);
             }
-            deviceString = device.getName();
+            if (device.getName() != null) {
+                deviceString = device.getName();
+            }
         } else {
             // Set to default icon
             iconView.setImageResource(iconResource);
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
index 8305050..267c1f5 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
@@ -265,7 +265,6 @@
             updateCurrent()
         }
 
-
         override fun onBroadcastStarted(reason: Int, broadcastId: Int) {
             if (DEBUG) {
                 Log.d(TAG, "onBroadcastStarted(), reason = $reason , broadcastId = $broadcastId")
@@ -279,8 +278,10 @@
             }
         }
 
-        override fun onBroadcastMetadataChanged(broadcastId: Int,
-                                                metadata: BluetoothLeBroadcastMetadata) {
+        override fun onBroadcastMetadataChanged(
+            broadcastId: Int,
+            metadata: BluetoothLeBroadcastMetadata
+        ) {
             if (DEBUG) {
                 Log.d(TAG, "onBroadcastMetadataChanged(), broadcastId = $broadcastId , " +
                         "metadata = $metadata")
@@ -291,7 +292,6 @@
         override fun onBroadcastStopped(reason: Int, broadcastId: Int) {
             if (DEBUG) {
                 Log.d(TAG, "onBroadcastStopped(), reason = $reason , broadcastId = $broadcastId")
-
             }
             updateCurrent()
         }
@@ -344,7 +344,11 @@
 
                 // If we have a controller but get a null route, then don't trust the device
                 val enabled = device != null && (controller == null || route != null)
-                val name = route?.name?.toString() ?: device?.name
+                val name = if (controller == null || route != null) {
+                    route?.name?.toString() ?: device?.name
+                } else {
+                    null
+                }
                 current = MediaDeviceData(enabled, device?.iconWithoutBackground, name,
                         id = device?.id, showBroadcastButton = false)
             }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
index 948fb14..6038006 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
@@ -129,6 +129,15 @@
         })
     }
 
+    fun logInternetTileUpdate(lastType: Int, callback: String) {
+        log(VERBOSE, {
+            int1 = lastType
+            str1 = callback
+        }, {
+            "mLastTileState=$int1, Callback=$str1."
+        })
+    }
+
     fun logTileUpdated(tileSpec: String, state: QSTile.State) {
         log(VERBOSE, {
             str1 = tileSpec
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
index e040ad0..d304024 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTile.java
@@ -365,6 +365,9 @@
             mWifiInfo.mNoDefaultNetwork = noDefaultNetwork;
             mWifiInfo.mNoValidatedNetwork = noValidatedNetwork;
             mWifiInfo.mNoNetworksAvailable = noNetworksAvailable;
+            if (!noDefaultNetwork) {
+                return;
+            }
             refreshState(mWifiInfo);
         }
 
@@ -380,6 +383,7 @@
 
     @Override
     protected void handleUpdateState(SignalState state, Object arg) {
+        mQSLogger.logInternetTileUpdate(mLastTileState, arg == null ? "null" : arg.toString());
         if (arg instanceof CellularCallbackInfo) {
             mLastTileState = 0;
             handleUpdateCellularState(state, arg);
@@ -597,4 +601,9 @@
         pw.print("    "); pw.println("mLastTileState=" + mLastTileState);
         pw.print("    "); pw.println("mSignalCallback=" + mSignalCallback.toString());
     }
+
+    // For testing usage only.
+    protected int getLastTileState() {
+        return mLastTileState;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt
index a918e5d..309059f 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.screenshot
 
 import android.graphics.Insets
+import android.util.Log
 import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
 import com.android.internal.util.ScreenshotHelper.HardwareBitmapBundler
 import com.android.internal.util.ScreenshotHelper.ScreenshotRequest
@@ -61,8 +62,9 @@
         ) {
 
             val info = policy.findPrimaryContent(policy.getDefaultDisplayId())
+            Log.d(TAG, "findPrimaryContent: $info")
 
-            result = if (policy.isManagedProfile(info.userId)) {
+            result = if (policy.isManagedProfile(info.user.identifier)) {
                 val image = capture.captureTask(info.taskId)
                     ?: error("Task snapshot returned a null Bitmap!")
 
@@ -70,7 +72,7 @@
                 ScreenshotRequest(
                     TAKE_SCREENSHOT_PROVIDED_IMAGE, request.source,
                     HardwareBitmapBundler.hardwareBitmapToBundle(image),
-                    info.bounds, Insets.NONE, info.taskId, info.userId, info.component
+                    info.bounds, Insets.NONE, info.taskId, info.user.identifier, info.component
                 )
             } else {
                 // Create a new request of the same type which includes the top component
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicy.kt
index 3580010..f73d204 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicy.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicy.kt
@@ -19,6 +19,7 @@
 import android.annotation.UserIdInt
 import android.content.ComponentName
 import android.graphics.Rect
+import android.os.UserHandle
 import android.view.Display
 
 /**
@@ -42,7 +43,7 @@
     data class DisplayContentInfo(
         val component: ComponentName,
         val bounds: Rect,
-        @UserIdInt val userId: Int,
+        val user: UserHandle,
         val taskId: Int,
     )
 
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt
index ba809f6..c2a5060 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotPolicyImpl.kt
@@ -29,9 +29,11 @@
 import android.graphics.Rect
 import android.os.Process
 import android.os.RemoteException
+import android.os.UserHandle
 import android.os.UserManager
 import android.util.Log
 import android.view.Display.DEFAULT_DISPLAY
+import com.android.internal.annotations.VisibleForTesting
 import com.android.internal.infra.ServiceConnector
 import com.android.systemui.SystemUIService
 import com.android.systemui.dagger.SysUISingleton
@@ -45,21 +47,13 @@
 import kotlinx.coroutines.withContext
 
 @SysUISingleton
-internal class ScreenshotPolicyImpl @Inject constructor(
+internal open class ScreenshotPolicyImpl @Inject constructor(
     context: Context,
     private val userMgr: UserManager,
     private val atmService: IActivityTaskManager,
     @Background val bgDispatcher: CoroutineDispatcher,
 ) : ScreenshotPolicy {
 
-    private val systemUiContent =
-        DisplayContentInfo(
-            ComponentName(context, SystemUIService::class.java),
-            Rect(),
-            ActivityTaskManager.INVALID_TASK_ID,
-            Process.myUserHandle().identifier,
-        )
-
     private val proxyConnector: ServiceConnector<IScreenshotProxy> =
         ServiceConnector.Impl(
             context,
@@ -78,6 +72,9 @@
     }
 
     private fun nonPipVisibleTask(info: RootTaskInfo): Boolean {
+        if (DEBUG) {
+            debugLogRootTaskInfo(info)
+        }
         return info.windowingMode != WindowConfiguration.WINDOWING_MODE_PINNED &&
             info.isVisible &&
             info.isRunning &&
@@ -99,58 +96,46 @@
         }
 
         val taskInfoList = getAllRootTaskInfosOnDisplay(displayId)
-        if (DEBUG) {
-            debugLogRootTaskInfos(taskInfoList)
-        }
 
         // If no visible task is located, then report SystemUI as the foreground content
         val target = taskInfoList.firstOrNull(::nonPipVisibleTask) ?: return systemUiContent
-
-        val topActivity: ComponentName = target.topActivity ?: error("should not be null")
-        val topChildTask = target.childTaskIds.size - 1
-        val childTaskId = target.childTaskIds[topChildTask]
-        val childTaskUserId = target.childTaskUserIds[topChildTask]
-        val childTaskBounds = target.childTaskBounds[topChildTask]
-
-        return DisplayContentInfo(topActivity, childTaskBounds, childTaskId, childTaskUserId)
+        return target.toDisplayContentInfo()
     }
 
-    private fun debugLogRootTaskInfos(taskInfoList: List<RootTaskInfo>) {
-        for (info in taskInfoList) {
-            Log.d(
-                TAG,
-                "[root task info] " +
-                    "taskId=${info.taskId} " +
-                    "parentTaskId=${info.parentTaskId} " +
-                    "position=${info.position} " +
-                    "positionInParent=${info.positionInParent} " +
-                    "isVisible=${info.isVisible()} " +
-                    "visible=${info.visible} " +
-                    "isFocused=${info.isFocused} " +
-                    "isSleeping=${info.isSleeping} " +
-                    "isRunning=${info.isRunning} " +
-                    "windowMode=${windowingModeToString(info.windowingMode)} " +
-                    "activityType=${activityTypeToString(info.activityType)} " +
-                    "topActivity=${info.topActivity} " +
-                    "topActivityInfo=${info.topActivityInfo} " +
-                    "numActivities=${info.numActivities} " +
-                    "childTaskIds=${Arrays.toString(info.childTaskIds)} " +
-                    "childUserIds=${Arrays.toString(info.childTaskUserIds)} " +
-                    "childTaskBounds=${Arrays.toString(info.childTaskBounds)} " +
-                    "childTaskNames=${Arrays.toString(info.childTaskNames)}"
-            )
+    private fun debugLogRootTaskInfo(info: RootTaskInfo) {
+        Log.d(TAG, "RootTaskInfo={" +
+                "taskId=${info.taskId} " +
+                "parentTaskId=${info.parentTaskId} " +
+                "position=${info.position} " +
+                "positionInParent=${info.positionInParent} " +
+                "isVisible=${info.isVisible()} " +
+                "visible=${info.visible} " +
+                "isFocused=${info.isFocused} " +
+                "isSleeping=${info.isSleeping} " +
+                "isRunning=${info.isRunning} " +
+                "windowMode=${windowingModeToString(info.windowingMode)} " +
+                "activityType=${activityTypeToString(info.activityType)} " +
+                "topActivity=${info.topActivity} " +
+                "topActivityInfo=${info.topActivityInfo} " +
+                "numActivities=${info.numActivities} " +
+                "childTaskIds=${Arrays.toString(info.childTaskIds)} " +
+                "childUserIds=${Arrays.toString(info.childTaskUserIds)} " +
+                "childTaskBounds=${Arrays.toString(info.childTaskBounds)} " +
+                "childTaskNames=${Arrays.toString(info.childTaskNames)}" +
+                "}"
+        )
 
-            for (j in 0 until info.childTaskIds.size) {
-                Log.d(TAG, "    *** [$j] ******")
-                Log.d(TAG, "        ***  childTaskIds[$j]: ${info.childTaskIds[j]}")
-                Log.d(TAG, "        ***  childTaskUserIds[$j]: ${info.childTaskUserIds[j]}")
-                Log.d(TAG, "        ***  childTaskBounds[$j]: ${info.childTaskBounds[j]}")
-                Log.d(TAG, "        ***  childTaskNames[$j]: ${info.childTaskNames[j]}")
-            }
+        for (j in 0 until info.childTaskIds.size) {
+            Log.d(TAG, "    *** [$j] ******")
+            Log.d(TAG, "        ***  childTaskIds[$j]: ${info.childTaskIds[j]}")
+            Log.d(TAG, "        ***  childTaskUserIds[$j]: ${info.childTaskUserIds[j]}")
+            Log.d(TAG, "        ***  childTaskBounds[$j]: ${info.childTaskBounds[j]}")
+            Log.d(TAG, "        ***  childTaskNames[$j]: ${info.childTaskNames[j]}")
         }
     }
 
-    private suspend fun getAllRootTaskInfosOnDisplay(displayId: Int): List<RootTaskInfo> =
+    @VisibleForTesting
+    open suspend fun getAllRootTaskInfosOnDisplay(displayId: Int): List<RootTaskInfo> =
         withContext(bgDispatcher) {
             try {
                 atmService.getAllRootTaskInfosOnDisplay(displayId)
@@ -160,7 +145,8 @@
             }
         }
 
-    private suspend fun isNotificationShadeExpanded(): Boolean = suspendCoroutine { k ->
+    @VisibleForTesting
+    open suspend fun isNotificationShadeExpanded(): Boolean = suspendCoroutine { k ->
         proxyConnector
             .postForResult { it.isNotificationShadeExpanded }
             .whenComplete { expanded, error ->
@@ -171,8 +157,30 @@
             }
     }
 
-    companion object {
-        const val TAG: String = "ScreenshotPolicyImpl"
-        const val DEBUG: Boolean = false
-    }
+    @VisibleForTesting
+    internal val systemUiContent =
+        DisplayContentInfo(
+            ComponentName(context, SystemUIService::class.java),
+            Rect(),
+            Process.myUserHandle(),
+            ActivityTaskManager.INVALID_TASK_ID
+        )
+}
+
+private const val TAG: String = "ScreenshotPolicyImpl"
+private const val DEBUG: Boolean = false
+
+@VisibleForTesting
+internal fun RootTaskInfo.toDisplayContentInfo(): DisplayContentInfo {
+    val topActivity: ComponentName = topActivity ?: error("should not be null")
+    val topChildTask = childTaskIds.size - 1
+    val childTaskId = childTaskIds[topChildTask]
+    val childTaskUserId = childTaskUserIds[topChildTask]
+    val childTaskBounds = childTaskBounds[topChildTask]
+
+    return DisplayContentInfo(
+        topActivity,
+        childTaskBounds,
+        UserHandle.of(childTaskUserId),
+        childTaskId)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index c983644..87f8a03 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -122,6 +122,7 @@
     private static final int MSG_HIDE_TRANSIENT = 1;
     private static final int MSG_SHOW_ACTION_TO_UNLOCK = 2;
     private static final int MSG_HIDE_BIOMETRIC_MESSAGE = 3;
+    private static final int MSG_RESET_ERROR_MESSAGE_ON_SCREEN_ON = 4;
     private static final long TRANSIENT_BIOMETRIC_ERROR_TIMEOUT = 1300;
     public static final long DEFAULT_HIDE_DELAY_MS =
             3500 + KeyguardIndicationTextView.Y_IN_DURATION;
@@ -188,9 +189,11 @@
                 }
             };
     private ScreenLifecycle mScreenLifecycle;
-    private final ScreenLifecycle.Observer mScreenObserver = new ScreenLifecycle.Observer() {
+    private final ScreenLifecycle.Observer mScreenObserver =
+            new ScreenLifecycle.Observer() {
         @Override
         public void onScreenTurnedOn() {
+            mHandler.removeMessages(MSG_RESET_ERROR_MESSAGE_ON_SCREEN_ON);
             if (mBiometricErrorMessageToShowOnScreenOn != null) {
                 showBiometricMessage(mBiometricErrorMessageToShowOnScreenOn);
                 // We want to keep this message around in case the screen was off
@@ -259,6 +262,8 @@
                     showActionToUnlock();
                 } else if (msg.what == MSG_HIDE_BIOMETRIC_MESSAGE) {
                     hideBiometricMessage();
+                } else if (msg.what == MSG_RESET_ERROR_MESSAGE_ON_SCREEN_ON) {
+                    mBiometricErrorMessageToShowOnScreenOn = null;
                 }
             }
         };
@@ -1060,6 +1065,11 @@
             } else if (showActionToUnlock) {
                 mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SHOW_ACTION_TO_UNLOCK),
                         TRANSIENT_BIOMETRIC_ERROR_TIMEOUT);
+            } else {
+                mBiometricErrorMessageToShowOnScreenOn = helpString;
+                mHandler.sendMessageDelayed(
+                        mHandler.obtainMessage(MSG_RESET_ERROR_MESSAGE_ON_SCREEN_ON),
+                        1000);
             }
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java
index d1bc14f..d908243 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManager.java
@@ -50,12 +50,6 @@
     void addUserChangedListener(UserChangedListener listener);
 
     /**
-     * Registers a [KeyguardNotificationSuppressor] that will be consulted during
-     * {@link #shouldShowOnKeyguard(NotificationEntry)}
-     */
-    void addKeyguardNotificationSuppressor(KeyguardNotificationSuppressor suppressor);
-
-    /**
      * Removes a listener previously registered with
      * {@link #addUserChangedListener(UserChangedListener)}
      */
@@ -63,14 +57,8 @@
 
     SparseArray<UserInfo> getCurrentProfiles();
 
-    void setLockscreenPublicMode(boolean isProfilePublic, int userId);
-
     boolean shouldShowLockscreenNotifications();
 
-    boolean shouldHideNotifications(int userId);
-    boolean shouldHideNotifications(String key);
-    boolean shouldShowOnKeyguard(NotificationEntry entry);
-
     boolean isAnyProfilePublicMode();
 
     void updatePublicMode();
@@ -108,11 +96,6 @@
         default void onUserRemoved(int userId) {}
     }
 
-    /** Used to hide notifications on the lockscreen */
-    interface KeyguardNotificationSuppressor {
-        boolean shouldSuppressOnKeyguard(NotificationEntry entry);
-    }
-
     /**
      * Notified when any state pertaining to Notifications has changed; any methods pertaining to
      * notifications should be re-queried.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
index f4ca7ed..ae5a2c3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java
@@ -15,17 +15,13 @@
  */
 package com.android.systemui.statusbar;
 
-import static android.app.Notification.VISIBILITY_SECRET;
 import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED;
 
 import static com.android.systemui.DejankUtils.whitelistIpcs;
-import static com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt.BUCKET_MEDIA_CONTROLS;
-import static com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt.BUCKET_SILENT;
 
 import android.app.ActivityManager;
 import android.app.KeyguardManager;
 import android.app.Notification;
-import android.app.NotificationManager;
 import android.app.admin.DevicePolicyManager;
 import android.content.BroadcastReceiver;
 import android.content.Context;
@@ -42,9 +38,10 @@
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
 
+import androidx.annotation.VisibleForTesting;
+
 import com.android.internal.statusbar.NotificationVisibility;
 import com.android.internal.widget.LockPatternUtils;
-import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.Dependency;
 import com.android.systemui.Dumpable;
 import com.android.systemui.broadcast.BroadcastDispatcher;
@@ -110,7 +107,6 @@
     private LockPatternUtils mLockPatternUtils;
     protected KeyguardManager mKeyguardManager;
     private int mState = StatusBarState.SHADE;
-    private List<KeyguardNotificationSuppressor> mKeyguardSuppressors = new ArrayList<>();
     private final ListenerSet<NotificationStateChangedListener> mNotifStateChangedListeners =
             new ListenerSet<>();
 
@@ -344,67 +340,6 @@
         }
     }
 
-    /**
-     * Returns true if notifications are temporarily disabled for this user for security reasons,
-     * regardless of the normal settings for that user.
-     */
-    private boolean shouldTemporarilyHideNotifications(int userId) {
-        if (userId == UserHandle.USER_ALL) {
-            userId = mCurrentUserId;
-        }
-        boolean inLockdown = Dependency.get(KeyguardUpdateMonitor.class).isUserInLockdown(userId);
-        mUsersInLockdownLatestResult.put(userId, inLockdown);
-        return inLockdown;
-    }
-
-    /**
-     * Returns true if we're on a secure lockscreen and the user wants to hide notification data.
-     * If so, notifications should be hidden.
-     */
-    public boolean shouldHideNotifications(int userId) {
-        boolean hide = isLockscreenPublicMode(userId) && !userAllowsNotificationsInPublic(userId)
-                || (userId != mCurrentUserId && shouldHideNotifications(mCurrentUserId))
-                || shouldTemporarilyHideNotifications(userId);
-        mShouldHideNotifsLatestResult.put(userId, hide);
-        return hide;
-    }
-
-    /**
-     * Returns true if we're on a secure lockscreen and the user wants to hide notifications via
-     * package-specific override.
-     */
-    public boolean shouldHideNotifications(String key) {
-        if (mCommonNotifCollectionLazy.get() == null) {
-            Log.wtf(TAG, "mCommonNotifCollectionLazy was null!", new Throwable());
-            return true;
-        }
-        NotificationEntry visibleEntry = mCommonNotifCollectionLazy.get().getEntry(key);
-        return isLockscreenPublicMode(mCurrentUserId) && visibleEntry != null
-                && visibleEntry.getRanking().getLockscreenVisibilityOverride() == VISIBILITY_SECRET;
-    }
-
-    public boolean shouldShowOnKeyguard(NotificationEntry entry) {
-        if (mCommonNotifCollectionLazy.get() == null) {
-            Log.wtf(TAG, "mCommonNotifCollectionLazy was null!", new Throwable());
-            return false;
-        }
-        for (int i = 0; i < mKeyguardSuppressors.size(); i++) {
-            if (mKeyguardSuppressors.get(i).shouldSuppressOnKeyguard(entry)) {
-                return false;
-            }
-        }
-        boolean exceedsPriorityThreshold;
-        if (mHideSilentNotificationsOnLockscreen) {
-            exceedsPriorityThreshold =
-                    entry.getBucket() == BUCKET_MEDIA_CONTROLS
-                            || (entry.getBucket() != BUCKET_SILENT
-                            && entry.getImportance() >= NotificationManager.IMPORTANCE_DEFAULT);
-        } else {
-            exceedsPriorityThreshold = !entry.getRanking().isAmbient();
-        }
-        return mShowLockscreenNotifications && exceedsPriorityThreshold;
-    }
-
     private void setShowLockscreenNotifications(boolean show) {
         mShowLockscreenNotifications = show;
     }
@@ -491,7 +426,8 @@
     /**
      * Save the current "public" (locked and secure) state of the lockscreen.
      */
-    public void setLockscreenPublicMode(boolean publicMode, int userId) {
+    @VisibleForTesting
+    void setLockscreenPublicMode(boolean publicMode, int userId) {
         mLockscreenPublicMode.put(userId, publicMode);
     }
 
@@ -674,11 +610,6 @@
     }
 
     @Override
-    public void addKeyguardNotificationSuppressor(KeyguardNotificationSuppressor suppressor) {
-        mKeyguardSuppressors.add(suppressor);
-    }
-
-    @Override
     public void removeUserChangedListener(UserChangedListener listener) {
         mListeners.remove(listener);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiIcons.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiIcons.java
index 7097568..e582a01 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiIcons.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/WifiIcons.java
@@ -31,7 +31,7 @@
             com.android.internal.R.drawable.ic_wifi_signal_4
     };
 
-    private static final int[] WIFI_NO_INTERNET_ICONS = {
+    public static final int[] WIFI_NO_INTERNET_ICONS = {
             R.drawable.ic_no_internet_wifi_signal_0,
             R.drawable.ic_no_internet_wifi_signal_1,
             R.drawable.ic_no_internet_wifi_signal_2,
@@ -48,7 +48,7 @@
 
     public static final int QS_WIFI_DISABLED = com.android.internal.R.drawable.ic_wifi_signal_0;
     public static final int QS_WIFI_NO_NETWORK = com.android.internal.R.drawable.ic_wifi_signal_0;
-    static final int WIFI_NO_NETWORK = QS_WIFI_NO_NETWORK;
+    public static final int WIFI_NO_NETWORK = QS_WIFI_NO_NETWORK;
 
     static final int WIFI_LEVEL_COUNT = WIFI_SIGNAL_STRENGTH[0].length;
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
index e2f87b6..54be9a6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
@@ -23,7 +23,6 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-import com.android.systemui.statusbar.notification.collection.legacy.VisualStabilityManager;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
 import com.android.systemui.statusbar.notification.dagger.NotificationsModule;
 
@@ -47,7 +46,7 @@
  * user. After an entry makes its way into the active state, we sort and filter the entire set to
  * repopulate the visible set.
  */
-public class NotificationEntryManager implements VisualStabilityManager.Callback {
+public class NotificationEntryManager {
 
     private final NotificationEntryManagerLogger mLogger;
 
@@ -85,11 +84,6 @@
         mNotificationEntryListeners.remove(listener);
     }
 
-    @Override
-    public void onChangeAllowed() {
-        updateNotifications("reordering is now allowed");
-    }
-
     /**
      * Update the notifications
      * @param reason why the notifications are updating
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
index 68bf69a..2887f97 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
@@ -88,9 +88,9 @@
 import com.android.systemui.statusbar.notification.collection.notifcollection.EntryUpdatedEvent;
 import com.android.systemui.statusbar.notification.collection.notifcollection.InitEntryEvent;
 import com.android.systemui.statusbar.notification.collection.notifcollection.InternalNotifUpdater;
+import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionInconsistencyTracker;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionLogger;
-import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionLoggerKt;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifDismissInterceptor;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifEvent;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender;
@@ -112,7 +112,6 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Queue;
-import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.concurrent.TimeUnit;
 
@@ -151,6 +150,7 @@
     private final Executor mBgExecutor;
     private final LogBufferEulogizer mEulogizer;
     private final DumpManager mDumpManager;
+    private final NotifCollectionInconsistencyTracker mInconsistencyTracker;
 
     private final Map<String, NotificationEntry> mNotificationSet = new ArrayMap<>();
     private final Collection<NotificationEntry> mReadOnlyNotificationSet =
@@ -162,7 +162,6 @@
     private final List<NotifLifetimeExtender> mLifetimeExtenders = new ArrayList<>();
     private final List<NotifDismissInterceptor> mDismissInterceptors = new ArrayList<>();
 
-    private Set<String> mNotificationsWithoutRankings = Collections.emptySet();
 
     private Queue<NotifEvent> mEventQueue = new ArrayDeque<>();
 
@@ -188,6 +187,7 @@
         mBgExecutor = bgExecutor;
         mEulogizer = logBufferEulogizer;
         mDumpManager = dumpManager;
+        mInconsistencyTracker = new NotifCollectionInconsistencyTracker(mLogger);
     }
 
     /** Initializes the NotifCollection and registers it to receive notification events. */
@@ -199,6 +199,7 @@
         mAttached = true;
         mDumpManager.registerDumpable(TAG, this);
         groupCoalescer.setNotificationHandler(mNotifHandler);
+        mInconsistencyTracker.attach(mNotificationSet::keySet, groupCoalescer::getCoalescedKeySet);
     }
 
     /**
@@ -598,14 +599,9 @@
                 }
             }
         }
-        NotifCollectionLoggerKt.maybeLogInconsistentRankings(
-                mLogger,
-                mNotificationsWithoutRankings,
-                currentEntriesWithoutRankings,
-                rankingMap
-        );
-        mNotificationsWithoutRankings = currentEntriesWithoutRankings == null
-                ? Collections.emptySet() : currentEntriesWithoutRankings.keySet();
+
+        mInconsistencyTracker.logNewMissingNotifications(rankingMap);
+        mInconsistencyTracker.logNewInconsistentRankings(currentEntriesWithoutRankings, rankingMap);
         if (currentEntriesWithoutRankings != null && mNotifPipelineFlags.removeUnrankedNotifs()) {
             for (NotificationEntry entry : currentEntriesWithoutRankings.values()) {
                 entry.mCancellationReason = REASON_UNKNOWN;
@@ -864,10 +860,7 @@
                         true,
                         "\t\t"));
 
-        pw.println("\n\tmNotificationsWithoutRankings: " + mNotificationsWithoutRankings.size());
-        for (String key : mNotificationsWithoutRankings) {
-            pw.println("\t * : " + key);
-        }
+        mInconsistencyTracker.dump(pw);
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescer.java
index 98f2167..96b3542 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coalescer/GroupCoalescer.java
@@ -39,9 +39,11 @@
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 import javax.inject.Inject;
 
@@ -118,6 +120,11 @@
         mHandler = handler;
     }
 
+    /** @return the set of notification keys currently in the coalescer */
+    public Set<String> getCoalescedKeySet() {
+        return Collections.unmodifiableSet(mCoalescedEvents.keySet());
+    }
+
     private final NotificationHandler mListener = new NotificationHandler() {
         @Override
         public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinder.java
index 46b467e..d52f3c6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinder.java
@@ -16,9 +16,8 @@
 
 package com.android.systemui.statusbar.notification.collection.inflation;
 
-import android.annotation.Nullable;
+import android.annotation.NonNull;
 
-import com.android.systemui.statusbar.NotificationUiAdjustment;
 import com.android.systemui.statusbar.notification.InflationException;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -35,23 +34,11 @@
      */
     void inflateViews(
             NotificationEntry entry,
-            NotifInflater.Params params,
+            @NonNull NotifInflater.Params params,
             NotificationRowContentBinder.InflationCallback callback)
             throws InflationException;
 
     /**
-     * Called when the ranking has been updated (but not add or remove has been done). The binder
-     * should inspect the old and new adjustments and re-inflate the entry's views if necessary
-     * (e.g. if something important changed).
-     */
-    void onNotificationRankingUpdated(
-            NotificationEntry entry,
-            @Nullable Integer oldImportance,
-            NotificationUiAdjustment oldAdjustment,
-            NotificationUiAdjustment newAdjustment,
-            NotificationRowContentBinder.InflationCallback callback);
-
-    /**
      * Called when a notification is no longer likely to be displayed and can have its views freed.
      */
     void releaseViews(NotificationEntry entry);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
index 528f720..47cdde4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java
@@ -22,6 +22,7 @@
 
 import static java.util.Objects.requireNonNull;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.Context;
 import android.os.Build;
@@ -32,12 +33,9 @@
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationPresenter;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
-import com.android.systemui.statusbar.NotificationUiAdjustment;
 import com.android.systemui.statusbar.notification.InflationException;
-import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.statusbar.notification.NotificationClicker;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-import com.android.systemui.statusbar.notification.collection.legacy.LowPriorityInflationHelper;
 import com.android.systemui.statusbar.notification.icon.IconManager;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowController;
@@ -68,8 +66,6 @@
     private final ExpandableNotificationRowComponent.Builder
             mExpandableNotificationRowComponentBuilder;
     private final IconManager mIconManager;
-    private final LowPriorityInflationHelper mLowPriorityInflationHelper;
-    private final NotifPipelineFlags mNotifPipelineFlags;
 
     private NotificationPresenter mPresenter;
     private NotificationListContainer mListContainer;
@@ -86,9 +82,7 @@
             RowContentBindStage rowContentBindStage,
             Provider<RowInflaterTask> rowInflaterTaskProvider,
             ExpandableNotificationRowComponent.Builder expandableNotificationRowComponentBuilder,
-            IconManager iconManager,
-            LowPriorityInflationHelper lowPriorityInflationHelper,
-            NotifPipelineFlags notifPipelineFlags) {
+            IconManager iconManager) {
         mContext = context;
         mNotifBindPipeline = notifBindPipeline;
         mRowContentBindStage = rowContentBindStage;
@@ -98,8 +92,6 @@
         mRowInflaterTaskProvider = rowInflaterTaskProvider;
         mExpandableNotificationRowComponentBuilder = expandableNotificationRowComponentBuilder;
         mIconManager = iconManager;
-        mLowPriorityInflationHelper = lowPriorityInflationHelper;
-        mNotifPipelineFlags = notifPipelineFlags;
     }
 
     /**
@@ -125,13 +117,9 @@
     @Override
     public void inflateViews(
             NotificationEntry entry,
-            NotifInflater.Params params,
+            @NonNull NotifInflater.Params params,
             NotificationRowContentBinder.InflationCallback callback)
             throws InflationException {
-        if (params == null) {
-            // weak assert that the params should always be passed in the new pipeline
-            mNotifPipelineFlags.checkLegacyPipelineEnabled();
-        }
         ViewGroup parent = mListContainer.getViewParentForNotification(entry);
 
         if (entry.rowExists()) {
@@ -191,39 +179,6 @@
     }
 
     /**
-     * Updates the views bound to an entry when the entry's ranking changes, either in-place or by
-     * reinflating them.
-     *
-     * TODO: Should this method be in this class?
-     */
-    @Override
-    public void onNotificationRankingUpdated(
-            NotificationEntry entry,
-            @Nullable Integer oldImportance,
-            NotificationUiAdjustment oldAdjustment,
-            NotificationUiAdjustment newAdjustment,
-            NotificationRowContentBinder.InflationCallback callback) {
-        mNotifPipelineFlags.checkLegacyPipelineEnabled();
-        if (NotificationUiAdjustment.needReinflate(oldAdjustment, newAdjustment)) {
-            if (entry.rowExists()) {
-                ExpandableNotificationRow row = entry.getRow();
-                row.reset();
-                updateRow(entry, row);
-                inflateContentViews(entry, null, row, callback);
-            } else {
-                // Once the RowInflaterTask is done, it will pick up the updated entry, so
-                // no-op here.
-            }
-        } else {
-            if (oldImportance != null && entry.getImportance() != oldImportance) {
-                if (entry.rowExists()) {
-                    entry.getRow().onNotificationRankingUpdated();
-                }
-            }
-        }
-    }
-
-    /**
      * Update row after the notification has updated.
      *
      * @param entry notification that has updated
@@ -243,24 +198,12 @@
      */
     private void inflateContentViews(
             NotificationEntry entry,
-            NotifInflater.Params inflaterParams,
+            @NonNull NotifInflater.Params inflaterParams,
             ExpandableNotificationRow row,
             @Nullable NotificationRowContentBinder.InflationCallback inflationCallback) {
         final boolean useIncreasedCollapsedHeight =
                 mMessagingUtil.isImportantMessaging(entry.getSbn(), entry.getImportance());
-        final boolean isLowPriority;
-        if (inflaterParams != null) {
-            // NEW pipeline
-            isLowPriority = inflaterParams.isLowPriority();
-        } else {
-            // LEGACY pipeline
-            mNotifPipelineFlags.checkLegacyPipelineEnabled();
-            // If this is our first time inflating, we don't actually know the groupings for real
-            // yet, so we might actually inflate a low priority content view incorrectly here and
-            // have to correct it later in the pipeline. On subsequent inflations (i.e. updates),
-            // this should inflate the correct view.
-            isLowPriority = mLowPriorityInflationHelper.shouldUseLowPriorityView(entry);
-        }
+        final boolean isLowPriority = inflaterParams.isLowPriority();
 
         RowContentBindParams params = mRowContentBindStage.getStageParams(entry);
         params.requireContentViews(FLAG_CONTENT_VIEW_CONTRACTED);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/LowPriorityInflationHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/LowPriorityInflationHelper.java
deleted file mode 100644
index 89445a5..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/LowPriorityInflationHelper.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright (C) 2020 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.notification.collection.legacy;
-
-import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.statusbar.notification.NotifPipelineFlags;
-import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-
-import javax.inject.Inject;
-
-/**
- * Helper class that provide methods to help check when we need to inflate a low priority version
- * ot notification content.
- */
-@SysUISingleton
-public class LowPriorityInflationHelper {
-    private final NotificationGroupManagerLegacy mGroupManager;
-    private final NotifPipelineFlags mNotifPipelineFlags;
-
-    @Inject
-    LowPriorityInflationHelper(
-            NotificationGroupManagerLegacy groupManager,
-            NotifPipelineFlags notifPipelineFlags) {
-        mGroupManager = groupManager;
-        mNotifPipelineFlags = notifPipelineFlags;
-    }
-
-    /**
-     * Whether the notification should inflate a low priority version of its content views.
-     */
-    public boolean shouldUseLowPriorityView(NotificationEntry entry) {
-        mNotifPipelineFlags.checkLegacyPipelineEnabled();
-        return entry.isAmbient() && !mGroupManager.isChildInGroup(entry);
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/NotificationGroupManagerLegacy.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/NotificationGroupManagerLegacy.java
deleted file mode 100644
index d41f6fe..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/NotificationGroupManagerLegacy.java
+++ /dev/null
@@ -1,950 +0,0 @@
-/*
- * Copyright (C) 2015 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.notification.collection.legacy;
-
-import static com.android.systemui.statusbar.notification.NotificationUtils.logKey;
-
-import static java.util.Objects.requireNonNull;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.app.Notification;
-import android.service.notification.StatusBarNotification;
-import android.util.ArraySet;
-import android.util.Log;
-
-import com.android.systemui.Dumpable;
-import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.dump.DumpManager;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
-import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager.OnGroupExpansionChangeListener;
-import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
-import com.android.systemui.util.Compile;
-import com.android.wm.shell.bubbles.Bubbles;
-
-import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.TreeSet;
-import java.util.function.Function;
-
-import javax.inject.Inject;
-
-import dagger.Lazy;
-
-/**
- * A class to handle notifications and their corresponding groups.
- * This includes:
- * 1. Determining whether an entry is a member of a group and whether it is a summary or a child
- * 2. Tracking group expansion states
- */
-@SysUISingleton
-public class NotificationGroupManagerLegacy implements StateListener, Dumpable {
-
-    private static final String TAG = "LegacyNotifGroupManager";
-    private static final boolean DEBUG = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.DEBUG);
-    private static final boolean SPEW = Compile.IS_DEBUG && Log.isLoggable(TAG, Log.VERBOSE);
-    /**
-     * The maximum amount of time (in ms) between the posting of notifications that can be
-     * considered part of the same update batch.
-     */
-    private static final long POST_BATCH_MAX_AGE = 5000;
-    private final HashMap<String, NotificationGroup> mGroupMap = new HashMap<>();
-    private final Lazy<PeopleNotificationIdentifier> mPeopleNotificationIdentifier;
-    private final Optional<Bubbles> mBubblesOptional;
-    private final GroupEventDispatcher mEventDispatcher = new GroupEventDispatcher(mGroupMap::get);
-    private final HashMap<String, StatusBarNotification> mIsolatedEntries = new HashMap<>();
-    private boolean mIsUpdatingUnchangedGroup;
-
-    @Inject
-    public NotificationGroupManagerLegacy(
-            StatusBarStateController statusBarStateController,
-            Lazy<PeopleNotificationIdentifier> peopleNotificationIdentifier,
-            Optional<Bubbles> bubblesOptional,
-            DumpManager dumpManager) {
-        statusBarStateController.addCallback(this);
-        mPeopleNotificationIdentifier = peopleNotificationIdentifier;
-        mBubblesOptional = bubblesOptional;
-
-        dumpManager.registerDumpable(this);
-    }
-
-    /**
-     * Add a listener for changes to groups.
-     */
-    public void registerGroupChangeListener(OnGroupChangeListener listener) {
-        mEventDispatcher.registerGroupChangeListener(listener);
-    }
-
-    private void setGroupExpanded(NotificationGroup group, boolean expanded) {
-        group.expanded = expanded;
-    }
-
-    /**
-     * When we want to remove an entry from being tracked for grouping
-     */
-    public void onEntryRemoved(NotificationEntry removed) {
-        if (SPEW) {
-            Log.d(TAG, "onEntryRemoved: entry=" + logKey(removed));
-        }
-        mEventDispatcher.openBufferScope();
-        onEntryRemovedInternal(removed, removed.getSbn());
-        StatusBarNotification oldSbn = mIsolatedEntries.remove(removed.getKey());
-        if (oldSbn != null) {
-            updateSuppression(mGroupMap.get(oldSbn.getGroupKey()));
-        }
-        mEventDispatcher.closeBufferScope();
-    }
-
-    /**
-     * An entry was removed.
-     *
-     * @param removed the removed entry
-     * @param sbn the notification the entry has, which doesn't need to be the same as it's internal
-     *            notification
-     */
-    private void onEntryRemovedInternal(NotificationEntry removed,
-            final StatusBarNotification sbn) {
-        onEntryRemovedInternal(removed, sbn.getGroupKey(), sbn.isGroup(),
-                sbn.getNotification().isGroupSummary());
-    }
-
-    private void onEntryRemovedInternal(NotificationEntry removed, String notifGroupKey, boolean
-            isGroup, boolean isGroupSummary) {
-        String groupKey = getGroupKey(removed.getKey(), notifGroupKey);
-        final NotificationGroup group = mGroupMap.get(groupKey);
-        if (group == null) {
-            // When an app posts 2 different notifications as summary of the same group, then a
-            // cancellation of the first notification removes this group.
-            // This situation is not supported and we will not allow such notifications anymore in
-            // the close future. See b/23676310 for reference.
-            return;
-        }
-        if (SPEW) {
-            Log.d(TAG, "onEntryRemovedInternal: entry=" + logKey(removed)
-                    + " group=" + logGroupKey(group));
-        }
-        if (isGroupChild(removed.getKey(), isGroup, isGroupSummary)) {
-            group.children.remove(removed.getKey());
-        } else {
-            group.summary = null;
-        }
-        updateSuppression(group);
-        if (group.children.isEmpty()) {
-            if (group.summary == null) {
-                mGroupMap.remove(groupKey);
-                mEventDispatcher.notifyGroupRemoved(group);
-            }
-        }
-    }
-
-    private void onEntryAddedInternal(final NotificationEntry added) {
-        if (added.isRowRemoved()) {
-            added.setDebugThrowable(new Throwable());
-        }
-        final StatusBarNotification sbn = added.getSbn();
-        boolean isGroupChild = isGroupChild(sbn);
-        String groupKey = getGroupKey(sbn);
-        NotificationGroup group = mGroupMap.get(groupKey);
-        if (group == null) {
-            group = new NotificationGroup(groupKey);
-            mGroupMap.put(groupKey, group);
-            mEventDispatcher.notifyGroupCreated(group);
-        }
-        if (SPEW) {
-            Log.d(TAG, "onEntryAddedInternal: entry=" + logKey(added)
-                    + " group=" + logGroupKey(group));
-        }
-        if (isGroupChild) {
-            NotificationEntry existing = group.children.get(added.getKey());
-            if (existing != null && existing != added) {
-                Throwable existingThrowable = existing.getDebugThrowable();
-                Log.wtf(TAG, "Inconsistent entries found with the same key " + logKey(added)
-                        + "existing removed: " + existing.isRowRemoved()
-                        + (existingThrowable != null
-                                ? Log.getStackTraceString(existingThrowable) + "\n" : "")
-                        + " added removed" + added.isRowRemoved(), new Throwable());
-            }
-            group.children.put(added.getKey(), added);
-            addToPostBatchHistory(group, added);
-            updateSuppression(group);
-        } else {
-            group.summary = added;
-            addToPostBatchHistory(group, added);
-            group.expanded = added.areChildrenExpanded();
-            updateSuppression(group);
-            if (!group.children.isEmpty()) {
-                ArrayList<NotificationEntry> childrenCopy =
-                        new ArrayList<>(group.children.values());
-                for (NotificationEntry child : childrenCopy) {
-                    onEntryBecomingChild(child);
-                }
-                mEventDispatcher.notifyGroupsChanged();
-            }
-        }
-    }
-
-    private void addToPostBatchHistory(NotificationGroup group, @Nullable NotificationEntry entry) {
-        if (entry == null) {
-            return;
-        }
-        boolean didAdd = group.postBatchHistory.add(new PostRecord(entry));
-        if (didAdd) {
-            trimPostBatchHistory(group.postBatchHistory);
-        }
-    }
-
-    /** remove all history that's too old to be in the batch. */
-    private void trimPostBatchHistory(@NonNull TreeSet<PostRecord> postBatchHistory) {
-        if (postBatchHistory.size() <= 1) {
-            return;
-        }
-        long batchStartTime = postBatchHistory.last().postTime - POST_BATCH_MAX_AGE;
-        while (!postBatchHistory.isEmpty() && postBatchHistory.first().postTime < batchStartTime) {
-            postBatchHistory.pollFirst();
-        }
-    }
-
-    private void onEntryBecomingChild(NotificationEntry entry) {
-        updateIsolation(entry);
-    }
-
-    private void updateSuppression(NotificationGroup group) {
-        if (group == null) {
-            return;
-        }
-        NotificationEntry prevAlertOverride = group.alertOverride;
-        group.alertOverride = getPriorityConversationAlertOverride(group);
-
-        int childCount = 0;
-        boolean hasBubbles = false;
-        for (NotificationEntry entry : group.children.values()) {
-            if (mBubblesOptional.isPresent() && mBubblesOptional.get()
-                    .isBubbleNotificationSuppressedFromShade(
-                            entry.getKey(), entry.getSbn().getGroupKey())) {
-                hasBubbles = true;
-            } else {
-                childCount++;
-            }
-        }
-
-        boolean prevSuppressed = group.suppressed;
-        group.suppressed = group.summary != null && !group.expanded
-                && (childCount == 1
-                || (childCount == 0
-                && group.summary.getSbn().getNotification().isGroupSummary()
-                && (hasIsolatedChildren(group) || hasBubbles)));
-
-        boolean alertOverrideChanged = prevAlertOverride != group.alertOverride;
-        boolean suppressionChanged = prevSuppressed != group.suppressed;
-        if (alertOverrideChanged || suppressionChanged) {
-            if (DEBUG) {
-                Log.d(TAG, "updateSuppression:"
-                        + " willNotifyListeners=" + !mIsUpdatingUnchangedGroup
-                        + " changes for group:\n" + group);
-                if (alertOverrideChanged) {
-                    Log.d(TAG, "updateSuppression: alertOverride was=" + logKey(prevAlertOverride)
-                            + " now=" + logKey(group.alertOverride));
-                }
-                if (suppressionChanged) {
-                    Log.d(TAG, "updateSuppression: suppressed changed to " + group.suppressed);
-                }
-            }
-            if (alertOverrideChanged) {
-                mEventDispatcher.notifyAlertOverrideChanged(group, prevAlertOverride);
-            }
-            if (suppressionChanged) {
-                mEventDispatcher.notifySuppressedChanged(group);
-            }
-            if (!mIsUpdatingUnchangedGroup) {
-                mEventDispatcher.notifyGroupsChanged();
-            }
-        }
-    }
-
-    /**
-     * Finds the isolated logical child of this group which is should be alerted instead.
-     *
-     * Notifications from priority conversations are isolated from their groups to make them more
-     * prominent, however apps may post these with a GroupAlertBehavior that has the group receiving
-     * the alert.  This would lead to the group alerting even though the conversation that was
-     * updated was not actually a part of that group.  This method finds the best priority
-     * conversation in this situation, if there is one, so they can be set as the alertOverride of
-     * the group.
-     *
-     * @param group the group to check
-     * @return the entry which should receive the alert instead of the group, if any.
-     */
-    @Nullable
-    private NotificationEntry getPriorityConversationAlertOverride(NotificationGroup group) {
-        // GOAL: if there is a priority child which wouldn't alert based on its groupAlertBehavior,
-        // but which should be alerting (because priority conversations are isolated), find it.
-        if (group == null || group.summary == null) {
-            if (SPEW) {
-                Log.d(TAG, "getPriorityConversationAlertOverride: null group or summary"
-                        + " group=" + logGroupKey(group));
-            }
-            return null;
-        }
-        if (isIsolated(group.summary.getKey())) {
-            if (SPEW) {
-                Log.d(TAG, "getPriorityConversationAlertOverride: isolated group"
-                        + " group=" + logGroupKey(group));
-            }
-            return null;
-        }
-
-        // Precondiions:
-        // * Only necessary when all notifications in the group use GROUP_ALERT_SUMMARY
-        // * Only necessary when at least one notification in the group is on a priority channel
-        if (group.summary.getSbn().getNotification().getGroupAlertBehavior()
-                == Notification.GROUP_ALERT_CHILDREN) {
-            if (SPEW) {
-                Log.d(TAG, "getPriorityConversationAlertOverride: summary == GROUP_ALERT_CHILDREN"
-                        + " group=" + logGroupKey(group));
-            }
-            return null;
-        }
-
-        // Get the important children first, copy the keys for the final importance check,
-        // then add the non-isolated children to the map for unified lookup.
-        HashMap<String, NotificationEntry> children = getImportantConversations(group);
-        if (children == null || children.isEmpty()) {
-            if (SPEW) {
-                Log.d(TAG, "getPriorityConversationAlertOverride: no important conversations"
-                        + " group=" + logGroupKey(group));
-            }
-            return null;
-        }
-        HashSet<String> importantChildKeys = new HashSet<>(children.keySet());
-        children.putAll(group.children);
-
-        // Ensure all children have GROUP_ALERT_SUMMARY
-        for (NotificationEntry child : children.values()) {
-            if (child.getSbn().getNotification().getGroupAlertBehavior()
-                    != Notification.GROUP_ALERT_SUMMARY) {
-                if (SPEW) {
-                    Log.d(TAG, "getPriorityConversationAlertOverride: child != GROUP_ALERT_SUMMARY"
-                            + " group=" + logGroupKey(group));
-                }
-                return null;
-            }
-        }
-
-        // Create a merged post history from all the children
-        TreeSet<PostRecord> combinedHistory = new TreeSet<>(group.postBatchHistory);
-        for (String importantChildKey : importantChildKeys) {
-            NotificationGroup importantChildGroup = mGroupMap.get(importantChildKey);
-            combinedHistory.addAll(importantChildGroup.postBatchHistory);
-        }
-        trimPostBatchHistory(combinedHistory);
-
-        // This is a streamlined implementation of the following idea:
-        // * From the subset of notifications in the latest 'batch' of updates.  A batch is:
-        //   * Notifs posted less than POST_BATCH_MAX_AGE before the most recently posted.
-        //   * Only including notifs newer than the second-to-last post of any notification.
-        // * Find the newest child in the batch -- the with the largest 'when' value.
-        // * If the newest child is a priority conversation, set that as the override.
-        HashSet<String> batchKeys = new HashSet<>();
-        long newestChildWhen = -1;
-        NotificationEntry newestChild = null;
-        // Iterate backwards through the post history, tracking the child with the smallest sort key
-        for (PostRecord record : combinedHistory.descendingSet()) {
-            if (batchKeys.contains(record.key)) {
-                // Once you see a notification again, the batch has ended
-                break;
-            }
-            batchKeys.add(record.key);
-            NotificationEntry child = children.get(record.key);
-            if (child != null) {
-                long childWhen = child.getSbn().getNotification().when;
-                if (newestChild == null || childWhen > newestChildWhen) {
-                    newestChildWhen = childWhen;
-                    newestChild = child;
-                }
-            }
-        }
-        if (newestChild != null && importantChildKeys.contains(newestChild.getKey())) {
-            if (SPEW) {
-                Log.d(TAG, "getPriorityConversationAlertOverride:"
-                        + " result=" + logKey(newestChild)
-                        + " group=" + logGroupKey(group));
-            }
-            return newestChild;
-        }
-        if (SPEW) {
-            Log.d(TAG, "getPriorityConversationAlertOverride:"
-                    + " result=null newestChild=" + logKey(newestChild)
-                    + " group=" + logGroupKey(group));
-        }
-        return null;
-    }
-
-    private boolean hasIsolatedChildren(NotificationGroup group) {
-        return getNumberOfIsolatedChildren(group.summary.getSbn().getGroupKey()) != 0;
-    }
-
-    private int getNumberOfIsolatedChildren(String groupKey) {
-        int count = 0;
-        for (StatusBarNotification sbn : mIsolatedEntries.values()) {
-            if (sbn.getGroupKey().equals(groupKey) && isIsolated(sbn.getKey())) {
-                count++;
-            }
-        }
-        return count;
-    }
-
-    @Nullable
-    private HashMap<String, NotificationEntry> getImportantConversations(NotificationGroup group) {
-        String groupKey = group.summary.getSbn().getGroupKey();
-        HashMap<String, NotificationEntry> result = null;
-        for (StatusBarNotification sbn : mIsolatedEntries.values()) {
-            if (sbn.getGroupKey().equals(groupKey)) {
-                NotificationEntry entry = mGroupMap.get(sbn.getKey()).summary;
-                if (isImportantConversation(entry)) {
-                    if (result == null) {
-                        result = new HashMap<>();
-                    }
-                    result.put(sbn.getKey(), entry);
-                }
-            }
-        }
-        return result;
-    }
-
-    private void setStatusBarState(int newState) {
-        if (newState == StatusBarState.KEYGUARD) {
-            collapseGroups();
-        }
-    }
-
-    private void collapseGroups() {
-        // Because notifications can become isolated when the group becomes suppressed it can
-        // lead to concurrent modifications while looping. We need to make a copy.
-        ArrayList<NotificationGroup> groupCopy = new ArrayList<>(mGroupMap.values());
-        int size = groupCopy.size();
-        for (int i = 0; i < size; i++) {
-            NotificationGroup group =  groupCopy.get(i);
-            if (group.expanded) {
-                setGroupExpanded(group, false);
-            }
-            updateSuppression(group);
-        }
-    }
-
-    public boolean isChildInGroup(NotificationEntry entry) {
-        final StatusBarNotification sbn = entry.getSbn();
-        if (!isGroupChild(sbn)) {
-            return false;
-        }
-        NotificationGroup group = mGroupMap.get(getGroupKey(sbn));
-        if (group == null || group.summary == null || group.suppressed) {
-            return false;
-        }
-        if (group.children.isEmpty()) {
-            // If the suppression of a group changes because the last child was removed, this can
-            // still be called temporarily because the child hasn't been fully removed yet. Let's
-            // make sure we still return false in that case.
-            return false;
-        }
-        return true;
-    }
-
-    /**
-     * If there is a {@link NotificationGroup} associated with the provided entry, this method
-     * will update the suppression of that group.
-     */
-    public void updateSuppression(NotificationEntry entry) {
-        NotificationGroup group = mGroupMap.get(getGroupKey(entry.getSbn()));
-        if (group != null) {
-            updateSuppression(group);
-        }
-    }
-
-    /**
-     * Get the group key. May differ from the one in the notification due to the notification
-     * being temporarily isolated.
-     *
-     * @param sbn notification to check
-     * @return the key of the notification
-     */
-    private String getGroupKey(StatusBarNotification sbn) {
-        return getGroupKey(sbn.getKey(), sbn.getGroupKey());
-    }
-
-    private String getGroupKey(String key, String groupKey) {
-        if (isIsolated(key)) {
-            return key;
-        }
-        return groupKey;
-    }
-
-    private boolean isIsolated(String sbnKey) {
-        return mIsolatedEntries.containsKey(sbnKey);
-    }
-
-    /**
-     * Whether a notification is visually a group child.
-     *
-     * @param sbn notification to check
-     * @return true if it is visually a group child
-     */
-    private boolean isGroupChild(StatusBarNotification sbn) {
-        return isGroupChild(sbn.getKey(), sbn.isGroup(), sbn.getNotification().isGroupSummary());
-    }
-
-    private boolean isGroupChild(String key, boolean isGroup, boolean isGroupSummary) {
-        if (isIsolated(key)) {
-            return false;
-        }
-        return isGroup && !isGroupSummary;
-    }
-
-    /**
-     * Whether a notification that is normally part of a group should be temporarily isolated from
-     * the group and put in their own group visually.  This generally happens when the notification
-     * is alerting.
-     *
-     * @param entry the notification to check
-     * @return true if the entry should be isolated
-     */
-    private boolean shouldIsolate(NotificationEntry entry) {
-        StatusBarNotification sbn = entry.getSbn();
-        if (!sbn.isGroup() || sbn.getNotification().isGroupSummary()) {
-            return false;
-        }
-        if (isImportantConversation(entry)) {
-            return true;
-        }
-        NotificationGroup notificationGroup = mGroupMap.get(sbn.getGroupKey());
-        return (sbn.getNotification().fullScreenIntent != null
-                    || notificationGroup == null
-                    || !notificationGroup.expanded
-                    || isGroupNotFullyVisible(notificationGroup));
-    }
-
-    private boolean isImportantConversation(NotificationEntry entry) {
-        int peopleNotificationType =
-                mPeopleNotificationIdentifier.get().getPeopleNotificationType(entry);
-        return peopleNotificationType == PeopleNotificationIdentifier.TYPE_IMPORTANT_PERSON;
-    }
-
-    /**
-     * Isolate a notification from its group so that it visually shows as its own group.
-     *
-     * @param entry the notification to isolate
-     */
-    private void isolateNotification(NotificationEntry entry) {
-        if (SPEW) {
-            Log.d(TAG, "isolateNotification: entry=" + logKey(entry));
-        }
-        // We will be isolated now, so lets update the groups
-        onEntryRemovedInternal(entry, entry.getSbn());
-
-        mIsolatedEntries.put(entry.getKey(), entry.getSbn());
-
-        onEntryAddedInternal(entry);
-        // We also need to update the suppression of the old group, because this call comes
-        // even before the groupManager knows about the notification at all.
-        // When the notification gets added afterwards it is already isolated and therefore
-        // it doesn't lead to an update.
-        updateSuppression(mGroupMap.get(entry.getSbn().getGroupKey()));
-        mEventDispatcher.notifyGroupsChanged();
-    }
-
-    /**
-     * Update the isolation of an entry, splitting it from the group.
-     */
-    private void updateIsolation(NotificationEntry entry) {
-        // We need to buffer a few events because we do isolation changes in 3 steps:
-        // removeInternal, update mIsolatedEntries, addInternal.  This means that often the
-        // alertOverride will update on the removal, however processing the event in that case can
-        // cause problems because the mIsolatedEntries map is not in its final state, so the event
-        // listener may be unable to correctly determine the true state of the group.  By delaying
-        // the alertOverride change until after the add phase, we can ensure that listeners only
-        // have to handle a consistent state.
-        mEventDispatcher.openBufferScope();
-        boolean isIsolated = isIsolated(entry.getSbn().getKey());
-        if (shouldIsolate(entry)) {
-            if (!isIsolated) {
-                isolateNotification(entry);
-            }
-        } else if (isIsolated) {
-            stopIsolatingNotification(entry);
-        }
-        mEventDispatcher.closeBufferScope();
-    }
-
-    /**
-     * Stop isolating a notification and re-group it with its original logical group.
-     *
-     * @param entry the notification to un-isolate
-     */
-    private void stopIsolatingNotification(NotificationEntry entry) {
-        if (SPEW) {
-            Log.d(TAG, "stopIsolatingNotification: entry=" + logKey(entry));
-        }
-        // not isolated anymore, we need to update the groups
-        onEntryRemovedInternal(entry, entry.getSbn());
-        mIsolatedEntries.remove(entry.getKey());
-        onEntryAddedInternal(entry);
-        mEventDispatcher.notifyGroupsChanged();
-    }
-
-    private boolean isGroupNotFullyVisible(NotificationGroup notificationGroup) {
-        return notificationGroup.summary == null
-                || notificationGroup.summary.isGroupNotFullyVisible();
-    }
-
-    @Override
-    public void dump(PrintWriter pw, String[] args) {
-        pw.println("GroupManagerLegacy state:");
-        pw.println("  number of groups: " +  mGroupMap.size());
-        for (Map.Entry<String, NotificationGroup>  entry : mGroupMap.entrySet()) {
-            pw.println("\n    key: " + logKey(entry.getKey())); pw.println(entry.getValue());
-        }
-        pw.println("\n    isolated entries: " +  mIsolatedEntries.size());
-        for (Map.Entry<String, StatusBarNotification> entry : mIsolatedEntries.entrySet()) {
-            pw.print("      "); pw.print(logKey(entry.getKey()));
-            pw.print(", "); pw.println(entry.getValue());
-        }
-    }
-
-    @Override
-    public void onStateChanged(int newState) {
-        setStatusBarState(newState);
-    }
-
-    /** Get the group key, reformatted for logging, for the (optional) group */
-    private static String logGroupKey(NotificationGroup group) {
-        if (group == null) {
-            return "null";
-        }
-        return logKey(group.groupKey);
-    }
-
-    /**
-     * A record of a notification being posted, containing the time of the post and the key of the
-     * notification entry.  These are stored in a TreeSet by the NotificationGroup and used to
-     * calculate a batch of notifications.
-     */
-    public static class PostRecord implements Comparable<PostRecord> {
-        public final long postTime;
-        public final String key;
-
-        /** constructs a record containing the post time and key from the notification entry */
-        public PostRecord(@NonNull NotificationEntry entry) {
-            this.postTime = entry.getSbn().getPostTime();
-            this.key = entry.getKey();
-        }
-
-        @Override
-        public int compareTo(PostRecord o) {
-            int postTimeComparison = Long.compare(this.postTime, o.postTime);
-            return postTimeComparison == 0
-                    ? String.CASE_INSENSITIVE_ORDER.compare(this.key, o.key)
-                    : postTimeComparison;
-        }
-
-        @Override
-        public boolean equals(Object o) {
-            if (this == o) return true;
-            if (o == null || getClass() != o.getClass()) return false;
-            PostRecord that = (PostRecord) o;
-            return postTime == that.postTime && key.equals(that.key);
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hash(postTime, key);
-        }
-    }
-
-    /**
-     * Represents a notification group in the notification shade.
-     */
-    public static class NotificationGroup {
-        public final String groupKey;
-        public final HashMap<String, NotificationEntry> children = new HashMap<>();
-        public final TreeSet<PostRecord> postBatchHistory = new TreeSet<>();
-        public NotificationEntry summary;
-        public boolean expanded;
-        /**
-         * Is this notification group suppressed, i.e its summary is hidden
-         */
-        public boolean suppressed;
-        /**
-         * The child (which is isolated from this group) to which the alert should be transferred,
-         * due to priority conversations.
-         */
-        public NotificationEntry alertOverride;
-
-        NotificationGroup(String groupKey) {
-            this.groupKey = groupKey;
-        }
-
-        @Override
-        public String toString() {
-            StringBuilder sb = new StringBuilder();
-            sb.append("    groupKey: ").append(groupKey);
-            sb.append("\n    summary:");
-            appendEntry(sb, summary);
-            sb.append("\n    children size: ").append(children.size());
-            for (NotificationEntry child : children.values()) {
-                appendEntry(sb, child);
-            }
-            sb.append("\n    alertOverride:");
-            appendEntry(sb, alertOverride);
-            sb.append("\n    summary suppressed: ").append(suppressed);
-            return sb.toString();
-        }
-
-        private void appendEntry(StringBuilder sb, NotificationEntry entry) {
-            sb.append("\n      ").append(entry != null ? entry.getSbn() : "null");
-            if (entry != null && entry.getDebugThrowable() != null) {
-                sb.append(Log.getStackTraceString(entry.getDebugThrowable()));
-            }
-        }
-    }
-
-    /**
-     * This class is a toggleable buffer for a subset of events of {@link OnGroupChangeListener}.
-     * When buffering, instead of notifying the listeners it will set internal state that will allow
-     * it to notify listeners of those events later
-     */
-    static class GroupEventDispatcher {
-        private final Function<String, NotificationGroup> mGroupMapGetter;
-        private final ArraySet<OnGroupChangeListener> mGroupChangeListeners = new ArraySet<>();
-        private final HashMap<String, NotificationEntry> mOldAlertOverrideByGroup = new HashMap<>();
-        private final HashMap<String, Boolean> mOldSuppressedByGroup = new HashMap<>();
-        private int mBufferScopeDepth = 0;
-        private boolean mDidGroupsChange = false;
-
-        GroupEventDispatcher(Function<String, NotificationGroup> groupMapGetter) {
-            mGroupMapGetter = requireNonNull(groupMapGetter);
-        }
-
-        void registerGroupChangeListener(OnGroupChangeListener listener) {
-            mGroupChangeListeners.add(listener);
-        }
-
-        private boolean isBuffering() {
-            return mBufferScopeDepth > 0;
-        }
-
-        void notifyAlertOverrideChanged(NotificationGroup group,
-                NotificationEntry oldAlertOverride) {
-            if (isBuffering()) {
-                // The value in this map is the override before the event.  If there is an entry
-                // already in the map, then we are effectively coalescing two events, which means
-                // we need to preserve the original initial value.
-                if (!mOldAlertOverrideByGroup.containsKey(group.groupKey)) {
-                    mOldAlertOverrideByGroup.put(group.groupKey, oldAlertOverride);
-                }
-            } else {
-                for (OnGroupChangeListener listener : mGroupChangeListeners) {
-                    listener.onGroupAlertOverrideChanged(group, oldAlertOverride,
-                            group.alertOverride);
-                }
-            }
-        }
-
-        void notifySuppressedChanged(NotificationGroup group) {
-            if (isBuffering()) {
-                // The value in this map is the 'suppressed' before the event.  If there is a value
-                // already in the map, then we are effectively coalescing two events, which means
-                // we need to preserve the original initial value.
-                mOldSuppressedByGroup.putIfAbsent(group.groupKey, !group.suppressed);
-            } else {
-                for (OnGroupChangeListener listener : mGroupChangeListeners) {
-                    listener.onGroupSuppressionChanged(group, group.suppressed);
-                }
-            }
-        }
-
-        void notifyGroupsChanged() {
-            if (isBuffering()) {
-                mDidGroupsChange = true;
-            } else {
-                for (OnGroupChangeListener listener : mGroupChangeListeners) {
-                    listener.onGroupsChanged();
-                }
-            }
-        }
-
-        void notifyGroupCreated(NotificationGroup group) {
-            // NOTE: given how this event is used, it doesn't need to be buffered right now
-            final String groupKey = group.groupKey;
-            for (OnGroupChangeListener listener : mGroupChangeListeners) {
-                listener.onGroupCreated(group, groupKey);
-            }
-        }
-
-        void notifyGroupRemoved(NotificationGroup group) {
-            // NOTE: given how this event is used, it doesn't need to be buffered right now
-            final String groupKey = group.groupKey;
-            for (OnGroupChangeListener listener : mGroupChangeListeners) {
-                listener.onGroupRemoved(group, groupKey);
-            }
-        }
-
-        void openBufferScope() {
-            mBufferScopeDepth++;
-            if (SPEW) {
-                Log.d(TAG, "openBufferScope: scopeDepth=" + mBufferScopeDepth);
-            }
-        }
-
-        void closeBufferScope() {
-            mBufferScopeDepth--;
-            if (SPEW) {
-                Log.d(TAG, "closeBufferScope: scopeDepth=" + mBufferScopeDepth);
-            }
-            // Flush buffered events if the last buffer scope has closed
-            if (!isBuffering()) {
-                flushBuffer();
-            }
-        }
-
-        private void flushBuffer() {
-            if (SPEW) {
-                Log.d(TAG, "flushBuffer: "
-                        + " suppressed.size=" + mOldSuppressedByGroup.size()
-                        + " alertOverride.size=" + mOldAlertOverrideByGroup.size()
-                        + " mDidGroupsChange=" + mDidGroupsChange);
-            }
-            // alert all group suppressed changes for groups that were not removed
-            for (Map.Entry<String, Boolean> entry : mOldSuppressedByGroup.entrySet()) {
-                NotificationGroup group = mGroupMapGetter.apply(entry.getKey());
-                if (group == null) {
-                    // The group can be null if this suppressed changed before the group was
-                    // permanently removed, meaning that there's no guarantee that listeners will
-                    // that field clear.
-                    if (SPEW) {
-                        Log.d(TAG, "flushBuffer: suppressed:"
-                                + " cannot report for removed group: " + logKey(entry.getKey()));
-                    }
-                    continue;
-                }
-                boolean oldSuppressed = entry.getValue();
-                if (group.suppressed == oldSuppressed) {
-                    // If the final suppressed equals the initial, it means we coalesced two
-                    // events which undid the change, so we can drop it entirely.
-                    if (SPEW) {
-                        Log.d(TAG, "flushBuffer: suppressed:"
-                                + " did not change for group: " + logKey(entry.getKey()));
-                    }
-                    continue;
-                }
-                notifySuppressedChanged(group);
-            }
-            mOldSuppressedByGroup.clear();
-            // alert all group alert override changes for groups that were not removed
-            for (Map.Entry<String, NotificationEntry> entry : mOldAlertOverrideByGroup.entrySet()) {
-                NotificationGroup group = mGroupMapGetter.apply(entry.getKey());
-                if (group == null) {
-                    // The group can be null if this alertOverride changed before the group was
-                    // permanently removed, meaning that there's no guarantee that listeners will
-                    // that field clear.
-                    if (SPEW) {
-                        Log.d(TAG, "flushBuffer: alertOverride:"
-                                + " cannot report for removed group: " + entry.getKey());
-                    }
-                    continue;
-                }
-                NotificationEntry oldAlertOverride = entry.getValue();
-                if (group.alertOverride == oldAlertOverride) {
-                    // If the final alertOverride equals the initial, it means we coalesced two
-                    // events which undid the change, so we can drop it entirely.
-                    if (SPEW) {
-                        Log.d(TAG, "flushBuffer: alertOverride:"
-                                + " did not change for group: " + logKey(entry.getKey()));
-                    }
-                    continue;
-                }
-                notifyAlertOverrideChanged(group, oldAlertOverride);
-            }
-            mOldAlertOverrideByGroup.clear();
-            // alert that groups changed
-            if (mDidGroupsChange) {
-                notifyGroupsChanged();
-                mDidGroupsChange = false;
-            }
-        }
-    }
-
-    /**
-     * Listener for group changes not including group expansion changes which are handled by
-     * {@link OnGroupExpansionChangeListener}.
-     */
-    public interface OnGroupChangeListener {
-        /**
-         * A new group has been created.
-         *
-         * @param group the group that was created
-         * @param groupKey the group's key
-         */
-        default void onGroupCreated(
-                NotificationGroup group,
-                String groupKey) {}
-
-        /**
-         * A group has been removed.
-         *
-         * @param group the group that was removed
-         * @param groupKey the group's key
-         */
-        default void onGroupRemoved(
-                NotificationGroup group,
-                String groupKey) {}
-
-        /**
-         * The suppression of a group has changed.
-         *
-         * @param group the group that has changed
-         * @param suppressed true if the group is now suppressed, false o/w
-         */
-        default void onGroupSuppressionChanged(
-                NotificationGroup group,
-                boolean suppressed) {}
-
-        /**
-         * The alert override of a group has changed.
-         *
-         * @param group the group that has changed
-         * @param oldAlertOverride the previous notification to which the group's alerts were sent
-         * @param newAlertOverride the notification to which the group's alerts should now be sent
-         */
-        default void onGroupAlertOverrideChanged(
-                NotificationGroup group,
-                @Nullable NotificationEntry oldAlertOverride,
-                @Nullable NotificationEntry newAlertOverride) {}
-
-        /**
-         * The groups have changed. This can happen if the isolation of a child has changes or if a
-         * group became suppressed / unsuppressed
-         */
-        default void onGroupsChanged() {}
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/VisualStabilityManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/VisualStabilityManager.java
deleted file mode 100644
index bb8c0e0..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/legacy/VisualStabilityManager.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright (C) 2016 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.notification.collection.legacy;
-
-import com.android.systemui.keyguard.WakefulnessLifecycle;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider;
-import com.android.systemui.statusbar.notification.dagger.NotificationsModule;
-
-/**
- * A manager that ensures that notifications are visually stable. It will suppress reorderings
- * and reorder at the right time when they are out of view.
- */
-public class VisualStabilityManager {
-
-    private final VisualStabilityProvider mVisualStabilityProvider;
-
-    private boolean mPanelExpanded;
-    private boolean mScreenOn;
-    private boolean mPulsing;
-
-    /**
-     * Injected constructor. See {@link NotificationsModule}.
-     */
-    public VisualStabilityManager(
-            VisualStabilityProvider visualStabilityProvider,
-            StatusBarStateController statusBarStateController,
-            WakefulnessLifecycle wakefulnessLifecycle) {
-
-        mVisualStabilityProvider = visualStabilityProvider;
-
-        if (statusBarStateController != null) {
-            setPulsing(statusBarStateController.isPulsing());
-            statusBarStateController.addCallback(new StatusBarStateController.StateListener() {
-                @Override
-                public void onPulsingChanged(boolean pulsing) {
-                    setPulsing(pulsing);
-                }
-
-                @Override
-                public void onExpandedChanged(boolean expanded) {
-                    setPanelExpanded(expanded);
-                }
-            });
-        }
-
-        if (wakefulnessLifecycle != null) {
-            wakefulnessLifecycle.addObserver(mWakefulnessObserver);
-        }
-    }
-
-    /**
-     * @param screenOn whether the screen is on
-     */
-    private void setScreenOn(boolean screenOn) {
-        mScreenOn = screenOn;
-        updateAllowedStates();
-    }
-
-    /**
-     * Set the panel to be expanded.
-     */
-    private void setPanelExpanded(boolean expanded) {
-        mPanelExpanded = expanded;
-        updateAllowedStates();
-    }
-
-    /**
-     * @param pulsing whether we are currently pulsing for ambient display.
-     */
-    private void setPulsing(boolean pulsing) {
-        if (mPulsing == pulsing) {
-            return;
-        }
-        mPulsing = pulsing;
-        updateAllowedStates();
-    }
-
-    private void updateAllowedStates() {
-        boolean reorderingAllowed = (!mScreenOn || !mPanelExpanded) && !mPulsing;
-        mVisualStabilityProvider.setReorderingAllowed(reorderingAllowed);
-    }
-
-    final WakefulnessLifecycle.Observer mWakefulnessObserver = new WakefulnessLifecycle.Observer() {
-        @Override
-        public void onFinishedGoingToSleep() {
-            setScreenOn(false);
-        }
-
-        @Override
-        public void onStartedWakingUp() {
-            setScreenOn(true);
-        }
-    };
-
-
-    /**
-     * See {@link Callback#onChangeAllowed()}
-     */
-    public interface Callback {
-
-        /**
-         * Called when changing is allowed again.
-         */
-        void onChangeAllowed();
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionInconsistencyTracker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionInconsistencyTracker.kt
new file mode 100644
index 0000000..8bce5b0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionInconsistencyTracker.kt
@@ -0,0 +1,123 @@
+/*
+ * 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.notification.collection.notifcollection
+
+import android.service.notification.NotificationListenerService.RankingMap
+import android.util.ArrayMap
+import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import java.io.PrintWriter
+
+class NotifCollectionInconsistencyTracker(val logger: NotifCollectionLogger) {
+    fun attach(
+        collectedKeySetAccessor: () -> Set<String>,
+        coalescedKeySetAccessor: () -> Set<String>,
+    ) {
+        if (attached) {
+            throw RuntimeException("attach() called twice")
+        }
+        attached = true
+        this.collectedKeySetAccessor = collectedKeySetAccessor
+        this.coalescedKeySetAccessor = coalescedKeySetAccessor
+    }
+
+    fun logNewMissingNotifications(rankingMap: RankingMap) {
+        val currentCollectedKeys = collectedKeySetAccessor()
+        val currentCoalescedKeys = coalescedKeySetAccessor()
+        val newMissingNotifications = rankingMap.orderedKeys.asSequence()
+            .filter { it !in currentCollectedKeys }
+            .filter { it !in currentCoalescedKeys }
+            .toSet()
+        maybeLogMissingNotifications(missingNotifications, newMissingNotifications)
+        missingNotifications = newMissingNotifications
+    }
+
+    @VisibleForTesting
+    fun maybeLogMissingNotifications(
+        oldMissingKeys: Set<String>,
+        newMissingKeys: Set<String>,
+    ) {
+        if (oldMissingKeys.isEmpty() && newMissingKeys.isEmpty()) return
+        if (oldMissingKeys == newMissingKeys) return
+        (oldMissingKeys - newMissingKeys).sorted().let { justFound ->
+            if (justFound.isNotEmpty()) {
+                logger.logFoundNotifications(justFound, newMissingKeys.size)
+            }
+        }
+        (newMissingKeys - oldMissingKeys).sorted().let { goneMissing ->
+            if (goneMissing.isNotEmpty()) {
+                logger.logMissingNotifications(goneMissing, newMissingKeys.size)
+            }
+        }
+    }
+
+    fun logNewInconsistentRankings(
+        currentEntriesWithoutRankings: ArrayMap<String, NotificationEntry>?,
+        rankingMap: RankingMap,
+    ) {
+        maybeLogInconsistentRankings(
+            notificationsWithoutRankings,
+            currentEntriesWithoutRankings ?: emptyMap(),
+            rankingMap
+        )
+        notificationsWithoutRankings = currentEntriesWithoutRankings?.keys ?: emptySet()
+    }
+
+    @VisibleForTesting
+    fun maybeLogInconsistentRankings(
+        oldKeysWithoutRankings: Set<String>,
+        newEntriesWithoutRankings: Map<String, NotificationEntry>,
+        rankingMap: RankingMap,
+    ) {
+        if (oldKeysWithoutRankings.isEmpty() && newEntriesWithoutRankings.isEmpty()) return
+        if (oldKeysWithoutRankings == newEntriesWithoutRankings.keys) return
+        val newlyConsistent: List<String> = oldKeysWithoutRankings
+            .mapNotNull { key ->
+                key.takeIf { key !in newEntriesWithoutRankings }
+                    .takeIf { key in rankingMap.orderedKeys }
+            }.sorted()
+        if (newlyConsistent.isNotEmpty()) {
+            val totalInconsistent: Int = newEntriesWithoutRankings.size
+            logger.logRecoveredRankings(newlyConsistent, totalInconsistent)
+        }
+        val newlyInconsistent: List<NotificationEntry> = newEntriesWithoutRankings
+            .mapNotNull { (key, entry) ->
+                entry.takeIf { key !in oldKeysWithoutRankings }
+            }.sortedBy { it.key }
+        if (newlyInconsistent.isNotEmpty()) {
+            val totalInconsistent: Int = newEntriesWithoutRankings.size
+            logger.logMissingRankings(newlyInconsistent, totalInconsistent, rankingMap)
+        }
+    }
+
+    fun dump(pw: PrintWriter) {
+        pw.println("notificationsWithoutRankings: ${notificationsWithoutRankings.size}")
+        for (key in notificationsWithoutRankings) {
+            pw.println("\t * : $key")
+        }
+        pw.println("missingNotifications: ${missingNotifications.size}")
+        for (key in missingNotifications) {
+            pw.println("\t * : $key")
+        }
+    }
+
+    private var attached: Boolean = false
+    private lateinit var collectedKeySetAccessor: (() -> Set<String>)
+    private lateinit var coalescedKeySetAccessor: (() -> Set<String>)
+    private var notificationsWithoutRankings = emptySet<String>()
+    private var missingNotifications = emptySet<String>()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt
index ebcac6b..aa27e1e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLogger.kt
@@ -215,8 +215,9 @@
         })
     }
 
-    fun logRecoveredRankings(newlyConsistentKeys: List<String>) {
+    fun logRecoveredRankings(newlyConsistentKeys: List<String>, totalInconsistent: Int) {
         buffer.log(TAG, INFO, {
+            int1 = totalInconsistent
             int1 = newlyConsistentKeys.size
             str1 = newlyConsistentKeys.joinToString { logKey(it) ?: "null" }
         }, {
@@ -224,6 +225,32 @@
         })
     }
 
+    fun logMissingNotifications(
+        newlyMissingKeys: List<String>,
+        totalMissing: Int,
+    ) {
+        buffer.log(TAG, WARNING, {
+            int1 = totalMissing
+            int2 = newlyMissingKeys.size
+            str1 = newlyMissingKeys.joinToString { logKey(it) ?: "null" }
+        }, {
+            "Collection missing $int1 entries in ranking update. Just lost $int2: $str1"
+        })
+    }
+
+    fun logFoundNotifications(
+        newlyFoundKeys: List<String>,
+        totalMissing: Int,
+    ) {
+        buffer.log(TAG, INFO, {
+            int1 = totalMissing
+            int2 = newlyFoundKeys.size
+            str1 = newlyFoundKeys.joinToString { logKey(it) ?: "null" }
+        }, {
+            "Collection missing $int1 entries in ranking update. Just found $int2: $str1"
+        })
+    }
+
     fun logRemoteExceptionOnNotificationClear(entry: NotificationEntry, e: RemoteException) {
         buffer.log(TAG, WTF, {
             str1 = entry.logKey
@@ -362,29 +389,4 @@
     }
 }
 
-fun maybeLogInconsistentRankings(
-    logger: NotifCollectionLogger,
-    oldKeysWithoutRankings: Set<String>,
-    newEntriesWithoutRankings: Map<String, NotificationEntry>?,
-    rankingMap: RankingMap
-) {
-    if (oldKeysWithoutRankings.isEmpty() && newEntriesWithoutRankings == null) return
-    val newlyConsistent: List<String> = oldKeysWithoutRankings
-        .mapNotNull { key ->
-            key.takeIf { key !in (newEntriesWithoutRankings ?: emptyMap()) }
-                .takeIf { key in rankingMap.orderedKeys }
-        }.sorted()
-    if (newlyConsistent.isNotEmpty()) {
-        logger.logRecoveredRankings(newlyConsistent)
-    }
-    val newlyInconsistent: List<NotificationEntry> = newEntriesWithoutRankings
-        ?.mapNotNull { (key, entry) ->
-            entry.takeIf { key !in oldKeysWithoutRankings }
-        }?.sortedBy { it.key } ?: emptyList()
-    if (newlyInconsistent.isNotEmpty()) {
-        val totalInconsistent: Int = newEntriesWithoutRankings?.size ?: 0
-        logger.logMissingRankings(newlyInconsistent, totalInconsistent, rankingMap)
-    }
-}
-
 private const val TAG = "NotifCollection"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
index eda2eec..9333c2a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
@@ -24,22 +24,18 @@
 import android.view.accessibility.AccessibilityManager;
 
 import com.android.internal.logging.UiEventLogger;
-import com.android.internal.statusbar.IStatusBarService;
 import com.android.systemui.R;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dagger.qualifiers.UiBackground;
-import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.people.widget.PeopleSpaceWidgetManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.settings.UserContextProvider;
 import com.android.systemui.shade.NotifPanelEventsModule;
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.statusbar.NotificationListener;
-import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.notification.AssistantFeedbackController;
-import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.notification.NotificationEntryManagerLogger;
 import com.android.systemui.statusbar.notification.collection.NotifInflaterImpl;
@@ -53,12 +49,9 @@
 import com.android.systemui.statusbar.notification.collection.inflation.BindEventManagerImpl;
 import com.android.systemui.statusbar.notification.collection.inflation.NotifInflater;
 import com.android.systemui.statusbar.notification.collection.inflation.OnUserInteractionCallbackImpl;
-import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy;
-import com.android.systemui.statusbar.notification.collection.legacy.VisualStabilityManager;
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
 import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider;
 import com.android.systemui.statusbar.notification.collection.provider.NotificationVisibilityProviderImpl;
-import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider;
 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManagerImpl;
 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
@@ -84,7 +77,6 @@
 import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
-import com.android.systemui.util.leak.LeakDetector;
 import com.android.systemui.wmshell.BubblesManager;
 
 import java.util.Optional;
@@ -118,16 +110,8 @@
     @SysUISingleton
     @Provides
     static NotificationEntryManager provideNotificationEntryManager(
-            NotificationEntryManagerLogger logger,
-            NotificationGroupManagerLegacy groupManager,
-            NotifPipelineFlags notifPipelineFlags,
-            Lazy<NotificationRemoteInputManager> notificationRemoteInputManagerLazy,
-            LeakDetector leakDetector,
-            IStatusBarService statusBarService,
-            @Background Executor bgExecutor) {
-        return new NotificationEntryManager(
-                logger
-        );
+            NotificationEntryManagerLogger logger) {
+        return new NotificationEntryManager(logger);
     }
 
     /** Provides an instance of {@link NotificationGutsManager} */
@@ -141,7 +125,6 @@
             AccessibilityManager accessibilityManager,
             HighPriorityProvider highPriorityProvider,
             INotificationManager notificationManager,
-            NotificationEntryManager notificationEntryManager,
             PeopleSpaceWidgetManager peopleSpaceWidgetManager,
             LauncherApps launcherApps,
             ShortcutManager shortcutManager,
@@ -177,20 +160,6 @@
     @Binds
     NotifGutsViewManager bindNotifGutsViewManager(NotificationGutsManager notificationGutsManager);
 
-    /** Provides an instance of {@link VisualStabilityManager} */
-    @SysUISingleton
-    @Provides
-    static VisualStabilityManager provideVisualStabilityManager(
-            VisualStabilityProvider visualStabilityProvider,
-            StatusBarStateController statusBarStateController,
-            WakefulnessLifecycle wakefulnessLifecycle) {
-        return new VisualStabilityManager(
-                visualStabilityProvider,
-                statusBarStateController,
-                wakefulnessLifecycle
-        );
-    }
-
     /** Provides an instance of {@link NotificationLogger} */
     @SysUISingleton
     @Provides
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/NotificationPersonExtractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/NotificationPersonExtractor.kt
new file mode 100644
index 0000000..4cf3572
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/NotificationPersonExtractor.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.notification.people
+
+import android.service.notification.StatusBarNotification
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.plugins.NotificationPersonExtractorPlugin
+import com.android.systemui.statusbar.policy.ExtensionController
+import javax.inject.Inject
+
+interface NotificationPersonExtractor {
+    fun isPersonNotification(sbn: StatusBarNotification): Boolean
+}
+
+@SysUISingleton
+class NotificationPersonExtractorPluginBoundary @Inject constructor(
+    extensionController: ExtensionController
+) : NotificationPersonExtractor {
+
+    private var plugin: NotificationPersonExtractorPlugin? = null
+
+    init {
+        plugin = extensionController
+                .newExtension(NotificationPersonExtractorPlugin::class.java)
+                .withPlugin(NotificationPersonExtractorPlugin::class.java)
+                .withCallback { extractor ->
+                    plugin = extractor
+                }
+                .build()
+                .get()
+    }
+
+    override fun isPersonNotification(sbn: StatusBarNotification): Boolean =
+            plugin?.isPersonNotification(sbn) ?: false
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHub.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHub.kt
deleted file mode 100644
index 3af6ba8..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHub.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright (C) 2019 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.notification.people
-
-import android.graphics.drawable.Drawable
-
-/**
- * `ViewModel` for PeopleHub view.
- *
- * @param people ViewModels for individual people in PeopleHub, in order that they should be
- *  displayed
- * @param isVisible Whether or not the whole PeopleHub UI is visible
- **/
-data class PeopleHubViewModel(val people: Sequence<PersonViewModel>, val isVisible: Boolean)
-
-/** `ViewModel` for a single "Person' in PeopleHub. */
-data class PersonViewModel(
-    val name: CharSequence,
-    val icon: Drawable,
-    val onClick: () -> Unit
-)
-
-/**
- * `Model` for PeopleHub.
- *
- * @param people Models for individual people in PeopleHub, in order that they should be displayed
- **/
-data class PeopleHubModel(val people: Collection<PersonModel>)
-
-/** `Model` for a single "Person" in PeopleHub. */
-data class PersonModel(
-    val key: PersonKey,
-    val userId: Int,
-    // TODO: these should live in the ViewModel
-    val name: CharSequence,
-    val avatar: Drawable,
-    val clickRunnable: Runnable
-)
-
-/** Unique identifier for a Person in PeopleHub. */
-typealias PersonKey = String
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubModule.kt
index 16574ab..c17ffb0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubModule.kt
@@ -21,25 +21,6 @@
 
 @Module
 abstract class PeopleHubModule {
-
-    @Binds
-    abstract fun peopleHubSectionFooterViewAdapter(
-        impl: PeopleHubViewAdapterImpl
-    ): PeopleHubViewAdapter
-
-    @Binds
-    abstract fun peopleHubDataSource(impl: PeopleHubDataSourceImpl): DataSource<PeopleHubModel>
-
-    @Binds
-    abstract fun peopleHubSettingChangeDataSource(
-        impl: PeopleHubSettingChangeDataSourceImpl
-    ): DataSource<Boolean>
-
-    @Binds
-    abstract fun peopleHubViewModelFactoryDataSource(
-        impl: PeopleHubViewModelFactoryDataSourceImpl
-    ): DataSource<PeopleHubViewModelFactory>
-
     @Binds
     abstract fun peopleNotificationIdentifier(
         impl: PeopleNotificationIdentifierImpl
@@ -49,4 +30,4 @@
     abstract fun notificationPersonExtractor(
         pluginImpl: NotificationPersonExtractorPluginBoundary
     ): NotificationPersonExtractor
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubNotificationListener.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubNotificationListener.kt
deleted file mode 100644
index 6062941..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubNotificationListener.kt
+++ /dev/null
@@ -1,315 +0,0 @@
-/*
- * Copyright (C) 2019 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.notification.people
-
-import android.app.Notification
-import android.content.Context
-import android.content.pm.LauncherApps
-import android.content.pm.PackageManager
-import android.content.pm.UserInfo
-import android.graphics.drawable.Drawable
-import android.os.UserManager
-import android.service.notification.NotificationListenerService
-import android.service.notification.NotificationListenerService.REASON_SNOOZED
-import android.service.notification.StatusBarNotification
-import android.util.IconDrawableFactory
-import android.util.SparseArray
-import android.view.View
-import android.view.ViewGroup
-import android.widget.ImageView
-import com.android.internal.widget.MessagingGroup
-import com.android.settingslib.notification.ConversationIconFactory
-import com.android.systemui.R
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.plugins.NotificationPersonExtractorPlugin
-import com.android.systemui.statusbar.NotificationListener
-import com.android.systemui.statusbar.NotificationLockscreenUserManager
-import com.android.systemui.statusbar.notification.collection.NotificationEntry
-import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
-import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
-import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_NON_PERSON
-import com.android.systemui.statusbar.policy.ExtensionController
-import java.util.ArrayDeque
-import java.util.concurrent.Executor
-import javax.inject.Inject
-
-private const val MAX_STORED_INACTIVE_PEOPLE = 10
-
-interface NotificationPersonExtractor {
-    fun extractPerson(sbn: StatusBarNotification): PersonModel?
-    fun extractPersonKey(sbn: StatusBarNotification): String?
-    fun isPersonNotification(sbn: StatusBarNotification): Boolean
-}
-
-@SysUISingleton
-class NotificationPersonExtractorPluginBoundary @Inject constructor(
-    extensionController: ExtensionController
-) : NotificationPersonExtractor {
-
-    private var plugin: NotificationPersonExtractorPlugin? = null
-
-    init {
-        plugin = extensionController
-                .newExtension(NotificationPersonExtractorPlugin::class.java)
-                .withPlugin(NotificationPersonExtractorPlugin::class.java)
-                .withCallback { extractor ->
-                    plugin = extractor
-                }
-                .build()
-                .get()
-    }
-
-    override fun extractPerson(sbn: StatusBarNotification) =
-            plugin?.extractPerson(sbn)?.run {
-                PersonModel(key, sbn.user.identifier, name, avatar, clickRunnable)
-            }
-
-    override fun extractPersonKey(sbn: StatusBarNotification) = plugin?.extractPersonKey(sbn)
-
-    override fun isPersonNotification(sbn: StatusBarNotification): Boolean =
-            plugin?.isPersonNotification(sbn) ?: false
-}
-
-@SysUISingleton
-class PeopleHubDataSourceImpl @Inject constructor(
-    private val notifCollection: CommonNotifCollection,
-    private val extractor: NotificationPersonExtractor,
-    private val userManager: UserManager,
-    launcherApps: LauncherApps,
-    packageManager: PackageManager,
-    context: Context,
-    private val notificationListener: NotificationListener,
-    @Background private val bgExecutor: Executor,
-    @Main private val mainExecutor: Executor,
-    private val notifLockscreenUserMgr: NotificationLockscreenUserManager,
-    private val peopleNotificationIdentifier: PeopleNotificationIdentifier
-) : DataSource<PeopleHubModel> {
-
-    private var userChangeSubscription: Subscription? = null
-    private val dataListeners = mutableListOf<DataListener<PeopleHubModel>>()
-    private val peopleHubManagerForUser = SparseArray<PeopleHubManager>()
-
-    private val iconFactory = run {
-        val appContext = context.applicationContext
-        ConversationIconFactory(
-                appContext,
-                launcherApps,
-                packageManager,
-                IconDrawableFactory.newInstance(appContext),
-                appContext.resources.getDimensionPixelSize(
-                        R.dimen.notification_guts_conversation_icon_size
-                )
-        )
-    }
-
-    private val notifCollectionListener = object : NotifCollectionListener {
-        override fun onEntryAdded(entry: NotificationEntry) = addVisibleEntry(entry)
-        override fun onEntryUpdated(entry: NotificationEntry) = addVisibleEntry(entry)
-        override fun onEntryRemoved(entry: NotificationEntry, reason: Int) =
-            removeVisibleEntry(entry, reason)
-    }
-
-    private fun removeVisibleEntry(entry: NotificationEntry, reason: Int) {
-        (extractor.extractPersonKey(entry.sbn) ?: entry.extractPersonKey())?.let { key ->
-            val userId = entry.sbn.user.identifier
-            bgExecutor.execute {
-                val parentId = userManager.getProfileParent(userId)?.id ?: userId
-                mainExecutor.execute {
-                    if (reason == REASON_SNOOZED) {
-                        if (peopleHubManagerForUser[parentId]?.migrateActivePerson(key) == true) {
-                            updateUi()
-                        }
-                    } else {
-                        peopleHubManagerForUser[parentId]?.removeActivePerson(key)
-                    }
-                }
-            }
-        }
-    }
-
-    private fun addVisibleEntry(entry: NotificationEntry) {
-        entry.extractPerson()?.let { personModel ->
-            val userId = entry.sbn.user.identifier
-            bgExecutor.execute {
-                val parentId = userManager.getProfileParent(userId)?.id ?: userId
-                mainExecutor.execute {
-                    val manager = peopleHubManagerForUser[parentId]
-                            ?: PeopleHubManager().also { peopleHubManagerForUser.put(parentId, it) }
-                    if (manager.addActivePerson(personModel)) {
-                        updateUi()
-                    }
-                }
-            }
-        }
-    }
-
-    override fun registerListener(listener: DataListener<PeopleHubModel>): Subscription {
-        val register = dataListeners.isEmpty()
-        dataListeners.add(listener)
-        if (register) {
-            userChangeSubscription = notifLockscreenUserMgr.registerListener(
-                    object : NotificationLockscreenUserManager.UserChangedListener {
-                        override fun onUserChanged(userId: Int) = updateUi()
-                        override fun onCurrentProfilesChanged(
-                            currentProfiles: SparseArray<UserInfo>?
-                        ) = updateUi()
-                    })
-            notifCollection.addCollectionListener(notifCollectionListener)
-        } else {
-            getPeopleHubModelForCurrentUser()?.let(listener::onDataChanged)
-        }
-        return object : Subscription {
-            override fun unsubscribe() {
-                dataListeners.remove(listener)
-                if (dataListeners.isEmpty()) {
-                    userChangeSubscription?.unsubscribe()
-                    userChangeSubscription = null
-                    notifCollection.removeCollectionListener(notifCollectionListener)
-                }
-            }
-        }
-    }
-
-    private fun getPeopleHubModelForCurrentUser(): PeopleHubModel? {
-        val currentUserId = notifLockscreenUserMgr.currentUserId
-        val model = peopleHubManagerForUser[currentUserId]?.getPeopleHubModel()
-                ?: return null
-        val currentProfiles = notifLockscreenUserMgr.currentProfiles
-        return model.copy(people = model.people.filter { person ->
-            currentProfiles[person.userId]?.isQuietModeEnabled == false
-        })
-    }
-
-    private fun updateUi() {
-        val model = getPeopleHubModelForCurrentUser() ?: return
-        for (listener in dataListeners) {
-            listener.onDataChanged(model)
-        }
-    }
-
-    private fun NotificationEntry.extractPerson(): PersonModel? {
-        val type = peopleNotificationIdentifier.getPeopleNotificationType(this)
-        if (type == TYPE_NON_PERSON) {
-            return null
-        }
-        val clickRunnable = Runnable { notificationListener.unsnoozeNotification(key) }
-        val extras = sbn.notification.extras
-        val name = ranking.conversationShortcutInfo?.label
-                ?: extras.getCharSequence(Notification.EXTRA_CONVERSATION_TITLE)
-                ?: extras.getCharSequence(Notification.EXTRA_TITLE)
-                ?: return null
-        val drawable = ranking.getIcon(iconFactory, sbn)
-                ?: iconFactory.getConversationDrawable(
-                        extractAvatarFromRow(this),
-                        sbn.packageName,
-                        sbn.uid,
-                        ranking.channel.isImportantConversation
-                )
-        return PersonModel(key, sbn.user.identifier, name, drawable, clickRunnable)
-    }
-
-    private fun NotificationListenerService.Ranking.getIcon(
-        iconFactory: ConversationIconFactory,
-        sbn: StatusBarNotification
-    ): Drawable? =
-            conversationShortcutInfo?.let { conversationShortcutInfo ->
-                iconFactory.getConversationDrawable(
-                        conversationShortcutInfo,
-                        sbn.packageName,
-                        sbn.uid,
-                        channel.isImportantConversation
-                )
-            }
-
-    private fun NotificationEntry.extractPersonKey(): PersonKey? {
-        // TODO migrate to shortcut id when snoozing is conversation wide
-        val type = peopleNotificationIdentifier.getPeopleNotificationType(this)
-        return if (type != TYPE_NON_PERSON) key else null
-    }
-}
-
-private fun NotificationLockscreenUserManager.registerListener(
-    listener: NotificationLockscreenUserManager.UserChangedListener
-): Subscription {
-    addUserChangedListener(listener)
-    return object : Subscription {
-        override fun unsubscribe() {
-            removeUserChangedListener(listener)
-        }
-    }
-}
-
-class PeopleHubManager {
-
-    // People currently visible in the notification shade, and so are not in the hub
-    private val activePeople = mutableMapOf<PersonKey, PersonModel>()
-
-    // People that were once "active" and have been dismissed, and so can be displayed in the hub
-    private val inactivePeople = ArrayDeque<PersonModel>(MAX_STORED_INACTIVE_PEOPLE)
-
-    fun migrateActivePerson(key: PersonKey): Boolean {
-        activePeople.remove(key)?.let { data ->
-            if (inactivePeople.size >= MAX_STORED_INACTIVE_PEOPLE) {
-                inactivePeople.removeLast()
-            }
-            inactivePeople.addFirst(data)
-            return true
-        }
-        return false
-    }
-
-    fun removeActivePerson(key: PersonKey) {
-        activePeople.remove(key)
-    }
-
-    fun addActivePerson(person: PersonModel): Boolean {
-        activePeople[person.key] = person
-        return inactivePeople.removeIf { it.key == person.key }
-    }
-
-    fun getPeopleHubModel(): PeopleHubModel = PeopleHubModel(inactivePeople)
-}
-
-private val ViewGroup.children
-    get(): Sequence<View> = sequence {
-        for (i in 0 until childCount) {
-            yield(getChildAt(i))
-        }
-    }
-
-private fun ViewGroup.childrenWithId(id: Int): Sequence<View> = children.filter { it.id == id }
-
-fun extractAvatarFromRow(entry: NotificationEntry): Drawable? =
-        entry.row
-                ?.childrenWithId(R.id.expanded)
-                ?.mapNotNull { it as? ViewGroup }
-                ?.flatMap {
-                    it.childrenWithId(com.android.internal.R.id.status_bar_latest_event_content)
-                }
-                ?.mapNotNull {
-                    it.findViewById<ViewGroup>(com.android.internal.R.id.notification_messaging)
-                }
-                ?.mapNotNull { messagesView ->
-                    messagesView.children
-                            .mapNotNull { it as? MessagingGroup }
-                            .lastOrNull()
-                            ?.findViewById<ImageView>(com.android.internal.R.id.message_icon)
-                            ?.drawable
-                }
-                ?.firstOrNull()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubViewController.kt
deleted file mode 100644
index 55bd77f..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubViewController.kt
+++ /dev/null
@@ -1,193 +0,0 @@
-/*
- * Copyright (C) 2019 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.notification.people
-
-import android.content.Context
-import android.database.ContentObserver
-import android.net.Uri
-import android.os.Handler
-import android.os.UserHandle
-import android.provider.Settings
-import android.view.View
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.plugins.ActivityStarter
-import javax.inject.Inject
-
-/** Boundary between the View and PeopleHub, as seen by the View. */
-interface PeopleHubViewAdapter {
-    fun bindView(viewBoundary: PeopleHubViewBoundary): Subscription
-}
-
-/** Abstract `View` representation of PeopleHub. */
-interface PeopleHubViewBoundary {
-    /** View used for animating the activity launch caused by clicking a person in the hub. */
-    val associatedViewForClickAnimation: View
-
-    /**
-     * [DataListener]s for individual people in the hub.
-     *
-     * These listeners should be ordered such that the first element will be bound to the most
-     * recent person to be added to the hub, and then continuing in descending order. If there are
-     * not enough people to satisfy each listener, `null` will be passed instead, indicating that
-     * the `View` should render a placeholder.
-     */
-    val personViewAdapters: Sequence<DataListener<PersonViewModel?>>
-
-    /** Sets the visibility of the Hub in the notification shade. */
-    fun setVisible(isVisible: Boolean)
-}
-
-/** Creates a [PeopleHubViewModel] given some additional information required from the `View`. */
-interface PeopleHubViewModelFactory {
-
-    /**
-     * Creates a [PeopleHubViewModel] that, when clicked, starts an activity using an animation
-     * involving the given [view].
-     */
-    fun createWithAssociatedClickView(view: View): PeopleHubViewModel
-}
-
-/**
- * Wraps a [PeopleHubViewBoundary] in a [DataListener], and connects it to the data
- * pipeline.
- *
- * @param dataSource PeopleHub data pipeline.
- */
-@SysUISingleton
-class PeopleHubViewAdapterImpl @Inject constructor(
-    private val dataSource: DataSource<@JvmSuppressWildcards PeopleHubViewModelFactory>
-) : PeopleHubViewAdapter {
-
-    override fun bindView(viewBoundary: PeopleHubViewBoundary): Subscription =
-            dataSource.registerListener(PeopleHubDataListenerImpl(viewBoundary))
-}
-
-private class PeopleHubDataListenerImpl(
-    private val viewBoundary: PeopleHubViewBoundary
-) : DataListener<PeopleHubViewModelFactory> {
-
-    override fun onDataChanged(data: PeopleHubViewModelFactory) {
-        val viewModel = data.createWithAssociatedClickView(
-                viewBoundary.associatedViewForClickAnimation
-        )
-        viewBoundary.setVisible(viewModel.isVisible)
-        val padded = viewModel.people + repeated(null)
-        for ((adapter, model) in viewBoundary.personViewAdapters.zip(padded)) {
-            adapter.onDataChanged(model)
-        }
-    }
-}
-
-/**
- * Converts [PeopleHubModel]s into [PeopleHubViewModelFactory]s.
- *
- * This class serves as the glue between the View layer (which depends on
- * [PeopleHubViewBoundary]) and the Data layer (which produces [PeopleHubModel]s).
- */
-@SysUISingleton
-class PeopleHubViewModelFactoryDataSourceImpl @Inject constructor(
-    private val activityStarter: ActivityStarter,
-    private val dataSource: DataSource<@JvmSuppressWildcards PeopleHubModel>
-) : DataSource<PeopleHubViewModelFactory> {
-
-    override fun registerListener(listener: DataListener<PeopleHubViewModelFactory>): Subscription {
-        var model: PeopleHubModel? = null
-
-        fun updateListener() {
-            // don't invoke listener until we've received our first model
-            model?.let { model ->
-                val factory = PeopleHubViewModelFactoryImpl(model, activityStarter)
-                listener.onDataChanged(factory)
-            }
-        }
-        val dataSub = dataSource.registerListener(object : DataListener<PeopleHubModel> {
-            override fun onDataChanged(data: PeopleHubModel) {
-                model = data
-                updateListener()
-            }
-        })
-        return object : Subscription {
-            override fun unsubscribe() {
-                dataSub.unsubscribe()
-            }
-        }
-    }
-}
-
-private object EmptyViewModelFactory : PeopleHubViewModelFactory {
-    override fun createWithAssociatedClickView(view: View): PeopleHubViewModel {
-        return PeopleHubViewModel(emptySequence(), false)
-    }
-}
-
-private class PeopleHubViewModelFactoryImpl(
-    private val model: PeopleHubModel,
-    private val activityStarter: ActivityStarter
-) : PeopleHubViewModelFactory {
-
-    override fun createWithAssociatedClickView(view: View): PeopleHubViewModel {
-        val personViewModels = model.people.asSequence().map { personModel ->
-            val onClick = {
-                personModel.clickRunnable.run()
-            }
-            PersonViewModel(personModel.name, personModel.avatar, onClick)
-        }
-        return PeopleHubViewModel(personViewModels, model.people.isNotEmpty())
-    }
-}
-
-@SysUISingleton
-class PeopleHubSettingChangeDataSourceImpl @Inject constructor(
-    @Main private val handler: Handler,
-    context: Context
-) : DataSource<Boolean> {
-
-    private val settingUri = Settings.Secure.getUriFor(Settings.Secure.PEOPLE_STRIP)
-    private val contentResolver = context.contentResolver
-
-    override fun registerListener(listener: DataListener<Boolean>): Subscription {
-        // Immediately report current value of setting
-        updateListener(listener)
-        val observer = object : ContentObserver(handler) {
-            override fun onChange(selfChange: Boolean, uri: Uri?, flags: Int) {
-                super.onChange(selfChange, uri, flags)
-                updateListener(listener)
-            }
-        }
-        contentResolver.registerContentObserver(settingUri, false, observer, UserHandle.USER_ALL)
-        return object : Subscription {
-            override fun unsubscribe() = contentResolver.unregisterContentObserver(observer)
-        }
-    }
-
-    private fun updateListener(listener: DataListener<Boolean>) {
-        val setting = Settings.Secure.getIntForUser(
-                contentResolver,
-                Settings.Secure.PEOPLE_STRIP,
-                0,
-                UserHandle.USER_CURRENT
-        )
-        listener.onDataChanged(setting != 0)
-    }
-}
-
-private fun <T> repeated(value: T): Sequence<T> = sequence {
-    while (true) {
-        yield(value)
-    }
-}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 79d883b3..cc87499 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -6116,9 +6116,6 @@
             }
             return row.canViewBeDismissed();
         }
-        if (v instanceof PeopleHubView) {
-            return ((PeopleHubView) v).getCanSwipe();
-        }
         return false;
     }
 
@@ -6130,9 +6127,6 @@
             }
             return row.canViewBeCleared();
         }
-        if (v instanceof PeopleHubView) {
-            return ((PeopleHubView) v).getCanSwipe();
-        }
         return false;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 3f6586c..e6eceb5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -87,8 +87,6 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.PipelineDumpable;
 import com.android.systemui.statusbar.notification.collection.PipelineDumper;
-import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy;
-import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy.OnGroupChangeListener;
 import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
@@ -633,7 +631,6 @@
             NotificationSwipeHelper.Builder notificationSwipeHelperBuilder,
             CentralSurfaces centralSurfaces,
             ScrimController scrimController,
-            NotificationGroupManagerLegacy legacyGroupManager,
             GroupExpansionManager groupManager,
             @SilentHeader SectionHeaderController silentHeaderController,
             NotifPipeline notifPipeline,
@@ -677,12 +674,6 @@
         mJankMonitor = jankMonitor;
         mNotificationStackSizeCalculator = notificationStackSizeCalculator;
         mGroupExpansionManager = groupManager;
-        legacyGroupManager.registerGroupChangeListener(new OnGroupChangeListener() {
-            @Override
-            public void onGroupsChanged() {
-                mCentralSurfaces.requestNotificationUpdate("onGroupsChanged");
-            }
-        });
         mSilentHeaderController = silentHeaderController;
         mNotifPipeline = notifPipeline;
         mNotifCollection = notifCollection;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/PeopleHubView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/PeopleHubView.kt
deleted file mode 100644
index b13e7fb..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/PeopleHubView.kt
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * Copyright (C) 2019 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.notification.stack
-
-import android.annotation.ColorInt
-import android.content.Context
-import android.util.AttributeSet
-import android.view.View
-import android.view.ViewGroup
-import android.widget.ImageView
-import android.widget.TextView
-import com.android.systemui.R
-import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin
-import com.android.systemui.statusbar.notification.people.DataListener
-import com.android.systemui.statusbar.notification.people.PersonViewModel
-import com.android.systemui.statusbar.notification.row.StackScrollerDecorView
-
-class PeopleHubView(context: Context, attrs: AttributeSet) :
-        StackScrollerDecorView(context, attrs), SwipeableView {
-
-    private lateinit var contents: ViewGroup
-    private lateinit var label: TextView
-
-    lateinit var personViewAdapters: Sequence<DataListener<PersonViewModel?>>
-        private set
-
-    override fun onFinishInflate() {
-        contents = requireViewById(R.id.people_list)
-        label = requireViewById(R.id.header_label)
-        personViewAdapters = (0 until contents.childCount)
-                .asSequence() // so we can map
-                .mapNotNull { idx ->
-                    // get all our people slots
-                    (contents.getChildAt(idx) as? ImageView)?.let(::PersonDataListenerImpl)
-                }
-                .toList() // cache it
-                .asSequence() // but don't reveal it's a list
-        super.onFinishInflate()
-        setVisible(true /* nowVisible */, false /* animate */)
-    }
-
-    fun setTextColor(@ColorInt color: Int) = label.setTextColor(color)
-
-    override fun findContentView(): View = contents
-    override fun findSecondaryView(): View? = null
-
-    override fun hasFinishedInitialization(): Boolean = true
-
-    override fun createMenu(): NotificationMenuRowPlugin? = null
-
-    override fun resetTranslation() {
-        translationX = 0f
-    }
-
-    override fun setTranslation(translation: Float) {
-        if (canSwipe) {
-            super.setTranslation(translation)
-        }
-    }
-
-    var canSwipe: Boolean = false
-        set(value) {
-            if (field != value) {
-                if (field) {
-                    resetTranslation()
-                }
-                field = value
-            }
-        }
-
-    override fun needsClippingToShelf(): Boolean = true
-
-    override fun applyContentTransformation(contentAlpha: Float, translationY: Float) {
-        super.applyContentTransformation(contentAlpha, translationY)
-        for (i in 0 until contents.childCount) {
-            val view = contents.getChildAt(i)
-            view.alpha = contentAlpha
-            view.translationY = translationY
-        }
-    }
-
-    fun setOnHeaderClickListener(listener: OnClickListener) = label.setOnClickListener(listener)
-
-    private inner class PersonDataListenerImpl(val avatarView: ImageView) :
-            DataListener<PersonViewModel?> {
-
-        override fun onDataChanged(data: PersonViewModel?) {
-            avatarView.visibility = data?.let { View.VISIBLE } ?: View.GONE
-            avatarView.setImageDrawable(data?.icon)
-            avatarView.setOnClickListener { data?.onClick?.invoke() }
-        }
-    }
-}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
index e754d5d..8bd7886 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
@@ -23,6 +23,7 @@
 import android.hardware.biometrics.BiometricFaceConstants;
 import android.hardware.biometrics.BiometricFingerprintConstants;
 import android.hardware.biometrics.BiometricSourceType;
+import android.hardware.face.FaceManager;
 import android.hardware.fingerprint.FingerprintManager;
 import android.metrics.LogMaker;
 import android.os.Handler;
@@ -64,7 +65,6 @@
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
-import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
@@ -87,7 +87,7 @@
     private static final long BIOMETRIC_WAKELOCK_TIMEOUT_MS = 15 * 1000;
     private static final String BIOMETRIC_WAKE_LOCK_NAME = "wake-and-unlock:wakelock";
     private static final UiEventLogger UI_EVENT_LOGGER = new UiEventLoggerImpl();
-    private static final int FP_ATTEMPTS_BEFORE_SHOW_BOUNCER = 2;
+    private static final int UDFPS_ATTEMPTS_BEFORE_SHOW_BOUNCER = 3;
     private static final VibrationEffect SUCCESS_VIBRATION_EFFECT =
             VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
     private static final VibrationEffect ERROR_VIBRATION_EFFECT =
@@ -450,7 +450,7 @@
         // During wake and unlock, we need to draw black before waking up to avoid abrupt
         // brightness changes due to display state transitions.
         Runnable wakeUp = ()-> {
-            if (!wasDeviceInteractive) {
+            if (!wasDeviceInteractive || mUpdateMonitor.isDreaming()) {
                 if (DEBUG_BIO_WAKELOCK) {
                     Log.i(TAG, "bio wakelock: Authenticated, waking up...");
                 }
@@ -662,7 +662,7 @@
 
         if (!mVibratorHelper.hasVibrator()
                 && (!mUpdateMonitor.isDeviceInteractive() || mUpdateMonitor.isDreaming())) {
-            startWakeAndUnlock(MODE_SHOW_BOUNCER);
+            startWakeAndUnlock(MODE_ONLY_WAKE);
         } else if (biometricSourceType == BiometricSourceType.FINGERPRINT
                 && mUpdateMonitor.isUdfpsSupported()) {
             long currUptimeMillis = SystemClock.uptimeMillis();
@@ -673,7 +673,7 @@
             }
             mLastFpFailureUptimeMillis = currUptimeMillis;
 
-            if (mNumConsecutiveFpFailures >= FP_ATTEMPTS_BEFORE_SHOW_BOUNCER) {
+            if (mNumConsecutiveFpFailures >= UDFPS_ATTEMPTS_BEFORE_SHOW_BOUNCER) {
                 startWakeAndUnlock(MODE_SHOW_BOUNCER);
                 UI_EVENT_LOGGER.log(BiometricUiEvent.BIOMETRIC_BOUNCER_SHOWN, getSessionId());
                 mNumConsecutiveFpFailures = 0;
@@ -699,13 +699,13 @@
         Optional.ofNullable(BiometricUiEvent.ERROR_EVENT_BY_SOURCE_TYPE.get(biometricSourceType))
                 .ifPresent(event -> UI_EVENT_LOGGER.log(event, getSessionId()));
 
-        // if we're on the shade and we're locked out, immediately show the bouncer
-        if (biometricSourceType == BiometricSourceType.FINGERPRINT
+        final boolean fingerprintLockout = biometricSourceType == BiometricSourceType.FINGERPRINT
                 && (msgId == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT
-                || msgId == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT_PERMANENT)
-                && mUpdateMonitor.isUdfpsSupported()
-                && (mStatusBarStateController.getState() == StatusBarState.SHADE
-                    || mStatusBarStateController.getState() == StatusBarState.SHADE_LOCKED)) {
+                || msgId == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT_PERMANENT);
+        final boolean faceLockout = biometricSourceType == BiometricSourceType.FACE
+                && (msgId == FaceManager.FACE_ERROR_LOCKOUT
+                || msgId == FaceManager.FACE_ERROR_LOCKOUT_PERMANENT);
+        if (fingerprintLockout || faceLockout) {
             startWakeAndUnlock(MODE_SHOW_BOUNCER);
             UI_EVENT_LOGGER.log(BiometricUiEvent.BIOMETRIC_BOUNCER_SHOWN, getSessionId());
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
index 5566fa6..c96faab 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModel.kt
@@ -16,13 +16,23 @@
 
 package com.android.systemui.statusbar.pipeline.wifi.data.model
 
+import androidx.annotation.VisibleForTesting
+
 /** Provides information about the current wifi network. */
 sealed class WifiNetworkModel {
     /** A model representing that we have no active wifi network. */
     object Inactive : WifiNetworkModel()
 
+    /**
+     * A model representing that our wifi network is actually a carrier merged network, meaning it's
+     * treated as more of a mobile network.
+     *
+     * See [android.net.wifi.WifiInfo.isCarrierMerged] for more information.
+     */
+    object CarrierMerged : WifiNetworkModel()
+
     /** Provides information about an active wifi network. */
-    class Active(
+    data class Active(
         /**
          * The [android.net.Network.netId] we received from
          * [android.net.ConnectivityManager.NetworkCallback] in association with this wifi network.
@@ -31,6 +41,19 @@
          */
         val networkId: Int,
 
+        /** See [android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED]. */
+        val isValidated: Boolean = false,
+
+        /**
+         * The wifi signal level, guaranteed to be 0 <= level <= 4.
+         *
+         * Null if we couldn't fetch the level for some reason.
+         *
+         * TODO(b/238425913): The level will only be null if we have a null WifiManager. Is there a
+         *   way we can guarantee a non-null WifiManager?
+         */
+        val level: Int? = null,
+
         /** See [android.net.wifi.WifiInfo.ssid]. */
         val ssid: String? = null,
 
@@ -42,5 +65,18 @@
 
         /** See [android.net.wifi.WifiInfo.passpointProviderFriendlyName]. */
         val passpointProviderFriendlyName: String? = null,
-    ) : WifiNetworkModel()
+    ) : WifiNetworkModel() {
+        init {
+            require(level == null || level in MIN_VALID_LEVEL..MAX_VALID_LEVEL) {
+                "0 <= wifi level <= 4 required; level was $level"
+            }
+        }
+
+        companion object {
+            @VisibleForTesting
+            internal const val MIN_VALID_LEVEL = 0
+            @VisibleForTesting
+            internal const val MAX_VALID_LEVEL = 4
+        }
+    }
 }
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 43fbabc..7765427 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
@@ -21,6 +21,7 @@
 import android.net.Network
 import android.net.NetworkCapabilities
 import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN
+import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
 import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
 import android.net.NetworkCapabilities.TRANSPORT_WIFI
 import android.net.NetworkRequest
@@ -31,6 +32,7 @@
 import com.android.settingslib.Utils
 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
@@ -38,10 +40,13 @@
 import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
 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.SharingStarted
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.shareIn
 
 /**
  * Provides data related to the wifi state.
@@ -63,10 +68,11 @@
 @SysUISingleton
 @SuppressLint("MissingPermission")
 class WifiRepositoryImpl @Inject constructor(
-        connectivityManager: ConnectivityManager,
-        wifiManager: WifiManager?,
-        @Main mainExecutor: Executor,
-        logger: ConnectivityPipelineLogger,
+    connectivityManager: ConnectivityManager,
+    logger: ConnectivityPipelineLogger,
+    @Main mainExecutor: Executor,
+    @Application scope: CoroutineScope,
+    wifiManager: WifiManager?,
 ) : WifiRepository {
     override val wifiNetwork: Flow<WifiNetworkModel> = conflatedCallbackFlow {
         var currentWifi: WifiNetworkModel = WIFI_NETWORK_DEFAULT
@@ -80,7 +86,12 @@
 
                 val wifiInfo = networkCapabilitiesToWifiInfo(networkCapabilities)
                 if (wifiInfo?.isPrimary == true) {
-                    val wifiNetworkModel = wifiInfoToModel(wifiInfo, network.getNetId())
+                    val wifiNetworkModel = createWifiNetworkModel(
+                        wifiInfo,
+                        network,
+                        networkCapabilities,
+                        wifiManager,
+                    )
                     logger.logTransformation(
                         WIFI_NETWORK_CALLBACK_NAME,
                         oldValue = currentWifi,
@@ -112,6 +123,7 @@
 
         awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
     }
+        .shareIn(scope, started = SharingStarted.WhileSubscribed())
 
     override val wifiActivity: Flow<WifiActivityModel> =
             if (wifiManager == null) {
@@ -164,14 +176,25 @@
             }
         }
 
-        private fun wifiInfoToModel(wifiInfo: WifiInfo, networkId: Int): WifiNetworkModel {
-            return WifiNetworkModel.Active(
-                networkId,
-                wifiInfo.ssid,
-                wifiInfo.isPasspointAp,
-                wifiInfo.isOsuAp,
-                wifiInfo.passpointProviderFriendlyName
-            )
+        private fun createWifiNetworkModel(
+            wifiInfo: WifiInfo,
+            network: Network,
+            networkCapabilities: NetworkCapabilities,
+            wifiManager: WifiManager?,
+        ): WifiNetworkModel {
+            return if (wifiInfo.isCarrierMerged) {
+                WifiNetworkModel.CarrierMerged
+            } else {
+                WifiNetworkModel.Active(
+                        network.getNetId(),
+                        isValidated = networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED),
+                        level = wifiManager?.calculateSignalLevel(wifiInfo.rssi),
+                        wifiInfo.ssid,
+                        wifiInfo.isPasspointAp,
+                        wifiInfo.isOsuAp,
+                        wifiInfo.passpointProviderFriendlyName
+                )
+            }
         }
 
         private fun prettyPrintActivity(activity: Int): String {
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 a9da63b..afe19af 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
@@ -33,11 +33,12 @@
  */
 @SysUISingleton
 class WifiInteractor @Inject constructor(
-        repository: WifiRepository,
+    repository: WifiRepository,
 ) {
     private val ssid: Flow<String?> = repository.wifiNetwork.map { info ->
         when (info) {
             is WifiNetworkModel.Inactive -> null
+            is WifiNetworkModel.CarrierMerged -> null
             is WifiNetworkModel.Active -> when {
                 info.isPasspointAccessPoint || info.isOnlineSignUpForPasspointAccessPoint ->
                     info.passpointProviderFriendlyName
@@ -47,6 +48,10 @@
         }
     }
 
+    /** Our current wifi network. See [WifiNetworkModel]. */
+    val wifiNetwork: Flow<WifiNetworkModel> = repository.wifiNetwork
+
+    /** True if our wifi network has activity in (download), and false otherwise. */
     val hasActivityIn: Flow<Boolean> = combine(repository.wifiActivity, ssid) { activity, ssid ->
             activity.hasActivityIn && ssid != null
         }
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 b06aaf4..7607ddf 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
@@ -25,10 +25,10 @@
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.systemui.R
 import com.android.systemui.lifecycle.repeatWhenAttached
-import com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS
 import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel
 import kotlinx.coroutines.InternalCoroutinesApi
 import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.launch
 
 /**
@@ -50,11 +50,22 @@
 
         view.isVisible = true
         iconView.isVisible = true
-        iconView.setImageDrawable(view.context.getDrawable(WIFI_FULL_ICONS[2]))
 
         view.repeatWhenAttached {
             repeatOnLifecycle(Lifecycle.State.STARTED) {
                 launch {
+                    viewModel.wifiIconResId.distinctUntilChanged().collect { iconResId ->
+                        iconView.setImageDrawable(
+                            if (iconResId != null && iconResId > 0) {
+                                iconView.context.getDrawable(iconResId)
+                            } else {
+                                null
+                            }
+                        )
+                    }
+                }
+
+                launch {
                     viewModel.tint.collect { tint ->
                         iconView.imageTintList = ColorStateList.valueOf(tint)
                     }
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 7a26260..4fdcc44 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,20 +17,24 @@
 package com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel
 
 import android.graphics.Color
+import androidx.annotation.DrawableRes
+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
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 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.domain.interactor.WifiInteractor
 import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
 
 /**
  * Models the UI state for the status bar wifi icon.
- *
- * TODO(b/238425913): Hook this up to the real status bar wifi view using a view binder.
  */
 class WifiViewModel @Inject constructor(
     statusBarPipelineFlags: StatusBarPipelineFlags,
@@ -38,6 +42,23 @@
     private val logger: ConnectivityPipelineLogger,
     private val interactor: WifiInteractor,
 ) {
+    /**
+     * The drawable resource ID to use for the wifi icon. Null if we shouldn't display any icon.
+     */
+    @DrawableRes
+    val wifiIconResId: Flow<Int?> = interactor.wifiNetwork.map {
+        when (it) {
+            is WifiNetworkModel.CarrierMerged -> null
+            is WifiNetworkModel.Inactive -> WIFI_NO_NETWORK
+            is WifiNetworkModel.Active ->
+                when {
+                    it.level == null -> null
+                    it.isValidated -> WIFI_FULL_ICONS[it.level]
+                    else -> WIFI_NO_INTERNET_ICONS[it.level]
+                }
+        }
+    }
+
     val isActivityInVisible: Flow<Boolean>
         get() =
             if (!constants.shouldShowActivityConfig) {
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt
new file mode 100644
index 0000000..7baebf4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt
@@ -0,0 +1,98 @@
+/*
+ * 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.util.kotlin
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.drop
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.zip
+
+/**
+ * Returns a new [Flow] that combines the two most recent emissions from [this] using [transform].
+ * Note that the new Flow will not start emitting until it has received two emissions from the
+ * upstream Flow.
+ *
+ * Useful for code that needs to compare the current value to the previous value.
+ */
+fun <T, R> Flow<T>.pairwiseBy(transform: suspend (old: T, new: T) -> R): Flow<R> {
+    // same as current flow, but with the very first event skipped
+    val nextEvents = drop(1)
+    // zip current flow and nextEvents; transform will receive a pair of old and new value. This
+    // works because zip will suppress emissions until both flows have emitted something; since in
+    // this case both flows are emitting at the same rate, but the current flow just has one extra
+    // thing emitted at the start, the effect is that zip will cache the most recent value while
+    // waiting for the next emission from nextEvents.
+    return zip(nextEvents, transform)
+}
+
+/**
+ * Returns a new [Flow] that combines the two most recent emissions from [this] using [transform].
+ * [initialValue] will be used as the "old" value for the first emission.
+ *
+ * Useful for code that needs to compare the current value to the previous value.
+ */
+fun <T, R> Flow<T>.pairwiseBy(
+    initialValue: T,
+    transform: suspend (previousValue: T, newValue: T) -> R,
+): Flow<R> =
+    onStart { emit(initialValue) }.pairwiseBy(transform)
+
+/**
+ * Returns a new [Flow] that produces the two most recent emissions from [this]. Note that the new
+ * Flow will not start emitting until it has received two emissions from the upstream Flow.
+ *
+ * Useful for code that needs to compare the current value to the previous value.
+ */
+fun <T> Flow<T>.pairwise(): Flow<WithPrev<T>> = pairwiseBy(::WithPrev)
+
+/**
+ * Returns a new [Flow] that produces the two most recent emissions from [this]. [initialValue]
+ * will be used as the "old" value for the first emission.
+ *
+ * Useful for code that needs to compare the current value to the previous value.
+ */
+fun <T> Flow<T>.pairwise(initialValue: T): Flow<WithPrev<T>> = pairwiseBy(initialValue, ::WithPrev)
+
+/** Holds a [newValue] emitted from a [Flow], along with the [previousValue] emitted value. */
+data class WithPrev<T>(val previousValue: T, val newValue: T)
+
+/**
+ * Returns a new [Flow] that combines the [Set] changes between each emission from [this] using
+ * [transform].
+ */
+fun <T, R> Flow<Set<T>>.setChangesBy(
+    transform: suspend (removed: Set<T>, added: Set<T>) -> R,
+): Flow<R> = onStart { emit(emptySet()) }.distinctUntilChanged()
+    .pairwiseBy { old: Set<T>, new: Set<T> ->
+        // If an element was present in the old set, but not the new one, then it was removed
+        val removed = old - new
+        // If an element is present in the new set, but on the old one, then it was added
+        val added = new - old
+        transform(removed, added)
+    }
+
+/** Returns a new [Flow] that produces the [Set] changes between each emission from [this]. */
+fun <T> Flow<Set<T>>.setChanges(): Flow<SetChanges<T>> = setChangesBy(::SetChanges)
+
+/** Contains the difference in elements between two [Set]s. */
+data class SetChanges<T>(
+    /** Elements that are present in the first [Set] but not in the second. */
+    val removed: Set<T>,
+    /** Elements that are present in the second [Set] but not in the first. */
+    val added: Set<T>,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index c94a915..e6a3e74c 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -48,7 +48,6 @@
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.DialogInterface;
-import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.res.ColorStateList;
 import android.content.res.Configuration;
@@ -248,6 +247,7 @@
 
     private final ConfigurationController mConfigurationController;
     private final MediaOutputDialogFactory mMediaOutputDialogFactory;
+    private final VolumePanelFactory mVolumePanelFactory;
     private final ActivityStarter mActivityStarter;
 
     private boolean mShowing;
@@ -279,6 +279,7 @@
             DeviceProvisionedController deviceProvisionedController,
             ConfigurationController configurationController,
             MediaOutputDialogFactory mediaOutputDialogFactory,
+            VolumePanelFactory volumePanelFactory,
             ActivityStarter activityStarter,
             InteractionJankMonitor interactionJankMonitor) {
         mContext =
@@ -290,6 +291,7 @@
         mDeviceProvisionedController = deviceProvisionedController;
         mConfigurationController = configurationController;
         mMediaOutputDialogFactory = mediaOutputDialogFactory;
+        mVolumePanelFactory = volumePanelFactory;
         mActivityStarter = activityStarter;
         mShowActiveStreamOnly = showActiveStreamOnly();
         mHasSeenODICaptionsTooltip =
@@ -1043,10 +1045,9 @@
         if (mSettingsIcon != null) {
             mSettingsIcon.setOnClickListener(v -> {
                 Events.writeEvent(Events.EVENT_SETTINGS_CLICK);
-                Intent intent = new Intent(Settings.Panel.ACTION_VOLUME);
                 dismissH(DISMISS_REASON_SETTINGS_CLICKED);
                 mMediaOutputDialogFactory.dismiss();
-                mActivityStarter.startActivity(intent, true /* dismissShade */);
+                mVolumePanelFactory.create(true /* aboveStatusBar */, null);
             });
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumePanelDialog.java b/packages/SystemUI/src/com/android/systemui/volume/VolumePanelDialog.java
new file mode 100644
index 0000000..2c74fb9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumePanelDialog.java
@@ -0,0 +1,299 @@
+/*
+ * 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.volume;
+
+import android.bluetooth.BluetoothDevice;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings;
+import android.provider.SettingsSlicesContract;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.Button;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
+import androidx.lifecycle.LiveData;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.slice.Slice;
+import androidx.slice.SliceMetadata;
+import androidx.slice.widget.EventInfo;
+import androidx.slice.widget.SliceLiveData;
+
+import com.android.settingslib.bluetooth.A2dpProfile;
+import com.android.settingslib.bluetooth.BluetoothUtils;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.settingslib.media.MediaOutputConstants;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.phone.SystemUIDialog;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Visual presentation of the volume panel dialog.
+ */
+public class VolumePanelDialog extends SystemUIDialog implements LifecycleOwner {
+    private static final String TAG = "VolumePanelDialog";
+
+    private static final int DURATION_SLICE_BINDING_TIMEOUT_MS = 200;
+    private static final int DEFAULT_SLICE_SIZE = 4;
+
+    private RecyclerView mVolumePanelSlices;
+    private VolumePanelSlicesAdapter mVolumePanelSlicesAdapter;
+    private final LifecycleRegistry mLifecycleRegistry;
+    private final Handler mHandler = new Handler(Looper.getMainLooper());
+    private final Map<Uri, LiveData<Slice>> mSliceLiveData = new LinkedHashMap<>();
+    private final HashSet<Uri> mLoadedSlices = new HashSet<>();
+    private boolean mSlicesReadyToLoad;
+    private LocalBluetoothProfileManager mProfileManager;
+
+    public VolumePanelDialog(Context context, boolean aboveStatusBar) {
+        super(context);
+        mLifecycleRegistry = new LifecycleRegistry(this);
+        if (!aboveStatusBar) {
+            getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
+        }
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        Log.d(TAG, "onCreate");
+
+        View dialogView = LayoutInflater.from(getContext()).inflate(R.layout.volume_panel_dialog,
+                null);
+        final Window window = getWindow();
+        window.setContentView(dialogView);
+
+        Button doneButton = dialogView.findViewById(R.id.done_button);
+        doneButton.setOnClickListener(v -> dismiss());
+        Button settingsButton = dialogView.findViewById(R.id.settings_button);
+        settingsButton.setOnClickListener(v -> {
+            getContext().startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS).addFlags(
+                    Intent.FLAG_ACTIVITY_NEW_TASK));
+            dismiss();
+        });
+
+        LocalBluetoothManager localBluetoothManager = LocalBluetoothManager.getInstance(
+                getContext(), null);
+        if (localBluetoothManager != null) {
+            mProfileManager = localBluetoothManager.getProfileManager();
+        }
+
+        mVolumePanelSlices = dialogView.findViewById(R.id.volume_panel_parent_layout);
+        mVolumePanelSlices.setLayoutManager(new LinearLayoutManager(getContext()));
+
+        loadAllSlices();
+
+        mLifecycleRegistry.setCurrentState(Lifecycle.State.CREATED);
+    }
+
+    private void loadAllSlices() {
+        mSliceLiveData.clear();
+        mLoadedSlices.clear();
+        final List<Uri> sliceUris = getSlices();
+
+        for (Uri uri : sliceUris) {
+            final LiveData<Slice> sliceLiveData = SliceLiveData.fromUri(getContext(), uri,
+                    (int type, Throwable source) -> {
+                        if (!removeSliceLiveData(uri)) {
+                            mLoadedSlices.add(uri);
+                        }
+                    });
+
+            // Add slice first to make it in order.  Will remove it later if there's an error.
+            mSliceLiveData.put(uri, sliceLiveData);
+
+            sliceLiveData.observe(this, slice -> {
+                if (mLoadedSlices.contains(uri)) {
+                    return;
+                }
+                Log.d(TAG, "received slice: " + (slice == null ? null : slice.getUri()));
+                final SliceMetadata metadata = SliceMetadata.from(getContext(), slice);
+                if (slice == null || metadata.isErrorSlice()) {
+                    if (!removeSliceLiveData(uri)) {
+                        mLoadedSlices.add(uri);
+                    }
+                } else if (metadata.getLoadingState() == SliceMetadata.LOADED_ALL) {
+                    mLoadedSlices.add(uri);
+                } else {
+                    mHandler.postDelayed(() -> {
+                        mLoadedSlices.add(uri);
+                        setupAdapterWhenReady();
+                    }, DURATION_SLICE_BINDING_TIMEOUT_MS);
+                }
+
+                setupAdapterWhenReady();
+            });
+        }
+    }
+
+    private void setupAdapterWhenReady() {
+        if (mLoadedSlices.size() == mSliceLiveData.size() && !mSlicesReadyToLoad) {
+            mSlicesReadyToLoad = true;
+            mVolumePanelSlicesAdapter = new VolumePanelSlicesAdapter(this, mSliceLiveData);
+            mVolumePanelSlicesAdapter.setOnSliceActionListener((eventInfo, sliceItem) -> {
+                if (eventInfo.actionType == EventInfo.ACTION_TYPE_SLIDER) {
+                    return;
+                }
+                this.dismiss();
+            });
+            if (mSliceLiveData.size() < DEFAULT_SLICE_SIZE) {
+                mVolumePanelSlices.setMinimumHeight(0);
+            }
+            mVolumePanelSlices.setAdapter(mVolumePanelSlicesAdapter);
+        }
+    }
+
+    private boolean removeSliceLiveData(Uri uri) {
+        boolean removed = false;
+        // Keeps observe media output slice
+        if (!uri.equals(MEDIA_OUTPUT_INDICATOR_SLICE_URI)) {
+            Log.d(TAG, "remove uri: " + uri);
+            removed = mSliceLiveData.remove(uri) != null;
+            if (mVolumePanelSlicesAdapter != null) {
+                mVolumePanelSlicesAdapter.updateDataSet(new ArrayList<>(mSliceLiveData.values()));
+            }
+        }
+        return removed;
+    }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+        Log.d(TAG, "onStart");
+        mLifecycleRegistry.setCurrentState(Lifecycle.State.STARTED);
+        mLifecycleRegistry.setCurrentState(Lifecycle.State.RESUMED);
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+        Log.d(TAG, "onStop");
+        mLifecycleRegistry.setCurrentState(Lifecycle.State.DESTROYED);
+    }
+
+    private List<Uri> getSlices() {
+        final List<Uri> uris = new ArrayList<>();
+        uris.add(REMOTE_MEDIA_SLICE_URI);
+        uris.add(VOLUME_MEDIA_URI);
+        Uri controlUri = getExtraControlUri();
+        if (controlUri != null) {
+            Log.d(TAG, "add extra control slice");
+            uris.add(controlUri);
+        }
+        uris.add(MEDIA_OUTPUT_INDICATOR_SLICE_URI);
+        uris.add(VOLUME_CALL_URI);
+        uris.add(VOLUME_RINGER_URI);
+        uris.add(VOLUME_ALARM_URI);
+        return uris;
+    }
+
+    private static final String SETTINGS_SLICE_AUTHORITY = "com.android.settings.slices";
+    private static final Uri REMOTE_MEDIA_SLICE_URI = new Uri.Builder()
+            .scheme(ContentResolver.SCHEME_CONTENT)
+            .authority(SETTINGS_SLICE_AUTHORITY)
+            .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION)
+            .appendPath(MediaOutputConstants.KEY_REMOTE_MEDIA)
+            .build();
+    private static final Uri VOLUME_MEDIA_URI = new Uri.Builder()
+            .scheme(ContentResolver.SCHEME_CONTENT)
+            .authority(SETTINGS_SLICE_AUTHORITY)
+            .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION)
+            .appendPath("media_volume")
+            .build();
+    private static final Uri MEDIA_OUTPUT_INDICATOR_SLICE_URI = new Uri.Builder()
+            .scheme(ContentResolver.SCHEME_CONTENT)
+            .authority(SETTINGS_SLICE_AUTHORITY)
+            .appendPath(SettingsSlicesContract.PATH_SETTING_INTENT)
+            .appendPath("media_output_indicator")
+            .build();
+    private static final Uri VOLUME_CALL_URI = new Uri.Builder()
+            .scheme(ContentResolver.SCHEME_CONTENT)
+            .authority(SETTINGS_SLICE_AUTHORITY)
+            .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION)
+            .appendPath("call_volume")
+            .build();
+    private static final Uri VOLUME_RINGER_URI = new Uri.Builder()
+            .scheme(ContentResolver.SCHEME_CONTENT)
+            .authority(SETTINGS_SLICE_AUTHORITY)
+            .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION)
+            .appendPath("ring_volume")
+            .build();
+    private static final Uri VOLUME_ALARM_URI = new Uri.Builder()
+            .scheme(ContentResolver.SCHEME_CONTENT)
+            .authority(SETTINGS_SLICE_AUTHORITY)
+            .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION)
+            .appendPath("alarm_volume")
+            .build();
+
+    private Uri getExtraControlUri() {
+        Uri controlUri = null;
+        final BluetoothDevice bluetoothDevice = findActiveDevice();
+        if (bluetoothDevice != null) {
+            // The control slice width = dialog width - horizontal padding of two sides
+            final int dialogWidth =
+                    getWindow().getWindowManager().getCurrentWindowMetrics().getBounds().width();
+            final int controlSliceWidth = dialogWidth
+                    - getContext().getResources().getDimensionPixelSize(
+                    R.dimen.volume_panel_slice_horizontal_padding) * 2;
+            final String uri = BluetoothUtils.getControlUriMetaData(bluetoothDevice);
+            if (!TextUtils.isEmpty(uri)) {
+                try {
+                    controlUri = Uri.parse(uri + controlSliceWidth);
+                } catch (NullPointerException exception) {
+                    Log.d(TAG, "unable to parse extra control uri");
+                    controlUri = null;
+                }
+            }
+        }
+        return controlUri;
+    }
+
+    private BluetoothDevice findActiveDevice() {
+        if (mProfileManager != null) {
+            final A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
+            if (a2dpProfile != null) {
+                return a2dpProfile.getActiveDevice();
+            }
+        }
+        return null;
+    }
+
+    @NonNull
+    @Override
+    public Lifecycle getLifecycle() {
+        return mLifecycleRegistry;
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumePanelDialogReceiver.kt b/packages/SystemUI/src/com/android/systemui/volume/VolumePanelDialogReceiver.kt
new file mode 100644
index 0000000..f11d5d1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumePanelDialogReceiver.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.volume
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.provider.Settings
+import android.text.TextUtils
+import android.util.Log
+import javax.inject.Inject
+
+private const val TAG = "VolumePanelDialogReceiver"
+private const val LAUNCH_ACTION = "com.android.systemui.action.LAUNCH_VOLUME_PANEL_DIALOG"
+private const val DISMISS_ACTION = "com.android.systemui.action.DISMISS_VOLUME_PANEL_DIALOG"
+
+/**
+ * BroadcastReceiver for handling volume panel dialog intent
+ */
+class VolumePanelDialogReceiver @Inject constructor(
+    private val volumePanelFactory: VolumePanelFactory
+) : BroadcastReceiver() {
+    override fun onReceive(context: Context, intent: Intent) {
+        Log.d(TAG, "onReceive intent" + intent.action)
+        if (TextUtils.equals(LAUNCH_ACTION, intent.action) ||
+                TextUtils.equals(Settings.Panel.ACTION_VOLUME, intent.action)) {
+            volumePanelFactory.create(true, null)
+        } else if (TextUtils.equals(DISMISS_ACTION, intent.action)) {
+            volumePanelFactory.dismiss()
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumePanelFactory.kt b/packages/SystemUI/src/com/android/systemui/volume/VolumePanelFactory.kt
new file mode 100644
index 0000000..c2fafbf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumePanelFactory.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.volume
+
+import android.content.Context
+import android.util.Log
+import android.view.View
+import com.android.systemui.animation.DialogLaunchAnimator
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+
+private const val TAG = "VolumePanelFactory"
+private val DEBUG = Log.isLoggable(TAG, Log.DEBUG)
+
+/**
+ * Factory to create [VolumePanelDialog] objects. This is the dialog that allows the user to adjust
+ * multiple streams with sliders.
+ */
+@SysUISingleton
+class VolumePanelFactory @Inject constructor(
+    private val context: Context,
+    private val dialogLaunchAnimator: DialogLaunchAnimator
+) {
+    companion object {
+        var volumePanelDialog: VolumePanelDialog? = null
+    }
+
+    /** Creates a [VolumePanelDialog]. The dialog will be animated from [view] if it is not null. */
+    fun create(aboveStatusBar: Boolean, view: View? = null) {
+        if (volumePanelDialog?.isShowing == true) {
+            return
+        }
+
+        val dialog = VolumePanelDialog(context, aboveStatusBar)
+        volumePanelDialog = dialog
+
+        // Show the dialog.
+        if (view != null) {
+            dialogLaunchAnimator.showFromView(dialog, view, animateBackgroundBoundsChange = true)
+        } else {
+            dialog.show()
+        }
+    }
+
+    /** Dismiss [VolumePanelDialog] if exist. */
+    fun dismiss() {
+        if (DEBUG) {
+            Log.d(TAG, "dismiss dialog")
+        }
+        volumePanelDialog?.dismiss()
+        volumePanelDialog = null
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumePanelSlicesAdapter.java b/packages/SystemUI/src/com/android/systemui/volume/VolumePanelSlicesAdapter.java
new file mode 100644
index 0000000..2371402
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumePanelSlicesAdapter.java
@@ -0,0 +1,137 @@
+/*
+ * 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.volume;
+
+import static android.app.slice.Slice.HINT_ERROR;
+import static android.app.slice.SliceItem.FORMAT_SLICE;
+
+import android.content.Context;
+import android.net.Uri;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LiveData;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.slice.Slice;
+import androidx.slice.SliceItem;
+import androidx.slice.widget.SliceView;
+
+import com.android.systemui.R;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * RecyclerView adapter for Slices in Settings Panels.
+ */
+public class VolumePanelSlicesAdapter extends
+        RecyclerView.Adapter<VolumePanelSlicesAdapter.SliceRowViewHolder> {
+
+    private final List<LiveData<Slice>> mSliceLiveData;
+    private final LifecycleOwner mLifecycleOwner;
+    private SliceView.OnSliceActionListener mOnSliceActionListener;
+
+    public VolumePanelSlicesAdapter(LifecycleOwner lifecycleOwner,
+            Map<Uri, LiveData<Slice>> sliceLiveData) {
+        mLifecycleOwner = lifecycleOwner;
+        mSliceLiveData = new ArrayList<>(sliceLiveData.values());
+    }
+
+    @NonNull
+    @Override
+    public SliceRowViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
+        final Context context = viewGroup.getContext();
+        final LayoutInflater inflater = LayoutInflater.from(context);
+        View view = inflater.inflate(R.layout.volume_panel_slice_slider_row, viewGroup, false);
+        return new SliceRowViewHolder(view);
+    }
+
+    @Override
+    public void onBindViewHolder(@NonNull SliceRowViewHolder sliceRowViewHolder, int position) {
+        sliceRowViewHolder.onBind(mSliceLiveData.get(position), position);
+    }
+
+    @Override
+    public int getItemCount() {
+        return mSliceLiveData.size();
+    }
+
+    @Override
+    public int getItemViewType(int position) {
+        return position;
+    }
+
+    void setOnSliceActionListener(SliceView.OnSliceActionListener listener) {
+        mOnSliceActionListener = listener;
+    }
+
+    void updateDataSet(ArrayList<LiveData<Slice>> list) {
+        mSliceLiveData.clear();
+        mSliceLiveData.addAll(list);
+        notifyDataSetChanged();
+    }
+
+    /**
+     * ViewHolder for binding Slices to SliceViews.
+     */
+    public class SliceRowViewHolder extends RecyclerView.ViewHolder {
+
+        private final SliceView mSliceView;
+
+        public SliceRowViewHolder(View view) {
+            super(view);
+            mSliceView = view.findViewById(R.id.slice_view);
+            mSliceView.setMode(SliceView.MODE_LARGE);
+            mSliceView.setShowTitleItems(true);
+            mSliceView.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
+            mSliceView.setOnSliceActionListener(mOnSliceActionListener);
+        }
+
+        /**
+         * Called when the view is displayed.
+         */
+        public void onBind(LiveData<Slice> sliceLiveData, int position) {
+            sliceLiveData.observe(mLifecycleOwner, mSliceView);
+
+            // Do not show the divider above media devices switcher slice per request
+            final Slice slice = sliceLiveData.getValue();
+
+            // Hides slice which reports with error hint or not contain any slice sub-item.
+            if (slice == null || !isValidSlice(slice)) {
+                mSliceView.setVisibility(View.GONE);
+            } else {
+                mSliceView.setVisibility(View.VISIBLE);
+            }
+        }
+
+        private boolean isValidSlice(Slice slice) {
+            if (slice.getHints().contains(HINT_ERROR)) {
+                return false;
+            }
+            for (SliceItem item : slice.getItems()) {
+                if (item.getFormat().equals(FORMAT_SLICE)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
index f3855bd..c5792b9 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
@@ -30,6 +30,7 @@
 import com.android.systemui.volume.VolumeComponent;
 import com.android.systemui.volume.VolumeDialogComponent;
 import com.android.systemui.volume.VolumeDialogImpl;
+import com.android.systemui.volume.VolumePanelFactory;
 
 import dagger.Binds;
 import dagger.Module;
@@ -52,6 +53,7 @@
             DeviceProvisionedController deviceProvisionedController,
             ConfigurationController configurationController,
             MediaOutputDialogFactory mediaOutputDialogFactory,
+            VolumePanelFactory volumePanelFactory,
             ActivityStarter activityStarter,
             InteractionJankMonitor interactionJankMonitor) {
         VolumeDialogImpl impl = new VolumeDialogImpl(
@@ -61,6 +63,7 @@
                 deviceProvisionedController,
                 configurationController,
                 mediaOutputDialogFactory,
+                volumePanelFactory,
                 activityStarter,
                 interactionJankMonitor);
         impl.setStreamImportant(AudioManager.STREAM_SYSTEM, false);
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
index 199048e..ce96741 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
@@ -62,7 +62,6 @@
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.coordinator.BubbleCoordinator;
-import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy;
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
 import com.android.systemui.statusbar.notification.collection.notifcollection.DismissedByUserStats;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
@@ -103,7 +102,6 @@
     private final NotificationVisibilityProvider mVisibilityProvider;
     private final NotificationInterruptStateProvider mNotificationInterruptStateProvider;
     private final NotificationLockscreenUserManager mNotifUserManager;
-    private final NotificationGroupManagerLegacy mNotificationGroupManager;
     private final CommonNotifCollection mCommonNotifCollection;
     private final NotifPipeline mNotifPipeline;
     private final Executor mSysuiMainExecutor;
@@ -130,7 +128,6 @@
             NotificationInterruptStateProvider interruptionStateProvider,
             ZenModeController zenModeController,
             NotificationLockscreenUserManager notifUserManager,
-            NotificationGroupManagerLegacy groupManager,
             CommonNotifCollection notifCollection,
             NotifPipeline notifPipeline,
             SysUiState sysUiState,
@@ -148,7 +145,6 @@
                     interruptionStateProvider,
                     zenModeController,
                     notifUserManager,
-                    groupManager,
                     notifCollection,
                     notifPipeline,
                     sysUiState,
@@ -171,7 +167,6 @@
             NotificationInterruptStateProvider interruptionStateProvider,
             ZenModeController zenModeController,
             NotificationLockscreenUserManager notifUserManager,
-            NotificationGroupManagerLegacy groupManager,
             CommonNotifCollection notifCollection,
             NotifPipeline notifPipeline,
             SysUiState sysUiState,
@@ -185,7 +180,6 @@
         mVisibilityProvider = visibilityProvider;
         mNotificationInterruptStateProvider = interruptionStateProvider;
         mNotifUserManager = notifUserManager;
-        mNotificationGroupManager = groupManager;
         mCommonNotifCollection = notifCollection;
         mNotifPipeline = notifPipeline;
         mSysuiMainExecutor = sysuiMainExecutor;
@@ -331,16 +325,6 @@
             }
 
             @Override
-            public void removeNotificationEntry(String key) {
-                sysuiMainExecutor.execute(() -> {
-                    final NotificationEntry entry = mCommonNotifCollection.getEntry(key);
-                    if (entry != null) {
-                        mNotificationGroupManager.onEntryRemoved(entry);
-                    }
-                });
-            }
-
-            @Override
             public void updateNotificationBubbleButton(String key) {
                 sysuiMainExecutor.execute(() -> {
                     final NotificationEntry entry = mCommonNotifCollection.getEntry(key);
@@ -351,16 +335,6 @@
             }
 
             @Override
-            public void updateNotificationSuppression(String key) {
-                sysuiMainExecutor.execute(() -> {
-                    final NotificationEntry entry = mCommonNotifCollection.getEntry(key);
-                    if (entry != null) {
-                        mNotificationGroupManager.updateSuppression(entry);
-                    }
-                });
-            }
-
-            @Override
             public void onStackExpandChanged(boolean shouldExpand) {
                 sysuiMainExecutor.execute(() -> {
                     sysUiState.setFlag(QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED, shouldExpand)
@@ -532,7 +506,10 @@
                                     REASON_GROUP_SUMMARY_CANCELED);
                         }
                     } else {
-                        mNotificationGroupManager.onEntryRemoved(entry);
+                        for (NotifCallback cb : mCallbacks) {
+                            cb.removeNotification(entry, getDismissedByUserStats(entry, true),
+                                    REASON_GROUP_SUMMARY_CANCELED);
+                        }
                     }
                 }, mSysuiMainExecutor);
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt
new file mode 100644
index 0000000..6b1ef38
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt
@@ -0,0 +1,179 @@
+package com.android.systemui
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.pm.PackageManager
+import android.content.res.Resources
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flag
+import com.android.systemui.flags.FlagListenable
+import com.android.systemui.flags.Flags
+import com.android.systemui.flags.UnreleasedFlag
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.kotlinArgumentCaptor
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.test.TestCoroutineDispatcher
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyZeroInteractions
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class ChooserSelectorTest : SysuiTestCase() {
+
+    private val flagListener = kotlinArgumentCaptor<FlagListenable.Listener>()
+
+    private val testDispatcher = TestCoroutineDispatcher()
+    private val testScope = CoroutineScope(testDispatcher)
+
+    private lateinit var chooserSelector: ChooserSelector
+
+    @Mock private lateinit var mockContext: Context
+    @Mock private lateinit var mockPackageManager: PackageManager
+    @Mock private lateinit var mockResources: Resources
+    @Mock private lateinit var mockFeatureFlags: FeatureFlags
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+
+        `when`(mockContext.packageManager).thenReturn(mockPackageManager)
+        `when`(mockContext.resources).thenReturn(mockResources)
+        `when`(mockResources.getString(anyInt())).thenReturn(
+                ComponentName("TestPackage", "TestClass").flattenToString())
+
+        chooserSelector = ChooserSelector(mockContext, mockFeatureFlags, testScope, testDispatcher)
+    }
+
+    @After
+    fun tearDown() {
+        testDispatcher.cleanupTestCoroutines()
+    }
+
+    @Test
+    fun initialize_registersFlagListenerUntilScopeCancelled() {
+        // Arrange
+
+        // Act
+        chooserSelector.start()
+
+        // Assert
+        verify(mockFeatureFlags).addListener(
+                eq<Flag<*>>(Flags.CHOOSER_UNBUNDLED), flagListener.capture())
+        verify(mockFeatureFlags, never()).removeListener(any())
+
+        // Act
+        testScope.cancel()
+
+        // Assert
+        verify(mockFeatureFlags).removeListener(eq(flagListener.value))
+    }
+
+    @Test
+    fun initialize_enablesUnbundledChooser_whenFlagEnabled() {
+        // Arrange
+        `when`(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(true)
+
+        // Act
+        chooserSelector.start()
+
+        // Assert
+        verify(mockPackageManager).setComponentEnabledSetting(
+                eq(ComponentName("TestPackage", "TestClass")),
+                eq(PackageManager.COMPONENT_ENABLED_STATE_ENABLED),
+                anyInt())
+    }
+
+    @Test
+    fun initialize_disablesUnbundledChooser_whenFlagDisabled() {
+        // Arrange
+        `when`(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
+
+        // Act
+        chooserSelector.start()
+
+        // Assert
+        verify(mockPackageManager).setComponentEnabledSetting(
+                eq(ComponentName("TestPackage", "TestClass")),
+                eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED),
+                anyInt())
+    }
+
+    @Test
+    fun enablesUnbundledChooser_whenFlagBecomesEnabled() {
+        // Arrange
+        `when`(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
+        chooserSelector.start()
+        verify(mockFeatureFlags).addListener(
+                eq<Flag<*>>(Flags.CHOOSER_UNBUNDLED), flagListener.capture())
+        verify(mockPackageManager, never()).setComponentEnabledSetting(
+                any(), eq(PackageManager.COMPONENT_ENABLED_STATE_ENABLED), anyInt())
+
+        // Act
+        `when`(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(true)
+        flagListener.value.onFlagChanged(TestFlagEvent(Flags.CHOOSER_UNBUNDLED.id))
+
+        // Assert
+        verify(mockPackageManager).setComponentEnabledSetting(
+                eq(ComponentName("TestPackage", "TestClass")),
+                eq(PackageManager.COMPONENT_ENABLED_STATE_ENABLED),
+                anyInt())
+    }
+
+    @Test
+    fun disablesUnbundledChooser_whenFlagBecomesDisabled() {
+        // Arrange
+        `when`(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(true)
+        chooserSelector.start()
+        verify(mockFeatureFlags).addListener(
+                eq<Flag<*>>(Flags.CHOOSER_UNBUNDLED), flagListener.capture())
+        verify(mockPackageManager, never()).setComponentEnabledSetting(
+                any(), eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED), anyInt())
+
+        // Act
+        `when`(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
+        flagListener.value.onFlagChanged(TestFlagEvent(Flags.CHOOSER_UNBUNDLED.id))
+
+        // Assert
+        verify(mockPackageManager).setComponentEnabledSetting(
+                eq(ComponentName("TestPackage", "TestClass")),
+                eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED),
+                anyInt())
+    }
+
+    @Test
+    fun doesNothing_whenAnotherFlagChanges() {
+        // Arrange
+        `when`(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
+        chooserSelector.start()
+        verify(mockFeatureFlags).addListener(
+                eq<Flag<*>>(Flags.CHOOSER_UNBUNDLED), flagListener.capture())
+        clearInvocations(mockPackageManager)
+
+        // Act
+        `when`(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
+        flagListener.value.onFlagChanged(TestFlagEvent(Flags.CHOOSER_UNBUNDLED.id + 1))
+
+        // Assert
+        verifyZeroInteractions(mockPackageManager)
+    }
+
+    private class TestFlagEvent(override val flagId: Int) : FlagListenable.FlagEvent {
+        override fun requestNoRestart() {}
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FakeFeatureFlagsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FakeFeatureFlagsTest.kt
index ff579a1..318f2bc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FakeFeatureFlagsTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FakeFeatureFlagsTest.kt
@@ -41,7 +41,7 @@
      * specified. If not, an exception is thrown.
      */
     @Test
-    fun throwsIfUnspecifiedFlagIsAccessed() {
+    fun accessingUnspecifiedFlags_throwsException() {
         val flags: FeatureFlags = FakeFeatureFlags()
         try {
             assertThat(flags.isEnabled(Flags.TEAMFOOD)).isFalse()
@@ -88,7 +88,7 @@
     }
 
     @Test
-    fun specifiedFlagsReturnCorrectValues() {
+    fun specifiedFlags_returnCorrectValues() {
         val flags = FakeFeatureFlags()
         flags.set(unreleasedFlag, false)
         flags.set(releasedFlag, false)
@@ -114,4 +114,125 @@
         assertThat(flags.isEnabled(sysPropBooleanFlag)).isTrue()
         assertThat(flags.getString(resourceStringFlag)).isEqualTo("Android")
     }
+
+    @Test
+    fun listenerForBooleanFlag_calledOnlyWhenFlagChanged() {
+        val flags = FakeFeatureFlags()
+        val listener = VerifyingListener()
+        flags.addListener(unreleasedFlag, listener)
+
+        flags.set(unreleasedFlag, true)
+        flags.set(unreleasedFlag, true)
+        flags.set(unreleasedFlag, false)
+        flags.set(unreleasedFlag, false)
+
+        listener.verifyInOrder(unreleasedFlag.id, unreleasedFlag.id)
+    }
+
+    @Test
+    fun listenerForStringFlag_calledOnlyWhenFlagChanged() {
+        val flags = FakeFeatureFlags()
+        val listener = VerifyingListener()
+        flags.addListener(stringFlag, listener)
+
+        flags.set(stringFlag, "Test")
+        flags.set(stringFlag, "Test")
+
+        listener.verifyInOrder(stringFlag.id)
+    }
+
+    @Test
+    fun listenerForBooleanFlag_notCalledAfterRemoved() {
+        val flags = FakeFeatureFlags()
+        val listener = VerifyingListener()
+        flags.addListener(unreleasedFlag, listener)
+        flags.set(unreleasedFlag, true)
+        flags.removeListener(listener)
+        flags.set(unreleasedFlag, false)
+
+        listener.verifyInOrder(unreleasedFlag.id)
+    }
+
+    @Test
+    fun listenerForStringFlag_notCalledAfterRemoved() {
+        val flags = FakeFeatureFlags()
+        val listener = VerifyingListener()
+
+        flags.addListener(stringFlag, listener)
+        flags.set(stringFlag, "Test")
+        flags.removeListener(listener)
+        flags.set(stringFlag, "Other")
+
+        listener.verifyInOrder(stringFlag.id)
+    }
+
+    @Test
+    fun listenerForMultipleFlags_calledWhenFlagsChange() {
+        val flags = FakeFeatureFlags()
+        val listener = VerifyingListener()
+        flags.addListener(unreleasedFlag, listener)
+        flags.addListener(releasedFlag, listener)
+
+        flags.set(releasedFlag, true)
+        flags.set(unreleasedFlag, true)
+
+        listener.verifyInOrder(releasedFlag.id, unreleasedFlag.id)
+    }
+
+    @Test
+    fun listenerForMultipleFlags_notCalledAfterRemoved() {
+        val flags = FakeFeatureFlags()
+        val listener = VerifyingListener()
+
+        flags.addListener(unreleasedFlag, listener)
+        flags.addListener(releasedFlag, listener)
+        flags.set(releasedFlag, true)
+        flags.set(unreleasedFlag, true)
+        flags.removeListener(listener)
+        flags.set(releasedFlag, false)
+        flags.set(unreleasedFlag, false)
+
+        listener.verifyInOrder(releasedFlag.id, unreleasedFlag.id)
+    }
+
+    @Test
+    fun multipleListenersForSingleFlag_allAreCalledWhenChanged() {
+        val flags = FakeFeatureFlags()
+        val listener1 = VerifyingListener()
+        val listener2 = VerifyingListener()
+        flags.addListener(releasedFlag, listener1)
+        flags.addListener(releasedFlag, listener2)
+
+        flags.set(releasedFlag, true)
+
+        listener1.verifyInOrder(releasedFlag.id)
+        listener2.verifyInOrder(releasedFlag.id)
+    }
+
+    @Test
+    fun multipleListenersForSingleFlag_removedListenerNotCalledAfterRemoval() {
+        val flags = FakeFeatureFlags()
+        val listener1 = VerifyingListener()
+        val listener2 = VerifyingListener()
+        flags.addListener(releasedFlag, listener1)
+        flags.addListener(releasedFlag, listener2)
+
+        flags.set(releasedFlag, true)
+        flags.removeListener(listener2)
+        flags.set(releasedFlag, false)
+
+        listener1.verifyInOrder(releasedFlag.id, releasedFlag.id)
+        listener2.verifyInOrder(releasedFlag.id)
+    }
+
+    class VerifyingListener : FlagListenable.Listener {
+        var flagEventIds = mutableListOf<Int>()
+        override fun onFlagChanged(event: FlagListenable.FlagEvent) {
+            flagEventIds.add(event.flagId)
+        }
+
+        fun verifyInOrder(vararg eventIds: Int) {
+            assertThat(flagEventIds).containsExactlyElementsIn(eventIds.asList())
+        }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
index 1785022..bef4695 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
@@ -1051,6 +1051,17 @@
     }
 
     @Test
+    fun bindDeviceWithNullName() {
+        val fallbackString = context.getResources().getString(R.string.media_seamless_other_device)
+        player.attachPlayer(viewHolder)
+        val state = mediaData.copy(device = device.copy(name = null))
+        player.bindPlayer(state, PACKAGE)
+        assertThat(seamless.isEnabled()).isTrue()
+        assertThat(seamlessText.getText()).isEqualTo(fallbackString)
+        assertThat(seamless.contentDescription).isEqualTo(fallbackString)
+    }
+
+    @Test
     fun bindDeviceResumptionPlayer() {
         player.attachPlayer(viewHolder)
         val state = mediaData.copy(resumption = true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
index ee10426..121c894 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
@@ -59,8 +59,8 @@
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyNoMoreInteractions
-import org.mockito.junit.MockitoJUnit
 import org.mockito.Mockito.`when` as whenever
+import org.mockito.junit.MockitoJUnit
 
 private const val KEY = "TEST_KEY"
 private const val KEY_OLD = "TEST_KEY_OLD"
@@ -402,9 +402,10 @@
         manager.onMediaDataLoaded(KEY, null, mediaData)
         fakeBgExecutor.runAllReady()
         fakeFgExecutor.runAllReady()
-        // THEN the device is disabled
+        // THEN the device is disabled and name is set to null
         val data = captureDeviceData(KEY)
         assertThat(data.enabled).isFalse()
+        assertThat(data.name).isNull()
     }
 
     @Test
@@ -421,9 +422,10 @@
         deviceCallback.onSelectedDeviceStateChanged(device, 1)
         fakeBgExecutor.runAllReady()
         fakeFgExecutor.runAllReady()
-        // THEN the device is disabled
+        // THEN the device is disabled and name is set to null
         val data = captureDeviceData(KEY)
         assertThat(data.enabled).isFalse()
+        assertThat(data.name).isNull()
     }
 
     @Test
@@ -440,9 +442,24 @@
         deviceCallback.onDeviceListUpdate(mutableListOf(device))
         fakeBgExecutor.runAllReady()
         fakeFgExecutor.runAllReady()
-        // THEN the device is disabled
+        // THEN the device is disabled and name is set to null
         val data = captureDeviceData(KEY)
         assertThat(data.enabled).isFalse()
+        assertThat(data.name).isNull()
+    }
+
+    @Test
+    fun mr2ReturnsRouteWithNullName_useLocalDeviceName() {
+        // GIVEN that MR2Manager returns a routing session that does not have a name
+        whenever(route.name).thenReturn(null)
+        // WHEN a notification is added
+        manager.onMediaDataLoaded(KEY, null, mediaData)
+        fakeBgExecutor.runAllReady()
+        fakeFgExecutor.runAllReady()
+        // THEN the device is enabled and uses the current connected device name
+        val data = captureDeviceData(KEY)
+        assertThat(data.name).isEqualTo(DEVICE_NAME)
+        assertThat(data.enabled).isTrue()
     }
 
     @Test
@@ -647,12 +664,14 @@
             override fun onPlaybackStopped(reason: Int, broadcastId: Int) {}
             override fun onBroadcastUpdated(reason: Int, broadcastId: Int) {}
             override fun onBroadcastUpdateFailed(reason: Int, broadcastId: Int) {}
-            override fun onBroadcastMetadataChanged(broadcastId: Int,
-                                                    metadata: BluetoothLeBroadcastMetadata) {}
+            override fun onBroadcastMetadataChanged(
+                broadcastId: Int,
+                metadata: BluetoothLeBroadcastMetadata
+            ) {}
         }
 
         bluetoothLeBroadcast.registerCallback(fakeFgExecutor, callback)
-        return callback;
+        return callback
     }
 
     fun setupLeAudioConfiguration(isLeAudio: Boolean) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java
new file mode 100644
index 0000000..d91baa5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/InternetTileTest.java
@@ -0,0 +1,116 @@
+/*
+ * 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.qs.tiles;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+
+import android.os.Handler;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.classifier.FalsingManagerFake;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.qs.QSTileHost;
+import com.android.systemui.qs.logging.QSLogger;
+import com.android.systemui.qs.tiles.dialog.InternetDialogFactory;
+import com.android.systemui.statusbar.connectivity.AccessPointController;
+import com.android.systemui.statusbar.connectivity.NetworkController;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@SmallTest
+public class InternetTileTest extends SysuiTestCase {
+
+    @Mock
+    private QSTileHost mHost;
+    @Mock
+    private NetworkController mNetworkController;
+    @Mock
+    private AccessPointController mAccessPointController;
+    @Mock
+    private InternetDialogFactory mInternetDialogFactory;
+
+    private TestableLooper mTestableLooper;
+    private InternetTile mTile;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mTestableLooper = TestableLooper.get(this);
+        when(mHost.getContext()).thenReturn(mContext);
+        when(mHost.getUserContext()).thenReturn(mContext);
+
+        mTile = new InternetTile(
+            mHost,
+            mTestableLooper.getLooper(),
+            new Handler(mTestableLooper.getLooper()),
+            new FalsingManagerFake(),
+            mock(MetricsLogger.class),
+            mock(StatusBarStateController.class),
+            mock(ActivityStarter.class),
+            mock(QSLogger.class),
+            mNetworkController,
+            mAccessPointController,
+            mInternetDialogFactory
+        );
+
+        mTile.initialize();
+        mTestableLooper.processAllMessages();
+    }
+
+    @Test
+    public void setConnectivityStatus_defaultNetworkNotExists_updateTile() {
+        mTile.mSignalCallback.setConnectivityStatus(
+            /* noDefaultNetwork= */ true,
+            /* noValidatedNetwork= */ true,
+            /* noNetworksAvailable= */ true);
+        mTestableLooper.processAllMessages();
+        assertThat(String.valueOf(mTile.getState().secondaryLabel))
+            .isEqualTo(mContext.getString(R.string.quick_settings_networks_unavailable));
+        assertThat(mTile.getLastTileState()).isEqualTo(1);
+    }
+
+    @Test
+    public void setConnectivityStatus_defaultNetworkExists_notUpdateTile() {
+        mTile.mSignalCallback.setConnectivityStatus(
+            /* noDefaultNetwork= */ false,
+            /* noValidatedNetwork= */ true,
+            /* noNetworksAvailable= */ true);
+        mTestableLooper.processAllMessages();
+        assertThat(String.valueOf(mTile.getState().secondaryLabel))
+            .isNotEqualTo(mContext.getString(R.string.quick_settings_networks_unavailable));
+        assertThat(String.valueOf(mTile.getState().secondaryLabel))
+            .isNotEqualTo(mContext.getString(R.string.quick_settings_networks_available));
+        assertThat(mTile.getLastTileState()).isEqualTo(-1);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
index 48fbd35..073c23c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
@@ -23,6 +23,7 @@
 import android.graphics.Rect
 import android.hardware.HardwareBuffer
 import android.os.Bundle
+import android.os.UserHandle
 import android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_CHORD
 import android.view.WindowManager.ScreenshotSource.SCREENSHOT_OTHER
 import android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN
@@ -97,7 +98,7 @@
         policy.setManagedProfile(USER_ID, false)
         policy.setDisplayContentInfo(
             policy.getDefaultDisplayId(),
-            DisplayContentInfo(component, bounds, USER_ID, TASK_ID))
+            DisplayContentInfo(component, bounds, UserHandle.of(USER_ID), TASK_ID))
 
         val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD)
         val processor = RequestProcessor(imageCapture, policy, flags, scope)
@@ -120,7 +121,7 @@
         // Indicate that the primary content belongs to a manged profile
         policy.setManagedProfile(USER_ID, true)
         policy.setDisplayContentInfo(policy.getDefaultDisplayId(),
-            DisplayContentInfo(component, bounds, USER_ID, TASK_ID))
+            DisplayContentInfo(component, bounds, UserHandle.of(USER_ID), TASK_ID))
 
         val request = ScreenshotRequest(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_CHORD)
         val processor = RequestProcessor(imageCapture, policy, flags, scope)
@@ -160,7 +161,7 @@
 
         policy.setManagedProfile(USER_ID, false)
         policy.setDisplayContentInfo(policy.getDefaultDisplayId(),
-            DisplayContentInfo(component, bounds, USER_ID, TASK_ID))
+            DisplayContentInfo(component, bounds, UserHandle.of(USER_ID), TASK_ID))
 
         val processedRequest = processor.process(request)
 
@@ -183,7 +184,7 @@
         // Indicate that the primary content belongs to a manged profile
         policy.setManagedProfile(USER_ID, true)
         policy.setDisplayContentInfo(policy.getDefaultDisplayId(),
-            DisplayContentInfo(component, bounds, USER_ID, TASK_ID))
+            DisplayContentInfo(component, bounds, UserHandle.of(USER_ID), TASK_ID))
 
         val processedRequest = processor.process(request)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotPolicyImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotPolicyImplTest.kt
new file mode 100644
index 0000000..17396b1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotPolicyImplTest.kt
@@ -0,0 +1,227 @@
+/*
+ * 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.screenshot
+
+import android.app.ActivityTaskManager.RootTaskInfo
+import android.app.IActivityTaskManager
+import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
+import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
+import android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED
+import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+import android.app.WindowConfiguration.WINDOWING_MODE_PINNED
+import android.content.ComponentName
+import android.content.Context
+import android.graphics.Rect
+import android.os.UserHandle
+import android.os.UserManager
+import android.testing.AndroidTestingRunner
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.screenshot.ScreenshotPolicy.DisplayContentInfo
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+
+// The following values are chosen to be distinct from commonly seen real values
+private const val DISPLAY_ID = 100
+private const val PRIMARY_USER = 2000
+private const val MANAGED_PROFILE_USER = 3000
+
+@RunWith(AndroidTestingRunner::class)
+class ScreenshotPolicyImplTest : SysuiTestCase() {
+
+    @Test
+    fun testToDisplayContentInfo() {
+        assertThat(fullScreenWorkProfileTask.toDisplayContentInfo())
+            .isEqualTo(
+                DisplayContentInfo(
+                    ComponentName(
+                        "com.google.android.apps.nbu.files",
+                        "com.google.android.apps.nbu.files.home.HomeActivity"
+                    ),
+                    Rect(0, 0, 1080, 2400),
+                    UserHandle.of(MANAGED_PROFILE_USER),
+                    65))
+    }
+
+    @Test
+    fun findPrimaryContent_ignoresPipTask() = runBlocking {
+        val policy = fakeTasksPolicyImpl(
+            mContext,
+            shadeExpanded = false,
+            tasks = listOf(
+                    pipTask,
+                    fullScreenWorkProfileTask,
+                    launcherTask,
+                    emptyTask)
+        )
+
+        val info = policy.findPrimaryContent(DISPLAY_ID)
+        assertThat(info).isEqualTo(fullScreenWorkProfileTask.toDisplayContentInfo())
+    }
+
+    @Test
+    fun findPrimaryContent_shadeExpanded_ignoresTopTask() = runBlocking {
+        val policy = fakeTasksPolicyImpl(
+            mContext,
+            shadeExpanded = true,
+            tasks = listOf(
+                fullScreenWorkProfileTask,
+                launcherTask,
+                emptyTask)
+        )
+
+        val info = policy.findPrimaryContent(DISPLAY_ID)
+        assertThat(info).isEqualTo(policy.systemUiContent)
+    }
+
+    @Test
+    fun findPrimaryContent_emptyTaskList() = runBlocking {
+        val policy = fakeTasksPolicyImpl(
+            mContext,
+            shadeExpanded = false,
+            tasks = listOf()
+        )
+
+        val info = policy.findPrimaryContent(DISPLAY_ID)
+        assertThat(info).isEqualTo(policy.systemUiContent)
+    }
+
+    @Test
+    fun findPrimaryContent_workProfileNotOnTop() = runBlocking {
+        val policy = fakeTasksPolicyImpl(
+            mContext,
+            shadeExpanded = false,
+            tasks = listOf(
+                launcherTask,
+                fullScreenWorkProfileTask,
+                emptyTask)
+        )
+
+        val info = policy.findPrimaryContent(DISPLAY_ID)
+        assertThat(info).isEqualTo(launcherTask.toDisplayContentInfo())
+    }
+
+    private fun fakeTasksPolicyImpl(
+        context: Context,
+        shadeExpanded: Boolean,
+        tasks: List<RootTaskInfo>
+    ): ScreenshotPolicyImpl {
+        val userManager = mock<UserManager>()
+        val atmService = mock<IActivityTaskManager>()
+        val dispatcher = Dispatchers.Unconfined
+
+        return object : ScreenshotPolicyImpl(context, userManager, atmService, dispatcher) {
+            override suspend fun isManagedProfile(userId: Int) = (userId == MANAGED_PROFILE_USER)
+            override suspend fun getAllRootTaskInfosOnDisplay(displayId: Int) = tasks
+            override suspend fun isNotificationShadeExpanded() = shadeExpanded
+        }
+    }
+
+    private val pipTask = RootTaskInfo().apply {
+        configuration.windowConfiguration.apply {
+            windowingMode = WINDOWING_MODE_PINNED
+            bounds = Rect(628, 1885, 1038, 2295)
+            activityType = ACTIVITY_TYPE_STANDARD
+        }
+        displayId = DISPLAY_ID
+        userId = PRIMARY_USER
+        taskId = 66
+        visible = true
+        isVisible = true
+        isRunning = true
+        numActivities = 1
+        topActivity = ComponentName(
+            "com.google.android.youtube",
+            "com.google.android.apps.youtube.app.watchwhile.WatchWhileActivity"
+        )
+        childTaskIds = intArrayOf(66)
+        childTaskNames = arrayOf("com.google.android.youtube/" +
+                "com.google.android.youtube.app.honeycomb.Shell\$HomeActivity")
+        childTaskUserIds = intArrayOf(0)
+        childTaskBounds = arrayOf(Rect(628, 1885, 1038, 2295))
+    }
+
+    private val fullScreenWorkProfileTask = RootTaskInfo().apply {
+        configuration.windowConfiguration.apply {
+            windowingMode = WINDOWING_MODE_FULLSCREEN
+            bounds = Rect(0, 0, 1080, 2400)
+            activityType = ACTIVITY_TYPE_STANDARD
+        }
+        displayId = DISPLAY_ID
+        userId = MANAGED_PROFILE_USER
+        taskId = 65
+        visible = true
+        isVisible = true
+        isRunning = true
+        numActivities = 1
+        topActivity = ComponentName(
+            "com.google.android.apps.nbu.files",
+            "com.google.android.apps.nbu.files.home.HomeActivity"
+        )
+        childTaskIds = intArrayOf(65)
+        childTaskNames = arrayOf("com.google.android.apps.nbu.files/" +
+                "com.google.android.apps.nbu.files.home.HomeActivity")
+        childTaskUserIds = intArrayOf(MANAGED_PROFILE_USER)
+        childTaskBounds = arrayOf(Rect(0, 0, 1080, 2400))
+    }
+
+    private val launcherTask = RootTaskInfo().apply {
+        configuration.windowConfiguration.apply {
+            windowingMode = WINDOWING_MODE_FULLSCREEN
+            bounds = Rect(0, 0, 1080, 2400)
+            activityType = ACTIVITY_TYPE_HOME
+        }
+        displayId = DISPLAY_ID
+        taskId = 1
+        userId = PRIMARY_USER
+        visible = true
+        isVisible = true
+        isRunning = true
+        numActivities = 1
+        topActivity = ComponentName(
+            "com.google.android.apps.nexuslauncher",
+            "com.google.android.apps.nexuslauncher.NexusLauncherActivity",
+        )
+        childTaskIds = intArrayOf(1)
+        childTaskNames = arrayOf("com.google.android.apps.nexuslauncher/" +
+                "com.google.android.apps.nexuslauncher.NexusLauncherActivity")
+        childTaskUserIds = intArrayOf(0)
+        childTaskBounds = arrayOf(Rect(0, 0, 1080, 2400))
+    }
+
+    private val emptyTask = RootTaskInfo().apply {
+        configuration.windowConfiguration.apply {
+            windowingMode = WINDOWING_MODE_FULLSCREEN
+            bounds = Rect(0, 0, 1080, 2400)
+            activityType = ACTIVITY_TYPE_UNDEFINED
+        }
+        displayId = DISPLAY_ID
+        taskId = 2
+        userId = PRIMARY_USER
+        visible = false
+        isVisible = false
+        isRunning = false
+        numActivities = 0
+        childTaskIds = intArrayOf(3, 4)
+        childTaskNames = arrayOf("", "")
+        childTaskUserIds = intArrayOf(0, 0)
+        childTaskBounds = arrayOf(Rect(0, 0, 1080, 2400), Rect(0, 2400, 1080, 4800))
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
index 8473c37..6afadcf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
@@ -20,6 +20,7 @@
 import static android.app.admin.DevicePolicyManager.DEVICE_OWNER_TYPE_FINANCED;
 import static android.content.pm.UserInfo.FLAG_MANAGED_PROFILE;
 
+import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED;
 import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_ALIGNMENT;
 import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_BATTERY;
 import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_BIOMETRIC_MESSAGE;
@@ -31,6 +32,7 @@
 import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_TRANSIENT;
 import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_TRUST;
 import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_USER_LOCKED;
+import static com.android.systemui.keyguard.ScreenLifecycle.SCREEN_OFF;
 import static com.android.systemui.keyguard.ScreenLifecycle.SCREEN_ON;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -178,9 +180,12 @@
     private ArgumentCaptor<KeyguardUpdateMonitorCallback> mKeyguardUpdateMonitorCallbackCaptor;
     @Captor
     private ArgumentCaptor<KeyguardStateController.Callback> mKeyguardStateControllerCallbackCaptor;
+    @Captor
+    private ArgumentCaptor<ScreenLifecycle.Observer> mScreenObserverCaptor;
     private KeyguardStateController.Callback mKeyguardStateControllerCallback;
     private KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback;
     private StatusBarStateController.StateListener mStatusBarStateListener;
+    private ScreenLifecycle.Observer mScreenObserver;
     private BroadcastReceiver mBroadcastReceiver;
     private FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
     private TestableLooper mTestableLooper;
@@ -273,6 +278,9 @@
                 mKeyguardUpdateMonitorCallbackCaptor.capture());
         mKeyguardUpdateMonitorCallback = mKeyguardUpdateMonitorCallbackCaptor.getValue();
 
+        verify(mScreenLifecycle).addObserver(mScreenObserverCaptor.capture());
+        mScreenObserver = mScreenObserverCaptor.getValue();
+
         mExecutor.runAllReady();
         reset(mRotateTextViewController);
     }
@@ -537,6 +545,31 @@
     }
 
     @Test
+    public void transientIndication_visibleWhenWokenUp() {
+        createController();
+        when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(false);
+        final String message = "helpMsg";
+
+        // GIVEN screen is off
+        when(mScreenLifecycle.getScreenState()).thenReturn(SCREEN_OFF);
+
+        // WHEN fingeprint help message received
+        mController.setVisible(true);
+        mController.getKeyguardCallback().onBiometricHelp(BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED,
+                message, BiometricSourceType.FINGERPRINT);
+
+        // THEN message isn't shown right away
+        verifyNoMessage(INDICATION_TYPE_BIOMETRIC_MESSAGE);
+
+        // WHEN the screen turns on
+        mScreenObserver.onScreenTurnedOn();
+        mTestableLooper.processAllMessages();
+
+        // THEN the message is shown
+        verifyIndicationMessage(INDICATION_TYPE_BIOMETRIC_MESSAGE, message);
+    }
+
+    @Test
     public void transientIndication_visibleWhenDozing_unlessSwipeUp_fromError() {
         createController();
         String message = mContext.getString(R.string.keyguard_unlock);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
index dd2b667..3cc4ee1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerTest.java
@@ -16,15 +16,8 @@
 
 package com.android.systemui.statusbar;
 
-import static android.app.NotificationManager.IMPORTANCE_HIGH;
-import static android.app.NotificationManager.IMPORTANCE_LOW;
 import static android.content.Intent.ACTION_USER_SWITCHED;
 
-import static com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt.BUCKET_ALERTING;
-import static com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt.BUCKET_MEDIA_CONTROLS;
-import static com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt.BUCKET_PEOPLE;
-import static com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt.BUCKET_SILENT;
-
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
 
@@ -60,7 +53,6 @@
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.statusbar.NotificationLockscreenUserManager.KeyguardNotificationSuppressor;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager.NotificationStateChangedListener;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -359,93 +351,6 @@
         verify(listener, never()).onNotificationStateChanged();
     }
 
-    @Test
-    public void testShowSilentNotifications_settingSaysShow() {
-        mSettings.putInt(Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, 1);
-        mSettings.putInt(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, 1);
-        mLockscreenUserManager.getLockscreenSettingsObserverForTest().onChange(false);
-
-        NotificationEntry entry = new NotificationEntryBuilder()
-                .setImportance(IMPORTANCE_LOW)
-                .build();
-        entry.setBucket(BUCKET_SILENT);
-
-        assertTrue(mLockscreenUserManager.shouldShowOnKeyguard(entry));
-    }
-
-    @Test
-    public void testShowSilentNotifications_settingSaysHide() {
-        mSettings.putInt(Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, 1);
-        mSettings.putInt(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, 0);
-        mLockscreenUserManager.getLockscreenSettingsObserverForTest().onChange(false);
-
-        final Notification notification = mock(Notification.class);
-        when(notification.isForegroundService()).thenReturn(true);
-        NotificationEntry entry = new NotificationEntryBuilder()
-                .setImportance(IMPORTANCE_LOW)
-                .setNotification(notification)
-                .build();
-        entry.setBucket(BUCKET_SILENT);
-        assertFalse(mLockscreenUserManager.shouldShowOnKeyguard(entry));
-    }
-
-    @Test
-    public void testShowSilentNotificationsPeopleBucket_settingSaysHide() {
-        mSettings.putInt(Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, 1);
-        mSettings.putInt(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, 0);
-        mLockscreenUserManager.getLockscreenSettingsObserverForTest().onChange(false);
-
-        final Notification notification = mock(Notification.class);
-        when(notification.isForegroundService()).thenReturn(true);
-        NotificationEntry entry = new NotificationEntryBuilder()
-                .setImportance(IMPORTANCE_LOW)
-                .setNotification(notification)
-                .build();
-        entry.setBucket(BUCKET_PEOPLE);
-        assertFalse(mLockscreenUserManager.shouldShowOnKeyguard(entry));
-    }
-
-    @Test
-    public void testShowSilentNotificationsMediaBucket_settingSaysHide() {
-        mSettings.putInt(Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, 1);
-        mSettings.putInt(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS, 0);
-        mLockscreenUserManager.getLockscreenSettingsObserverForTest().onChange(false);
-
-        final Notification notification = mock(Notification.class);
-        when(notification.isForegroundService()).thenReturn(true);
-        NotificationEntry entry = new NotificationEntryBuilder()
-                .setImportance(IMPORTANCE_LOW)
-                .setNotification(notification)
-                .build();
-        entry.setBucket(BUCKET_MEDIA_CONTROLS);
-        // always show media controls, even if they're silent
-        assertTrue(mLockscreenUserManager.shouldShowOnKeyguard(entry));
-    }
-
-    @Test
-    public void testKeyguardNotificationSuppressors() {
-        // GIVEN a notification that should be shown on the lockscreen
-        mSettings.putInt(Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS, 1);
-        mLockscreenUserManager.getLockscreenSettingsObserverForTest().onChange(false);
-        final NotificationEntry entry = new NotificationEntryBuilder()
-                .setImportance(IMPORTANCE_HIGH)
-                .build();
-        entry.setBucket(BUCKET_ALERTING);
-
-        // WHEN a suppressor is added that filters out all entries
-        FakeKeyguardSuppressor suppressor = new FakeKeyguardSuppressor();
-        mLockscreenUserManager.addKeyguardNotificationSuppressor(suppressor);
-
-        // THEN it's filtered out
-        assertFalse(mLockscreenUserManager.shouldShowOnKeyguard(entry));
-
-        // WHEN the suppressor no longer filters out entries
-        suppressor.setShouldSuppress(false);
-
-        // THEN it's no longer filtered out
-        assertTrue(mLockscreenUserManager.shouldShowOnKeyguard(entry));
-    }
-
     private class TestNotificationLockscreenUserManager
             extends NotificationLockscreenUserManagerImpl {
         public TestNotificationLockscreenUserManager(Context context) {
@@ -478,17 +383,4 @@
             return mSettingsObserver;
         }
     }
-
-    private static class FakeKeyguardSuppressor implements KeyguardNotificationSuppressor {
-        private boolean mShouldSuppress = true;
-
-        @Override
-        public boolean shouldSuppressOnKeyguard(NotificationEntry entry) {
-            return mShouldSuppress;
-        }
-
-        public void setShouldSuppress(boolean shouldSuppress) {
-            mShouldSuppress = shouldSuppress;
-        }
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java
index 3fc0c81..b719c7f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/DynamicPrivacyControllerTest.java
@@ -18,7 +18,6 @@
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.reset;
@@ -128,8 +127,6 @@
     @Test
     public void testNotNotifiedWithoutNotifications() {
         when(mKeyguardStateController.canDismissLockScreen()).thenReturn(true);
-        when(mLockScreenUserManager.shouldHideNotifications(anyInt())).thenReturn(
-                true);
         mDynamicPrivacyController.onUnlockedChanged();
         verifyNoMoreInteractions(mListener);
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
index 4df99be..851517e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
@@ -1518,7 +1518,7 @@
         verify(mCollectionListener).onRankingApplied();
         verifyNoMoreInteractions(mCollectionListener);
         verify(mLogger).logMissingRankings(eq(List.of(entry1)), eq(1), any());
-        verify(mLogger, never()).logRecoveredRankings(any());
+        verify(mLogger, never()).logRecoveredRankings(any(), anyInt());
         clearInvocations(mCollectionListener, mLogger);
 
         // WHEN a ranking update includes key1 again
@@ -1530,7 +1530,7 @@
         verify(mCollectionListener).onRankingApplied();
         verifyNoMoreInteractions(mCollectionListener);
         verify(mLogger, never()).logMissingRankings(any(), anyInt(), any());
-        verify(mLogger).logRecoveredRankings(eq(List.of(key1)));
+        verify(mLogger).logRecoveredRankings(eq(List.of(key1)), eq(0));
     }
 
     @Test
@@ -1556,7 +1556,7 @@
         verify(mCollectionListener).onRankingApplied();
         verifyNoMoreInteractions(mCollectionListener);
         verify(mLogger).logMissingRankings(eq(List.of(entry1)), eq(1), any());
-        verify(mLogger, never()).logRecoveredRankings(any());
+        verify(mLogger, never()).logRecoveredRankings(any(), anyInt());
         clearInvocations(mCollectionListener, mLogger);
 
         // WHEN a ranking update includes key1 again
@@ -1568,7 +1568,7 @@
         verify(mCollectionListener).onRankingApplied();
         verifyNoMoreInteractions(mCollectionListener);
         verify(mLogger, never()).logMissingRankings(any(), anyInt(), any());
-        verify(mLogger).logRecoveredRankings(eq(List.of(key1)));
+        verify(mLogger).logRecoveredRankings(eq(List.of(key1)), eq(0));
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/legacy/GroupEventDispatcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/legacy/GroupEventDispatcherTest.kt
deleted file mode 100644
index c17fe6f..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/legacy/GroupEventDispatcherTest.kt
+++ /dev/null
@@ -1,294 +0,0 @@
-/*
- * 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.notification.collection.legacy
-
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper.RunWithLooper
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy.GroupEventDispatcher
-import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy.NotificationGroup
-import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy.OnGroupChangeListener
-import com.android.systemui.statusbar.phone.NotificationGroupTestHelper
-import com.android.systemui.util.mockito.mock
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyNoMoreInteractions
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-@RunWithLooper
-class GroupEventDispatcherTest : SysuiTestCase() {
-    val groupMap = mutableMapOf<String, NotificationGroup>()
-    val groupTestHelper = NotificationGroupTestHelper(mContext)
-
-    private val dispatcher = GroupEventDispatcher(groupMap::get)
-    private val listener: OnGroupChangeListener = mock()
-
-    @Before
-    fun setup() {
-        dispatcher.registerGroupChangeListener(listener)
-    }
-
-    @Test
-    fun testOnGroupsChangedUnbuffered() {
-        dispatcher.notifyGroupsChanged()
-        verify(listener).onGroupsChanged()
-        verifyNoMoreInteractions(listener)
-    }
-
-    @Test
-    fun testOnGroupsChangedBuffered() {
-        dispatcher.openBufferScope()
-        dispatcher.notifyGroupsChanged()
-        verifyNoMoreInteractions(listener)
-        dispatcher.closeBufferScope()
-        verify(listener).onGroupsChanged()
-        verifyNoMoreInteractions(listener)
-    }
-
-    @Test
-    fun testOnGroupsChangedDoubleBuffered() {
-        dispatcher.openBufferScope()
-        dispatcher.notifyGroupsChanged()
-        dispatcher.openBufferScope() // open a nested buffer scope
-        dispatcher.notifyGroupsChanged()
-        dispatcher.closeBufferScope() // should NOT flush events
-        dispatcher.notifyGroupsChanged()
-        verifyNoMoreInteractions(listener)
-        dispatcher.closeBufferScope() // this SHOULD flush events
-        verify(listener).onGroupsChanged()
-        verifyNoMoreInteractions(listener)
-    }
-
-    @Test
-    fun testOnGroupsChangedBufferCoalesces() {
-        dispatcher.openBufferScope()
-        dispatcher.notifyGroupsChanged()
-        dispatcher.notifyGroupsChanged()
-        verifyNoMoreInteractions(listener)
-        dispatcher.closeBufferScope()
-        verify(listener).onGroupsChanged()
-        verifyNoMoreInteractions(listener)
-    }
-
-    @Test
-    fun testOnGroupCreatedIsNeverBuffered() {
-        val group = addGroup(1)
-
-        dispatcher.openBufferScope()
-        dispatcher.notifyGroupCreated(group)
-        verify(listener).onGroupCreated(group, group.groupKey)
-        verifyNoMoreInteractions(listener)
-
-        dispatcher.closeBufferScope()
-        verifyNoMoreInteractions(listener)
-    }
-
-    @Test
-    fun testOnGroupRemovedIsNeverBuffered() {
-        val group = addGroup(1)
-
-        dispatcher.openBufferScope()
-        dispatcher.notifyGroupRemoved(group)
-        verify(listener).onGroupRemoved(group, group.groupKey)
-        verifyNoMoreInteractions(listener)
-
-        dispatcher.closeBufferScope()
-        verifyNoMoreInteractions(listener)
-    }
-
-    @Test
-    fun testAlertOverrideAddedUnbuffered() {
-        val group = addGroup(1)
-        val newAlertEntry = groupTestHelper.createChildNotification()
-        group.alertOverride = newAlertEntry
-        dispatcher.notifyAlertOverrideChanged(group, null)
-        verify(listener).onGroupAlertOverrideChanged(group, null, newAlertEntry)
-        verifyNoMoreInteractions(listener)
-    }
-
-    @Test
-    fun testAlertOverrideRemovedUnbuffered() {
-        val group = addGroup(1)
-        val oldAlertEntry = groupTestHelper.createChildNotification()
-        dispatcher.notifyAlertOverrideChanged(group, oldAlertEntry)
-        verify(listener).onGroupAlertOverrideChanged(group, oldAlertEntry, null)
-        verifyNoMoreInteractions(listener)
-    }
-
-    @Test
-    fun testAlertOverrideChangedUnbuffered() {
-        val group = addGroup(1)
-        val oldAlertEntry = groupTestHelper.createChildNotification()
-        val newAlertEntry = groupTestHelper.createChildNotification()
-        group.alertOverride = newAlertEntry
-        dispatcher.notifyAlertOverrideChanged(group, oldAlertEntry)
-        verify(listener).onGroupAlertOverrideChanged(group, oldAlertEntry, newAlertEntry)
-        verifyNoMoreInteractions(listener)
-    }
-
-    @Test
-    fun testAlertOverrideChangedBuffered() {
-        dispatcher.openBufferScope()
-        val group = addGroup(1)
-        val oldAlertEntry = groupTestHelper.createChildNotification()
-        val newAlertEntry = groupTestHelper.createChildNotification()
-        group.alertOverride = newAlertEntry
-        dispatcher.notifyAlertOverrideChanged(group, oldAlertEntry)
-        verifyNoMoreInteractions(listener)
-        dispatcher.closeBufferScope()
-        verify(listener).onGroupAlertOverrideChanged(group, oldAlertEntry, newAlertEntry)
-        verifyNoMoreInteractions(listener)
-    }
-
-    @Test
-    fun testAlertOverrideIgnoredIfRemoved() {
-        dispatcher.openBufferScope()
-        val group = addGroup(1)
-        val oldAlertEntry = groupTestHelper.createChildNotification()
-        val newAlertEntry = groupTestHelper.createChildNotification()
-        group.alertOverride = newAlertEntry
-        dispatcher.notifyAlertOverrideChanged(group, oldAlertEntry)
-        verifyNoMoreInteractions(listener)
-        groupMap.clear()
-        dispatcher.closeBufferScope()
-        verifyNoMoreInteractions(listener)
-    }
-
-    @Test
-    fun testAlertOverrideMultipleChangesBuffered() {
-        dispatcher.openBufferScope()
-        val group = addGroup(1)
-        val oldAlertEntry = groupTestHelper.createChildNotification()
-        val newAlertEntry = groupTestHelper.createChildNotification()
-        group.alertOverride = null
-        dispatcher.notifyAlertOverrideChanged(group, oldAlertEntry)
-        group.alertOverride = newAlertEntry
-        dispatcher.notifyAlertOverrideChanged(group, null)
-        verifyNoMoreInteractions(listener)
-        dispatcher.closeBufferScope()
-        verify(listener).onGroupAlertOverrideChanged(group, oldAlertEntry, newAlertEntry)
-        verifyNoMoreInteractions(listener)
-    }
-
-    @Test
-    fun testAlertOverrideTemporaryValueSwallowed() {
-        dispatcher.openBufferScope()
-        val group = addGroup(1)
-        val stableAlertEntry = groupTestHelper.createChildNotification()
-        group.alertOverride = null
-        dispatcher.notifyAlertOverrideChanged(group, stableAlertEntry)
-        group.alertOverride = stableAlertEntry
-        dispatcher.notifyAlertOverrideChanged(group, null)
-        verifyNoMoreInteractions(listener)
-        dispatcher.closeBufferScope()
-        verifyNoMoreInteractions(listener)
-    }
-
-    @Test
-    fun testAlertOverrideTemporaryNullSwallowed() {
-        dispatcher.openBufferScope()
-        val group = addGroup(1)
-        val temporaryAlertEntry = groupTestHelper.createChildNotification()
-        group.alertOverride = temporaryAlertEntry
-        dispatcher.notifyAlertOverrideChanged(group, null)
-        group.alertOverride = null
-        dispatcher.notifyAlertOverrideChanged(group, temporaryAlertEntry)
-        verifyNoMoreInteractions(listener)
-        dispatcher.closeBufferScope()
-        verifyNoMoreInteractions(listener)
-    }
-
-    @Test
-    fun testSuppressOnUnbuffered() {
-        val group = addGroup(1)
-        group.suppressed = true
-        dispatcher.notifySuppressedChanged(group)
-        verify(listener).onGroupSuppressionChanged(group, true)
-        verifyNoMoreInteractions(listener)
-    }
-
-    @Test
-    fun testSuppressOffUnbuffered() {
-        val group = addGroup(1)
-        group.suppressed = false
-        dispatcher.notifySuppressedChanged(group)
-        verify(listener).onGroupSuppressionChanged(group, false)
-        verifyNoMoreInteractions(listener)
-    }
-
-    @Test
-    fun testSuppressOnBuffered() {
-        dispatcher.openBufferScope()
-        val group = addGroup(1)
-        group.suppressed = false
-        dispatcher.notifySuppressedChanged(group)
-        verifyNoMoreInteractions(listener)
-        dispatcher.closeBufferScope()
-        verify(listener).onGroupSuppressionChanged(group, false)
-        verifyNoMoreInteractions(listener)
-    }
-
-    @Test
-    fun testSuppressOnIgnoredIfRemoved() {
-        dispatcher.openBufferScope()
-        val group = addGroup(1)
-        group.suppressed = false
-        dispatcher.notifySuppressedChanged(group)
-        verifyNoMoreInteractions(listener)
-        groupMap.clear()
-        dispatcher.closeBufferScope()
-        verifyNoMoreInteractions(listener)
-    }
-
-    @Test
-    fun testSuppressOnOffBuffered() {
-        dispatcher.openBufferScope()
-        val group = addGroup(1)
-        group.suppressed = true
-        dispatcher.notifySuppressedChanged(group)
-        group.suppressed = false
-        dispatcher.notifySuppressedChanged(group)
-        verifyNoMoreInteractions(listener)
-        dispatcher.closeBufferScope()
-        verifyNoMoreInteractions(listener)
-    }
-
-    @Test
-    fun testSuppressOnOffOnBuffered() {
-        dispatcher.openBufferScope()
-        val group = addGroup(1)
-        group.suppressed = true
-        dispatcher.notifySuppressedChanged(group)
-        group.suppressed = false
-        dispatcher.notifySuppressedChanged(group)
-        group.suppressed = true
-        dispatcher.notifySuppressedChanged(group)
-        verifyNoMoreInteractions(listener)
-        dispatcher.closeBufferScope()
-        verify(listener).onGroupSuppressionChanged(group, true)
-        verifyNoMoreInteractions(listener)
-    }
-
-    private fun addGroup(id: Int): NotificationGroup {
-        val group = NotificationGroup("group:$id")
-        groupMap[group.groupKey] = group
-        return group
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionInconsistencyTrackerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionInconsistencyTrackerTest.kt
new file mode 100644
index 0000000..6eb391a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionInconsistencyTrackerTest.kt
@@ -0,0 +1,215 @@
+/*
+ * 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.notification.collection.notifcollection
+
+import android.service.notification.NotificationListenerService.RankingMap
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper
+class NotifCollectionInconsistencyTrackerTest : SysuiTestCase() {
+    private val logger: NotifCollectionLogger = mock()
+    private val entry1: NotificationEntry = NotificationEntryBuilder().setId(1).build()
+    private val entry2: NotificationEntry = NotificationEntryBuilder().setId(2).build()
+    private val collectionSet = mutableSetOf<String>()
+    private val coalescedSet = mutableSetOf<String>()
+    private lateinit var inconsistencyTracker: NotifCollectionInconsistencyTracker
+
+    private fun mapOfEntries(vararg entries: NotificationEntry): Map<String, NotificationEntry> =
+        entries.associateBy { it.key }
+
+    private fun rankingMapOf(vararg entries: NotificationEntry): RankingMap =
+        RankingMap(entries.map { it.ranking }.toTypedArray())
+
+    @Before
+    fun setUp() {
+        inconsistencyTracker = NotifCollectionInconsistencyTracker(logger)
+        inconsistencyTracker.attach({ collectionSet }, { coalescedSet })
+        collectionSet.clear()
+        coalescedSet.clear()
+    }
+
+    @Test
+    fun maybeLogInconsistentRankings_logsNewlyInconsistentRanking() {
+        val rankingMap = rankingMapOf(entry1)
+        inconsistencyTracker.maybeLogInconsistentRankings(
+            oldKeysWithoutRankings = emptySet(),
+            newEntriesWithoutRankings = mapOfEntries(entry2),
+            rankingMap = rankingMap
+        )
+        verify(logger).logMissingRankings(
+            newlyInconsistentEntries = eq(listOf(entry2)),
+            totalInconsistent = eq(1),
+            rankingMap = eq(rankingMap),
+        )
+        verifyNoMoreInteractions(logger)
+    }
+
+    @Test
+    fun maybeLogInconsistentRankings_doesNotLogAlreadyInconsistentRanking() {
+        inconsistencyTracker.maybeLogInconsistentRankings(
+            oldKeysWithoutRankings = setOf(entry2.key),
+            newEntriesWithoutRankings = mapOfEntries(entry2),
+            rankingMap = rankingMapOf(entry1)
+        )
+        verifyNoMoreInteractions(logger)
+    }
+
+    @Test
+    fun maybeLogInconsistentRankings_logsWhenRankingIsAdded() {
+        inconsistencyTracker.maybeLogInconsistentRankings(
+            oldKeysWithoutRankings = setOf(entry2.key),
+            newEntriesWithoutRankings = mapOfEntries(),
+            rankingMap = rankingMapOf(entry1, entry2)
+        )
+        verify(logger).logRecoveredRankings(
+            newlyConsistentKeys = eq(listOf(entry2.key)),
+            totalInconsistent = eq(0),
+        )
+        verifyNoMoreInteractions(logger)
+    }
+
+    @Test
+    fun maybeLogInconsistentRankings_doesNotLogsWhenEntryIsRemoved() {
+        inconsistencyTracker.maybeLogInconsistentRankings(
+            oldKeysWithoutRankings = setOf(entry2.key),
+            newEntriesWithoutRankings = mapOfEntries(),
+            rankingMap = rankingMapOf(entry1)
+        )
+        verifyNoMoreInteractions(logger)
+    }
+
+    @Test
+    fun maybeLogMissingNotifications_logsNewlyMissingNotifications() {
+        inconsistencyTracker.maybeLogMissingNotifications(
+            oldMissingKeys = setOf("a"),
+            newMissingKeys = setOf("a", "b"),
+        )
+        verify(logger).logMissingNotifications(
+            newlyMissingKeys = eq(listOf("b")),
+            totalMissing = eq(2),
+        )
+        verifyNoMoreInteractions(logger)
+    }
+
+    @Test
+    fun maybeLogMissingNotifications_logsNoLongerMissingNotifications() {
+        inconsistencyTracker.maybeLogMissingNotifications(
+            oldMissingKeys = setOf("a", "b"),
+            newMissingKeys = setOf("a"),
+        )
+        verify(logger).logFoundNotifications(
+            newlyFoundKeys = eq(listOf("b")),
+            totalMissing = eq(1),
+        )
+        verifyNoMoreInteractions(logger)
+    }
+
+    @Test
+    fun maybeLogMissingNotifications_logsBothAtOnce() {
+        inconsistencyTracker.maybeLogMissingNotifications(
+            oldMissingKeys = setOf("a"),
+            newMissingKeys = setOf("b"),
+        )
+        verify(logger).logFoundNotifications(
+            newlyFoundKeys = eq(listOf("a")),
+            totalMissing = eq(1),
+        )
+        verify(logger).logMissingNotifications(
+            newlyMissingKeys = eq(listOf("b")),
+            totalMissing = eq(1),
+        )
+        verifyNoMoreInteractions(logger)
+    }
+
+    @Test
+    fun maybeLogMissingNotifications_logsNothingWhenNoChange() {
+        inconsistencyTracker.maybeLogMissingNotifications(
+            oldMissingKeys = setOf("a"),
+            newMissingKeys = setOf("a"),
+        )
+        verifyNoMoreInteractions(logger)
+    }
+
+    @Test
+    fun logNewMissingNotifications_doesNotLogForConsistentSets() {
+        inconsistencyTracker.logNewMissingNotifications(rankingMapOf())
+        verifyNoMoreInteractions(logger)
+
+        collectionSet.add(entry1.key)
+        inconsistencyTracker.logNewMissingNotifications(rankingMapOf(entry1))
+        verifyNoMoreInteractions(logger)
+
+        coalescedSet.add(entry2.key)
+        inconsistencyTracker.logNewMissingNotifications(rankingMapOf(entry1, entry2))
+        verifyNoMoreInteractions(logger)
+
+        coalescedSet.add(entry1.key)
+        inconsistencyTracker.logNewMissingNotifications(rankingMapOf(entry1, entry2))
+        verifyNoMoreInteractions(logger)
+
+        coalescedSet.remove(entry1.key)
+        collectionSet.remove(entry1.key)
+        inconsistencyTracker.logNewMissingNotifications(rankingMapOf(entry2))
+        verifyNoMoreInteractions(logger)
+
+        coalescedSet.remove(entry2.key)
+        inconsistencyTracker.logNewMissingNotifications(rankingMapOf())
+        verifyNoMoreInteractions(logger)
+    }
+
+    @Test
+    fun logNewMissingNotifications_logsAsExpected() {
+        inconsistencyTracker.logNewMissingNotifications(rankingMapOf())
+        verifyNoMoreInteractions(logger)
+
+        collectionSet.add(entry1.key)
+        inconsistencyTracker.logNewMissingNotifications(rankingMapOf(entry1, entry2))
+        verify(logger).logMissingNotifications(
+            newlyMissingKeys = eq(listOf(entry2.key)),
+            totalMissing = eq(1),
+        )
+        verifyNoMoreInteractions(logger)
+        clearInvocations(logger)
+
+        inconsistencyTracker.logNewMissingNotifications(rankingMapOf(entry1, entry2))
+        verifyNoMoreInteractions(logger)
+
+        coalescedSet.add(entry2.key)
+        inconsistencyTracker.logNewMissingNotifications(rankingMapOf(entry1, entry2))
+        verify(logger).logFoundNotifications(
+            newlyFoundKeys = eq(listOf(entry2.key)),
+            totalMissing = eq(0),
+        )
+        verifyNoMoreInteractions(logger)
+        clearInvocations(logger)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLoggerTest.kt
deleted file mode 100644
index 6c07174..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/notifcollection/NotifCollectionLoggerTest.kt
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * 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.notification.collection.notifcollection
-
-import android.service.notification.NotificationListenerService.RankingMap
-import android.testing.AndroidTestingRunner
-import android.testing.TestableLooper.RunWithLooper
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.statusbar.notification.collection.NotificationEntry
-import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.mock
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyNoMoreInteractions
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-@RunWithLooper
-class NotifCollectionLoggerTest : SysuiTestCase() {
-    private val logger: NotifCollectionLogger = mock()
-    private val entry1: NotificationEntry = NotificationEntryBuilder().setId(1).build()
-    private val entry2: NotificationEntry = NotificationEntryBuilder().setId(2).build()
-
-    private fun mapOfEntries(vararg entries: NotificationEntry): Map<String, NotificationEntry> =
-        entries.associateBy { it.key }
-
-    private fun rankingMapOf(vararg entries: NotificationEntry): RankingMap =
-        RankingMap(entries.map { it.ranking }.toTypedArray())
-
-    @Test
-    fun testMaybeLogInconsistentRankings_logsNewlyInconsistentRanking() {
-        val rankingMap = rankingMapOf(entry1)
-        maybeLogInconsistentRankings(
-            logger = logger,
-            oldKeysWithoutRankings = emptySet(),
-            newEntriesWithoutRankings = mapOfEntries(entry2),
-            rankingMap = rankingMap
-        )
-        verify(logger).logMissingRankings(
-            newlyInconsistentEntries = eq(listOf(entry2)),
-            totalInconsistent = eq(1),
-            rankingMap = eq(rankingMap),
-        )
-        verifyNoMoreInteractions(logger)
-    }
-
-    @Test
-    fun testMaybeLogInconsistentRankings_doesNotLogAlreadyInconsistentRanking() {
-        maybeLogInconsistentRankings(
-            logger = logger,
-            oldKeysWithoutRankings = setOf(entry2.key),
-            newEntriesWithoutRankings = mapOfEntries(entry2),
-            rankingMap = rankingMapOf(entry1)
-        )
-        verifyNoMoreInteractions(logger)
-    }
-
-    @Test
-    fun testMaybeLogInconsistentRankings_logsWhenRankingIsAdded() {
-        maybeLogInconsistentRankings(
-            logger = logger,
-            oldKeysWithoutRankings = setOf(entry2.key),
-            newEntriesWithoutRankings = mapOfEntries(),
-            rankingMap = rankingMapOf(entry1, entry2)
-        )
-        verify(logger).logRecoveredRankings(
-            newlyConsistentKeys = eq(listOf(entry2.key)),
-        )
-        verifyNoMoreInteractions(logger)
-    }
-
-    @Test
-    fun testMaybeLogInconsistentRankings_doesNotLogsWhenEntryIsRemoved() {
-        maybeLogInconsistentRankings(
-            logger = logger,
-            oldKeysWithoutRankings = setOf(entry2.key),
-            newEntriesWithoutRankings = mapOfEntries(),
-            rankingMap = rankingMapOf(entry1)
-        )
-        verifyNoMoreInteractions(logger)
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/people/PeopleFakes.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/people/PeopleFakes.kt
deleted file mode 100644
index 1fb4ca1..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/people/PeopleFakes.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2020 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.notification.people
-
-object EmptySubscription : Subscription {
-    override fun unsubscribe() {}
-}
-
-class FakeDataSource<T>(
-    private val data: T,
-    private val subscription: Subscription = EmptySubscription
-) : DataSource<T> {
-    override fun registerListener(listener: DataListener<T>): Subscription {
-        listener.onDataChanged(data)
-        return subscription
-    }
-}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/people/PeopleHubViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/people/PeopleHubViewControllerTest.kt
deleted file mode 100644
index 5898664..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/people/PeopleHubViewControllerTest.kt
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- * Copyright (C) 2019 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.notification.people
-
-import android.graphics.drawable.Drawable
-import android.testing.AndroidTestingRunner
-import android.view.View
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.plugins.ActivityStarter
-import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Mockito
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.verify
-import org.mockito.junit.MockitoJUnit
-import org.mockito.junit.MockitoRule
-import kotlin.reflect.KClass
-import org.mockito.Mockito.`when` as whenever
-
-@SmallTest
-@RunWith(AndroidTestingRunner::class)
-class PeopleHubViewControllerTest : SysuiTestCase() {
-
-    @JvmField @Rule val mockito: MockitoRule = MockitoJUnit.rule()
-
-    @Mock private lateinit var mockViewBoundary: PeopleHubViewBoundary
-    @Mock private lateinit var mockActivityStarter: ActivityStarter
-
-    @Test
-    fun testBindViewModelToViewBoundary() {
-        val fakePerson1 = fakePersonViewModel("name")
-        val fakeViewModel = PeopleHubViewModel(sequenceOf(fakePerson1), true)
-
-        val mockFactory = mock(PeopleHubViewModelFactory::class.java)
-        whenever(mockFactory.createWithAssociatedClickView(any())).thenReturn(fakeViewModel)
-
-        val mockClickView = mock(View::class.java)
-        whenever(mockViewBoundary.associatedViewForClickAnimation).thenReturn(mockClickView)
-
-        val fakePersonViewAdapter1 = FakeDataListener<PersonViewModel?>()
-        val fakePersonViewAdapter2 = FakeDataListener<PersonViewModel?>()
-        whenever(mockViewBoundary.personViewAdapters)
-                .thenReturn(sequenceOf(fakePersonViewAdapter1, fakePersonViewAdapter2))
-
-        val adapter = PeopleHubViewAdapterImpl(FakeDataSource(mockFactory))
-
-        adapter.bindView(mockViewBoundary)
-
-        assertThat(fakePersonViewAdapter1.lastSeen).isEqualTo(Maybe.Just(fakePerson1))
-        assertThat(fakePersonViewAdapter2.lastSeen).isEqualTo(Maybe.Just<PersonViewModel?>(null))
-        verify(mockViewBoundary).setVisible(true)
-        verify(mockFactory).createWithAssociatedClickView(mockClickView)
-    }
-
-    @Test
-    fun testBindViewModelToViewBoundary_moreDataThanCanBeDisplayed_displaysMostRecent() {
-        val fakePerson1 = fakePersonViewModel("person1")
-        val fakePerson2 = fakePersonViewModel("person2")
-        val fakePerson3 = fakePersonViewModel("person3")
-        val fakePeople = sequenceOf(fakePerson3, fakePerson2, fakePerson1)
-        val fakeViewModel = PeopleHubViewModel(fakePeople, true)
-
-        val mockFactory = mock(PeopleHubViewModelFactory::class.java)
-        whenever(mockFactory.createWithAssociatedClickView(any())).thenReturn(fakeViewModel)
-
-        whenever(mockViewBoundary.associatedViewForClickAnimation)
-                .thenReturn(mock(View::class.java))
-
-        val fakePersonViewAdapter1 = FakeDataListener<PersonViewModel?>()
-        val fakePersonViewAdapter2 = FakeDataListener<PersonViewModel?>()
-        whenever(mockViewBoundary.personViewAdapters)
-                .thenReturn(sequenceOf(fakePersonViewAdapter1, fakePersonViewAdapter2))
-
-        val adapter = PeopleHubViewAdapterImpl(FakeDataSource(mockFactory))
-
-        adapter.bindView(mockViewBoundary)
-
-        assertThat(fakePersonViewAdapter1.lastSeen).isEqualTo(Maybe.Just(fakePerson3))
-        assertThat(fakePersonViewAdapter2.lastSeen).isEqualTo(Maybe.Just(fakePerson2))
-    }
-
-    @Test
-    fun testViewModelDataSourceTransformsModel() {
-        val fakeClickRunnable = mock(Runnable::class.java)
-        val fakePerson = fakePersonModel("id", "name", fakeClickRunnable)
-        val fakeModel = PeopleHubModel(listOf(fakePerson))
-        val fakeModelDataSource = FakeDataSource(fakeModel)
-        val factoryDataSource = PeopleHubViewModelFactoryDataSourceImpl(
-                mockActivityStarter,
-                fakeModelDataSource
-        )
-        val fakeListener = FakeDataListener<PeopleHubViewModelFactory>()
-        val mockClickView = mock(View::class.java)
-
-        factoryDataSource.registerListener(fakeListener)
-
-        val viewModel = (fakeListener.lastSeen as Maybe.Just).value
-                .createWithAssociatedClickView(mockClickView)
-        assertThat(viewModel.isVisible).isTrue()
-        val people = viewModel.people.toList()
-        assertThat(people.size).isEqualTo(1)
-        assertThat(people[0].name).isEqualTo("name")
-        assertThat(people[0].icon).isSameInstanceAs(fakePerson.avatar)
-
-        people[0].onClick()
-
-        verify(fakeClickRunnable).run()
-    }
-}
-
-/** Works around Mockito matchers returning `null` and breaking non-nullable Kotlin code. */
-private inline fun <reified T : Any> any(): T {
-    return Mockito.any() ?: createInstance(T::class)
-}
-
-/** Works around Mockito matchers returning `null` and breaking non-nullable Kotlin code. */
-private inline fun <reified T : Any> same(value: T): T {
-    return Mockito.same(value) ?: createInstance(T::class)
-}
-
-/** Creates an instance of the given class. */
-private fun <T : Any> createInstance(clazz: KClass<T>): T = castNull()
-
-/** Tricks the Kotlin compiler into assigning `null` to a non-nullable variable. */
-@Suppress("UNCHECKED_CAST")
-private fun <T> castNull(): T = null as T
-
-private fun fakePersonModel(
-    id: String,
-    name: CharSequence,
-    clickRunnable: Runnable,
-    userId: Int = 0
-): PersonModel =
-        PersonModel(id, userId, name, mock(Drawable::class.java), clickRunnable)
-
-private fun fakePersonViewModel(name: CharSequence): PersonViewModel =
-        PersonViewModel(name, mock(Drawable::class.java), mock({}.javaClass))
-
-sealed class Maybe<T> {
-    data class Just<T>(val value: T) : Maybe<T>()
-    class Nothing<T> : Maybe<T>() {
-        override fun equals(other: Any?): Boolean {
-            if (this === other) return true
-            if (javaClass != other?.javaClass) return false
-            return true
-        }
-
-        override fun hashCode(): Int {
-            return javaClass.hashCode()
-        }
-    }
-}
-
-class FakeDataListener<T> : DataListener<T> {
-
-    var lastSeen: Maybe<T> = Maybe.Nothing()
-
-    override fun onDataChanged(data: T) {
-        lastSeen = Maybe.Just(data)
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
index 1460e04..966e233 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
@@ -34,7 +34,6 @@
 import android.content.res.Resources;
 import android.metrics.LogMaker;
 import android.testing.AndroidTestingRunner;
-import android.view.LayoutInflater;
 
 import androidx.test.filters.SmallTest;
 
@@ -42,11 +41,9 @@
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.logging.nano.MetricsProto;
-import com.android.internal.statusbar.IStatusBarService;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.classifier.FalsingCollectorFake;
 import com.android.systemui.classifier.FalsingManagerFake;
-import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.media.KeyguardMediaController;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
@@ -61,12 +58,9 @@
 import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
-import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.notification.collection.NotifCollection;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
-import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy;
-import com.android.systemui.statusbar.notification.collection.legacy.VisualStabilityManager;
 import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager;
 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
 import com.android.systemui.statusbar.notification.collection.render.SectionHeaderController;
@@ -112,7 +106,6 @@
     @Mock private KeyguardMediaController mKeyguardMediaController;
     @Mock private SysuiStatusBarStateController mSysuiStatusBarStateController;
     @Mock private KeyguardBypassController mKeyguardBypassController;
-    @Mock private SysuiColorExtractor mColorExtractor;
     @Mock private NotificationLockscreenUserManager mNotificationLockscreenUserManager;
     @Mock private MetricsLogger mMetricsLogger;
     @Mock private DumpManager mDumpManager;
@@ -122,19 +115,14 @@
     @Mock private NotificationSwipeHelper mNotificationSwipeHelper;
     @Mock private CentralSurfaces mCentralSurfaces;
     @Mock private ScrimController mScrimController;
-    @Mock private NotificationGroupManagerLegacy mGroupManagerLegacy;
     @Mock private GroupExpansionManager mGroupExpansionManager;
     @Mock private SectionHeaderController mSilentHeaderController;
-    @Mock private NotifPipelineFlags mNotifPipelineFlags;
     @Mock private NotifPipeline mNotifPipeline;
     @Mock private NotifCollection mNotifCollection;
     @Mock private NotificationEntryManager mEntryManager;
-    @Mock private IStatusBarService mIStatusBarService;
     @Mock private UiEventLogger mUiEventLogger;
     @Mock private LockscreenShadeTransitionController mLockscreenShadeTransitionController;
-    @Mock private LayoutInflater mLayoutInflater;
     @Mock private NotificationRemoteInputManager mRemoteInputManager;
-    @Mock private VisualStabilityManager mVisualStabilityManager;
     @Mock private ShadeController mShadeController;
     @Mock private InteractionJankMonitor mJankMonitor;
     @Mock private StackStateLogger mStackLogger;
@@ -176,7 +164,6 @@
                 mNotificationSwipeHelperBuilder,
                 mCentralSurfaces,
                 mScrimController,
-                mGroupManagerLegacy,
                 mGroupExpansionManager,
                 mSilentHeaderController,
                 mNotifPipeline,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
index d2680eb5..a6808e0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
@@ -22,6 +22,8 @@
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
@@ -395,15 +397,19 @@
     }
 
     @Test
-    public void onUdfpsConsecutivelyFailedTwoTimes_showBouncer() {
+    public void onUdfpsConsecutivelyFailedThreeTimes_showBouncer() {
         // GIVEN UDFPS is supported
         when(mUpdateMonitor.isUdfpsSupported()).thenReturn(true);
 
-        // WHEN udfps fails once - then don't show the bouncer
+        // WHEN udfps fails once - then don't show the bouncer yet
         mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT);
         verify(mStatusBarKeyguardViewManager, never()).showBouncer(anyBoolean());
 
-        // WHEN udfps fails the second time
+        // WHEN udfps fails the second time - then don't show the bouncer yet
+        mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT);
+        verify(mStatusBarKeyguardViewManager, never()).showBouncer(anyBoolean());
+
+        // WHEN udpfs fails the third time
         mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT);
 
         // THEN show the bouncer
@@ -427,8 +433,8 @@
     }
 
     @Test
-    public void onFPFailureNoHaptics_notDeviceInteractive_showBouncer() {
-        // GIVEN no vibrator and the screen is off
+    public void onFPFailureNoHaptics_notInteractive_showLockScreen() {
+        // GIVEN no vibrator and device is dreaming
         when(mVibratorHelper.hasVibrator()).thenReturn(false);
         when(mUpdateMonitor.isDeviceInteractive()).thenReturn(false);
         when(mUpdateMonitor.isDreaming()).thenReturn(false);
@@ -436,15 +442,12 @@
         // WHEN FP fails
         mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT);
 
-        // after device is finished waking up
-        mBiometricUnlockController.mWakefulnessObserver.onFinishedWakingUp();
-
-        // THEN show the bouncer
-        verify(mStatusBarKeyguardViewManager).showBouncer(true);
+        // THEN wakeup the device
+        verify(mPowerManager).wakeUp(anyLong(), anyInt(), anyString());
     }
 
     @Test
-    public void onFPFailureNoHaptics_dreaming_showBouncer() {
+    public void onFPFailureNoHaptics_dreaming_showLockScreen() {
         // GIVEN no vibrator and device is dreaming
         when(mVibratorHelper.hasVibrator()).thenReturn(false);
         when(mUpdateMonitor.isDeviceInteractive()).thenReturn(true);
@@ -453,7 +456,7 @@
         // WHEN FP fails
         mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT);
 
-        // THEN show the bouncer
-        verify(mStatusBarKeyguardViewManager).showBouncer(true);
+        // THEN wakeup the device
+        verify(mPowerManager).wakeUp(anyLong(), anyInt(), anyString());
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index a0f7087..735a88d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -133,7 +133,6 @@
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
-import com.android.systemui.statusbar.notification.collection.legacy.VisualStabilityManager;
 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
 import com.android.systemui.statusbar.notification.init.NotificationsController;
 import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider;
@@ -209,7 +208,6 @@
     @Mock private DozeScrimController mDozeScrimController;
     @Mock private Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy;
     @Mock private BiometricUnlockController mBiometricUnlockController;
-    @Mock private VisualStabilityManager mVisualStabilityManager;
     @Mock private NotificationListener mNotificationListener;
     @Mock private KeyguardViewMediator mKeyguardViewMediator;
     @Mock private NotificationLockscreenUserManager mLockscreenUserManager;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt
new file mode 100644
index 0000000..3d29d2b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/model/WifiNetworkModelTest.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.data.model
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel.Active.Companion.MAX_VALID_LEVEL
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel.Active.Companion.MIN_VALID_LEVEL
+import org.junit.Test
+
+@SmallTest
+class WifiNetworkModelTest : SysuiTestCase() {
+    @Test
+    fun active_levelsInValidRange_noException() {
+        (MIN_VALID_LEVEL..MAX_VALID_LEVEL).forEach { level ->
+            WifiNetworkModel.Active(NETWORK_ID, level = level)
+            // No assert, just need no crash
+        }
+    }
+
+    @Test
+    fun active_levelNull_noException() {
+        WifiNetworkModel.Active(NETWORK_ID, level = null)
+        // No assert, just need no crash
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun active_levelNegative_exceptionThrown() {
+        WifiNetworkModel.Active(NETWORK_ID, level = MIN_VALID_LEVEL - 1)
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun active_levelTooHigh_exceptionThrown() {
+        WifiNetworkModel.Active(NETWORK_ID, level = MAX_VALID_LEVEL + 1)
+    }
+
+    companion object {
+        private const val NETWORK_ID = 2
+    }
+}
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 d0a3808..9829271 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
@@ -19,6 +19,7 @@
 import android.net.ConnectivityManager
 import android.net.Network
 import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
 import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
 import android.net.NetworkCapabilities.TRANSPORT_WIFI
 import android.net.vcn.VcnTransportInfo
@@ -39,11 +40,14 @@
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import java.util.concurrent.Executor
+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 org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.mockito.Mock
@@ -61,20 +65,28 @@
     @Mock private lateinit var connectivityManager: ConnectivityManager
     @Mock private lateinit var wifiManager: WifiManager
     private lateinit var executor: Executor
+    private lateinit var scope: CoroutineScope
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
         executor = FakeExecutor(FakeSystemClock())
+        scope = CoroutineScope(IMMEDIATE)
 
         underTest = WifiRepositoryImpl(
             connectivityManager,
-            wifiManager,
-            executor,
             logger,
+            executor,
+            scope,
+            wifiManager,
         )
     }
 
+    @After
+    fun tearDown() {
+        scope.cancel()
+    }
+
     @Test
     fun wifiNetwork_initiallyGetsDefault() = runBlocking(IMMEDIATE) {
         var latest: WifiNetworkModel? = null
@@ -115,6 +127,61 @@
     }
 
     @Test
+    fun wifiNetwork_isCarrierMerged_flowHasCarrierMerged() = runBlocking(IMMEDIATE) {
+        var latest: WifiNetworkModel? = null
+        val job = underTest
+            .wifiNetwork
+            .onEach { latest = it }
+            .launchIn(this)
+
+        val wifiInfo = mock<WifiInfo>().apply {
+            whenever(this.ssid).thenReturn(SSID)
+            whenever(this.isPrimary).thenReturn(true)
+            whenever(this.isCarrierMerged).thenReturn(true)
+        }
+
+        getNetworkCallback().onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo))
+
+        assertThat(latest is WifiNetworkModel.CarrierMerged).isTrue()
+
+        job.cancel()
+    }
+
+    @Test
+    fun wifiNetwork_notValidated_networkNotValidated() = runBlocking(IMMEDIATE) {
+        var latest: WifiNetworkModel? = null
+        val job = underTest
+            .wifiNetwork
+            .onEach { latest = it }
+            .launchIn(this)
+
+        getNetworkCallback().onCapabilitiesChanged(
+            NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO, isValidated = false)
+        )
+
+        assertThat((latest as WifiNetworkModel.Active).isValidated).isFalse()
+
+        job.cancel()
+    }
+
+    @Test
+    fun wifiNetwork_validated_networkValidated() = runBlocking(IMMEDIATE) {
+        var latest: WifiNetworkModel? = null
+        val job = underTest
+            .wifiNetwork
+            .onEach { latest = it }
+            .launchIn(this)
+
+        getNetworkCallback().onCapabilitiesChanged(
+            NETWORK, createWifiNetworkCapabilities(PRIMARY_WIFI_INFO, isValidated = true)
+        )
+
+        assertThat((latest as WifiNetworkModel.Active).isValidated).isTrue()
+
+        job.cancel()
+    }
+
+    @Test
     fun wifiNetwork_nonPrimaryWifiNetworkAdded_flowHasNoNetwork() = runBlocking(IMMEDIATE) {
         var latest: WifiNetworkModel? = null
         val job = underTest
@@ -282,13 +349,14 @@
             .launchIn(this)
 
         val wifiInfo = mock<WifiInfo>().apply {
-        whenever(this.ssid).thenReturn(SSID)
-        whenever(this.isPrimary).thenReturn(true)
-    }
+            whenever(this.ssid).thenReturn(SSID)
+            whenever(this.isPrimary).thenReturn(true)
+        }
 
         // Start with the original network
-        getNetworkCallback()
-            .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(wifiInfo))
+        getNetworkCallback().onCapabilitiesChanged(
+            NETWORK, createWifiNetworkCapabilities(wifiInfo, isValidated = true)
+        )
 
         // WHEN we keep the same network ID but change the SSID
         val newSsid = "CD"
@@ -297,14 +365,16 @@
             whenever(this.isPrimary).thenReturn(true)
         }
 
-        getNetworkCallback()
-            .onCapabilitiesChanged(NETWORK, createWifiNetworkCapabilities(newWifiInfo))
+        getNetworkCallback().onCapabilitiesChanged(
+            NETWORK, createWifiNetworkCapabilities(newWifiInfo, isValidated = false)
+        )
 
         // THEN we've updated to the new SSID
         assertThat(latest is WifiNetworkModel.Active).isTrue()
         val latestActive = latest as WifiNetworkModel.Active
         assertThat(latestActive.networkId).isEqualTo(NETWORK_ID)
         assertThat(latestActive.ssid).isEqualTo(newSsid)
+        assertThat(latestActive.isValidated).isFalse()
 
         job.cancel()
     }
@@ -406,10 +476,11 @@
     @Test
     fun wifiActivity_nullWifiManager_receivesDefault() = runBlocking(IMMEDIATE) {
         underTest = WifiRepositoryImpl(
-                connectivityManager,
-                wifiManager = null,
-                executor,
-                logger,
+            connectivityManager,
+            logger,
+            executor,
+            scope,
+            wifiManager = null,
         )
 
         var latest: WifiActivityModel? = null
@@ -501,11 +572,16 @@
         return callbackCaptor.value!!
     }
 
-    private fun createWifiNetworkCapabilities(wifiInfo: WifiInfo) =
-        mock<NetworkCapabilities>().apply {
-            whenever(this.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
-            whenever(this.transportInfo).thenReturn(wifiInfo)
+    private fun createWifiNetworkCapabilities(
+        wifiInfo: WifiInfo,
+        isValidated: Boolean = true,
+    ): NetworkCapabilities {
+        return mock<NetworkCapabilities>().also {
+            whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
+            whenever(it.transportInfo).thenReturn(wifiInfo)
+            whenever(it.hasCapability(NET_CAPABILITY_VALIDATED)).thenReturn(isValidated)
         }
+    }
 
     private companion object {
         const val NETWORK_ID = 45
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 5f1b1db..9d8b4bc 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
@@ -126,6 +126,38 @@
     }
 
     @Test
+    fun hasActivityIn_inactiveNetwork_outputsFalse() = runBlocking(IMMEDIATE) {
+        repository.setWifiNetwork(WifiNetworkModel.Inactive)
+        repository.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_carrierMergedNetwork_outputsFalse() = runBlocking(IMMEDIATE) {
+        repository.setWifiNetwork(WifiNetworkModel.CarrierMerged)
+        repository.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_multipleChanges_multipleOutputChanges() = runBlocking(IMMEDIATE) {
         repository.setWifiNetwork(VALID_WIFI_NETWORK_MODEL)
 
@@ -159,6 +191,28 @@
         job.cancel()
     }
 
+    @Test
+    fun wifiNetwork_matchesRepoWifiNetwork() = runBlocking(IMMEDIATE) {
+        val wifiNetwork = WifiNetworkModel.Active(
+            networkId = 45,
+            isValidated = true,
+            level = 3,
+            ssid = "AB",
+            passpointProviderFriendlyName = "friendly"
+        )
+        repository.setWifiNetwork(wifiNetwork)
+
+        var latest: WifiNetworkModel? = null
+        val job = underTest
+            .wifiNetwork
+            .onEach { latest = it }
+            .launchIn(this)
+
+        assertThat(latest).isEqualTo(wifiNetwork)
+
+        job.cancel()
+    }
+
     companion object {
         val VALID_WIFI_NETWORK_MODEL = WifiNetworkModel.Active(networkId = 1, ssid = "AB")
     }
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 c790734..f0a775b 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
@@ -18,6 +18,9 @@
 
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+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
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiActivityModel
@@ -62,14 +65,103 @@
             logger,
             interactor
         )
-
-        // Set up with a valid SSID
-        repository.setWifiNetwork(WifiNetworkModel.Active(networkId = 1, ssid = "AB"))
     }
 
     @Test
-    fun activityInVisible_showActivityConfigFalse_receivesFalse() = runBlocking(IMMEDIATE) {
+    fun wifiIconResId_inactiveNetwork_outputsNoNetworkIcon() = runBlocking(IMMEDIATE) {
+        repository.setWifiNetwork(WifiNetworkModel.Inactive)
+
+        var latest: Int? = null
+        val job = underTest
+                .wifiIconResId
+                .onEach { latest = it }
+                .launchIn(this)
+
+        assertThat(latest).isEqualTo(WIFI_NO_NETWORK)
+
+        job.cancel()
+    }
+
+    @Test
+    fun wifiIconResId_carrierMergedNetwork_outputsNull() = runBlocking(IMMEDIATE) {
+        repository.setWifiNetwork(WifiNetworkModel.CarrierMerged)
+
+        var latest: Int? = null
+        val job = underTest
+                .wifiIconResId
+                .onEach { latest = it }
+                .launchIn(this)
+
+        assertThat(latest).isNull()
+
+        job.cancel()
+    }
+
+    @Test
+    fun wifiIconResId_isActiveNullLevel_outputsNull() = runBlocking(IMMEDIATE) {
+        repository.setWifiNetwork(WifiNetworkModel.Active(NETWORK_ID, level = null))
+
+        var latest: Int? = null
+        val job = underTest
+            .wifiIconResId
+            .onEach { latest = it }
+            .launchIn(this)
+
+        assertThat(latest).isNull()
+
+        job.cancel()
+    }
+
+    @Test
+    fun wifiIconResId_isActiveAndValidated_level1_outputsFull1Icon() = runBlocking(IMMEDIATE) {
+        val level = 1
+
+        repository.setWifiNetwork(
+                WifiNetworkModel.Active(
+                        NETWORK_ID,
+                        isValidated = true,
+                        level = level
+                )
+        )
+
+        var latest: Int? = null
+        val job = underTest
+                .wifiIconResId
+                .onEach { latest = it }
+                .launchIn(this)
+
+        assertThat(latest).isEqualTo(WIFI_FULL_ICONS[level])
+
+        job.cancel()
+    }
+
+    @Test
+    fun wifiIconResId_isActiveAndNotValidated_level4_outputsEmpty4Icon() = runBlocking(IMMEDIATE) {
+        val level = 4
+
+        repository.setWifiNetwork(
+                WifiNetworkModel.Active(
+                        NETWORK_ID,
+                        isValidated = false,
+                        level = level
+                )
+        )
+
+        var latest: Int? = null
+        val job = underTest
+                .wifiIconResId
+                .onEach { latest = it }
+                .launchIn(this)
+
+        assertThat(latest).isEqualTo(WIFI_NO_INTERNET_ICONS[level])
+
+        job.cancel()
+    }
+
+    @Test
+    fun activityInVisible_showActivityConfigFalse_outputsFalse() = runBlocking(IMMEDIATE) {
         whenever(constants.shouldShowActivityConfig).thenReturn(false)
+        repository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
 
         var latest: Boolean? = null
         val job = underTest
@@ -86,6 +178,7 @@
     @Test
     fun activityInVisible_showActivityConfigFalse_noUpdatesReceived() = runBlocking(IMMEDIATE) {
         whenever(constants.shouldShowActivityConfig).thenReturn(false)
+        repository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
 
         var latest: Boolean? = null
         val job = underTest
@@ -104,8 +197,9 @@
     }
 
     @Test
-    fun activityInVisible_showActivityConfigTrue_receivesUpdate() = runBlocking(IMMEDIATE) {
+    fun activityInVisible_showActivityConfigTrue_outputsUpdate() = runBlocking(IMMEDIATE) {
         whenever(constants.shouldShowActivityConfig).thenReturn(true)
+        repository.setWifiNetwork(ACTIVE_VALID_WIFI_NETWORK)
 
         var latest: Boolean? = null
         val job = underTest
@@ -122,6 +216,11 @@
 
         job.cancel()
     }
+
+    companion object {
+        private const val NETWORK_ID = 2
+        private val ACTIVE_VALID_WIFI_NETWORK = WifiNetworkModel.Active(NETWORK_ID, ssid = "AB")
+    }
 }
 
 private val IMMEDIATE = Dispatchers.Main.immediate
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/FlowUtilTests.kt b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/FlowUtilTests.kt
new file mode 100644
index 0000000..092e82c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/kotlin/FlowUtilTests.kt
@@ -0,0 +1,141 @@
+/*
+ * 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.util.kotlin
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asFlow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.filterIsInstance
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.takeWhile
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class PairwiseFlowTest : SysuiTestCase() {
+    @Test
+    fun simple() = runBlocking {
+        assertThatFlow((1..3).asFlow().pairwise())
+            .emitsExactly(
+                WithPrev(1, 2),
+                WithPrev(2, 3),
+            )
+    }
+
+    @Test
+    fun notEnough() = runBlocking {
+        assertThatFlow(flowOf(1).pairwise()).emitsNothing()
+    }
+
+    @Test
+    fun withInit() = runBlocking {
+        assertThatFlow(flowOf(2).pairwise(initialValue = 1))
+            .emitsExactly(WithPrev(1, 2))
+    }
+
+    @Test
+    fun notEnoughWithInit() = runBlocking {
+        assertThatFlow(emptyFlow<Int>().pairwise(initialValue = 1)).emitsNothing()
+    }
+
+    @Test
+    fun withStateFlow() = runBlocking(Dispatchers.Main.immediate) {
+        val state = MutableStateFlow(1)
+        val stop = MutableSharedFlow<Unit>()
+
+        val stoppable = merge(state, stop)
+            .takeWhile { it is Int }
+            .filterIsInstance<Int>()
+
+        val job1 = launch {
+            assertThatFlow(stoppable.pairwise()).emitsExactly(WithPrev(1, 2))
+        }
+        state.value = 2
+        val job2 = launch { assertThatFlow(stoppable.pairwise()).emitsNothing() }
+
+        stop.emit(Unit)
+
+        assertThatJob(job1).isCompleted()
+        assertThatJob(job2).isCompleted()
+    }
+}
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class SetChangesFlowTest : SysuiTestCase() {
+    @Test
+    fun simple() = runBlocking {
+        assertThatFlow(
+            flowOf(setOf(1, 2, 3), setOf(2, 3, 4)).setChanges()
+        ).emitsExactly(
+            SetChanges(
+                added = setOf(1, 2, 3),
+                removed = emptySet(),
+            ),
+            SetChanges(
+                added = setOf(4),
+                removed = setOf(1),
+            ),
+        )
+    }
+
+    @Test
+    fun onlyOneEmission() = runBlocking {
+        assertThatFlow(flowOf(setOf(1)).setChanges())
+            .emitsExactly(
+                SetChanges(
+                    added = setOf(1),
+                    removed = emptySet(),
+                )
+            )
+    }
+
+    @Test
+    fun fromEmptySet() = runBlocking {
+        assertThatFlow(flowOf(emptySet(), setOf(1, 2)).setChanges())
+            .emitsExactly(
+                SetChanges(
+                    removed = emptySet(),
+                    added = setOf(1, 2),
+                )
+            )
+    }
+}
+
+private fun <T> assertThatFlow(flow: Flow<T>) = object {
+    suspend fun emitsExactly(vararg emissions: T) =
+        assertThat(flow.toList()).containsExactly(*emissions).inOrder()
+    suspend fun emitsNothing() =
+        assertThat(flow.toList()).isEmpty()
+}
+
+private fun assertThatJob(job: Job) = object {
+    fun isCompleted() = assertThat(job.isCompleted).isTrue()
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
index 312db2d..2e74bf5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -85,6 +85,8 @@
     @Mock
     MediaOutputDialogFactory mMediaOutputDialogFactory;
     @Mock
+    VolumePanelFactory mVolumePanelFactory;
+    @Mock
     ActivityStarter mActivityStarter;
     @Mock
     InteractionJankMonitor mInteractionJankMonitor;
@@ -102,6 +104,7 @@
                 mDeviceProvisionedController,
                 mConfigurationController,
                 mMediaOutputDialogFactory,
+                mVolumePanelFactory,
                 mActivityStarter,
                 mInteractionJankMonitor);
         mDialog.init(0, null);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 5d63632..09da52e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -99,7 +99,6 @@
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
-import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy;
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
@@ -162,8 +161,6 @@
     @Mock
     private CommonNotifCollection mCommonNotifCollection;
     @Mock
-    private NotificationGroupManagerLegacy mNotificationGroupManager;
-    @Mock
     private BubblesManager.NotifCallback mNotifCallback;
     @Mock
     private WindowManager mWindowManager;
@@ -389,7 +386,6 @@
                 interruptionStateProvider,
                 mZenModeController,
                 mLockscreenUserManager,
-                mNotificationGroupManager,
                 mCommonNotifCollection,
                 mNotifPipeline,
                 mSysUiState,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt
index b53ad0a..c56fdb1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FakeFeatureFlags.kt
@@ -16,14 +16,12 @@
 
 package com.android.systemui.flags
 
-import android.util.SparseArray
-import android.util.SparseBooleanArray
-import androidx.core.util.containsKey
-
 class FakeFeatureFlags : FeatureFlags {
-    private val booleanFlags = SparseBooleanArray()
-    private val stringFlags = SparseArray<String>()
+    private val booleanFlags = mutableMapOf<Int, Boolean>()
+    private val stringFlags = mutableMapOf<Int, String>()
     private val knownFlagNames = mutableMapOf<Int, String>()
+    private val flagListeners = mutableMapOf<Int, MutableSet<FlagListenable.Listener>>()
+    private val listenerFlagIds = mutableMapOf<FlagListenable.Listener, MutableSet<Int>>()
 
     init {
         Flags.getFlagFields().forEach { field ->
@@ -33,27 +31,52 @@
     }
 
     fun set(flag: BooleanFlag, value: Boolean) {
-        booleanFlags.put(flag.id, value)
+        if (booleanFlags.put(flag.id, value)?.let { value != it } != false) {
+            notifyFlagChanged(flag)
+        }
     }
 
     fun set(flag: DeviceConfigBooleanFlag, value: Boolean) {
-        booleanFlags.put(flag.id, value)
+        if (booleanFlags.put(flag.id, value)?.let { value != it } != false) {
+            notifyFlagChanged(flag)
+        }
     }
 
     fun set(flag: ResourceBooleanFlag, value: Boolean) {
-        booleanFlags.put(flag.id, value)
+        if (booleanFlags.put(flag.id, value)?.let { value != it } != false) {
+            notifyFlagChanged(flag)
+        }
     }
 
     fun set(flag: SysPropBooleanFlag, value: Boolean) {
-        booleanFlags.put(flag.id, value)
+        if (booleanFlags.put(flag.id, value)?.let { value != it } != false) {
+            notifyFlagChanged(flag)
+        }
     }
 
     fun set(flag: StringFlag, value: String) {
-        stringFlags.put(flag.id, value)
+        if (stringFlags.put(flag.id, value)?.let { value != it } == null) {
+            notifyFlagChanged(flag)
+        }
     }
 
     fun set(flag: ResourceStringFlag, value: String) {
-        stringFlags.put(flag.id, value)
+        if (stringFlags.put(flag.id, value)?.let { value != it } == null) {
+            notifyFlagChanged(flag)
+        }
+    }
+
+    private fun notifyFlagChanged(flag: Flag<*>) {
+        flagListeners[flag.id]?.let { listeners ->
+            listeners.forEach { listener ->
+                listener.onFlagChanged(
+                    object : FlagListenable.FlagEvent {
+                        override val flagId = flag.id
+                        override fun requestNoRestart() {}
+                    }
+                )
+            }
+        }
     }
 
     override fun isEnabled(flag: UnreleasedFlag): Boolean = requireBooleanValue(flag.id)
@@ -70,25 +93,30 @@
 
     override fun getString(flag: ResourceStringFlag): String = requireStringValue(flag.id)
 
-    override fun addListener(flag: Flag<*>, listener: FlagListenable.Listener) {}
+    override fun addListener(flag: Flag<*>, listener: FlagListenable.Listener) {
+        flagListeners.getOrPut(flag.id) { mutableSetOf() }.add(listener)
+        listenerFlagIds.getOrPut(listener) { mutableSetOf() }.add(flag.id)
+    }
 
-    override fun removeListener(listener: FlagListenable.Listener) {}
+    override fun removeListener(listener: FlagListenable.Listener) {
+        listenerFlagIds.remove(listener)?.let {
+                flagIds -> flagIds.forEach {
+                        id -> flagListeners[id]?.remove(listener)
+                }
+        }
+    }
 
     private fun flagName(flagId: Int): String {
         return knownFlagNames[flagId] ?: "UNKNOWN(id=$flagId)"
     }
 
     private fun requireBooleanValue(flagId: Int): Boolean {
-        if (!booleanFlags.containsKey(flagId)) {
-            throw IllegalStateException("Flag ${flagName(flagId)} was accessed but not specified.")
-        }
         return booleanFlags[flagId]
+            ?: error("Flag ${flagName(flagId)} was accessed but not specified.")
     }
 
     private fun requireStringValue(flagId: Int): String {
-        if (!stringFlags.containsKey(flagId)) {
-            throw IllegalStateException("Flag ${flagName(flagId)} was accessed but not specified.")
-        }
         return stringFlags[flagId]
+            ?: error("Flag ${flagName(flagId)} was accessed but not specified.")
     }
 }
diff --git a/services/core/java/com/android/server/biometrics/log/ALSProbe.java b/services/core/java/com/android/server/biometrics/log/ALSProbe.java
new file mode 100644
index 0000000..62f94ed
--- /dev/null
+++ b/services/core/java/com/android/server/biometrics/log/ALSProbe.java
@@ -0,0 +1,163 @@
+/*
+ * 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.server.biometrics.log;
+
+import android.annotation.DurationMillisLong;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.biometrics.sensors.BaseClientMonitor;
+
+import java.util.concurrent.TimeUnit;
+
+/** Probe for ambient light. */
+final class ALSProbe implements Probe {
+    private static final String TAG = "ALSProbe";
+
+    @Nullable
+    private final SensorManager mSensorManager;
+    @Nullable
+    private final Sensor mLightSensor;
+    @NonNull
+    private final Handler mTimer;
+    @DurationMillisLong
+    private long mMaxSubscriptionTime = -1;
+
+    private boolean mEnabled = false;
+    private boolean mDestroyed = false;
+    private volatile float mLastAmbientLux = -1;
+
+    private final SensorEventListener mLightSensorListener = new SensorEventListener() {
+        @Override
+        public void onSensorChanged(SensorEvent event) {
+            mLastAmbientLux = event.values[0];
+        }
+
+        @Override
+        public void onAccuracyChanged(Sensor sensor, int accuracy) {
+            // Not used.
+        }
+    };
+
+    /**
+     * Create a probe with a 1-minute max sampling time.
+     *
+     * @param sensorManager Sensor manager
+     */
+    ALSProbe(@NonNull SensorManager sensorManager) {
+        this(sensorManager, new Handler(Looper.getMainLooper()),
+                TimeUnit.MINUTES.toMillis(1));
+    }
+
+    /**
+     * Create a probe with a given max sampling time.
+     *
+     * Note: The max time is a workaround for potential scheduler bugs where
+     * {@link BaseClientMonitor#destroy()} is not called due to an abnormal lifecycle. Clients
+     * should ensure that {@link #disable()} and {@link #destroy()} are called appropriately and
+     * avoid relying on this timeout to unsubscribe from the sensor when it is not needed.
+     *
+     * @param sensorManager Sensor manager
+     * @param handler Timeout handler
+     * @param maxTime The max amount of time to subscribe to events. If this time is exceeded
+     *                {@link #disable()} will be called and no sampling will occur until {@link
+     *                #enable()} is called again.
+     */
+    @VisibleForTesting
+    ALSProbe(@Nullable SensorManager sensorManager, @NonNull Handler handler,
+            @DurationMillisLong long maxTime) {
+        mSensorManager = sensorManager;
+        mLightSensor = sensorManager != null
+                ? sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT) : null;
+        mTimer = handler;
+        mMaxSubscriptionTime = maxTime;
+
+        if (mSensorManager == null || mLightSensor == null) {
+            Slog.w(TAG, "No sensor - probe disabled");
+            mDestroyed = true;
+        }
+    }
+
+    @Override
+    public synchronized void enable() {
+        if (!mDestroyed) {
+            enableLightSensorLoggingLocked();
+        }
+    }
+
+    @Override
+    public synchronized void disable() {
+        if (!mDestroyed) {
+            disableLightSensorLoggingLocked();
+        }
+    }
+
+    @Override
+    public synchronized void destroy() {
+        disable();
+        mDestroyed = true;
+    }
+
+    /** The most recent lux reading. */
+    public float getCurrentLux() {
+        return mLastAmbientLux;
+    }
+
+    private void enableLightSensorLoggingLocked() {
+        if (!mEnabled) {
+            mEnabled = true;
+            mLastAmbientLux = -1;
+            mSensorManager.registerListener(mLightSensorListener, mLightSensor,
+                    SensorManager.SENSOR_DELAY_NORMAL);
+            Slog.v(TAG, "Enable ALS: " + mLightSensorListener.hashCode());
+        }
+
+        resetTimerLocked(true /* start */);
+    }
+
+    private void disableLightSensorLoggingLocked() {
+        resetTimerLocked(false /* start */);
+
+        if (mEnabled) {
+            mEnabled = false;
+            mLastAmbientLux = -1;
+            mSensorManager.unregisterListener(mLightSensorListener);
+            Slog.v(TAG, "Disable ALS: " + mLightSensorListener.hashCode());
+        }
+    }
+
+    private void resetTimerLocked(boolean start) {
+        mTimer.removeCallbacksAndMessages(this /* token */);
+        if (start && mMaxSubscriptionTime > 0) {
+            mTimer.postDelayed(this::onTimeout, this /* token */, mMaxSubscriptionTime);
+        }
+    }
+
+    private void onTimeout() {
+        Slog.e(TAG, "Max time exceeded for ALS logger - disabling: "
+                + mLightSensorListener.hashCode());
+        disable();
+    }
+}
diff --git a/services/core/java/com/android/server/biometrics/log/BiometricLogger.java b/services/core/java/com/android/server/biometrics/log/BiometricLogger.java
index 262be08..02b350e 100644
--- a/services/core/java/com/android/server/biometrics/log/BiometricLogger.java
+++ b/services/core/java/com/android/server/biometrics/log/BiometricLogger.java
@@ -17,21 +17,15 @@
 package com.android.server.biometrics.log;
 
 import android.annotation.NonNull;
-import android.annotation.Nullable;
 import android.content.Context;
-import android.hardware.Sensor;
-import android.hardware.SensorEvent;
-import android.hardware.SensorEventListener;
 import android.hardware.SensorManager;
 import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.BiometricsProtoEnums;
 import android.hardware.biometrics.common.OperationContext;
 import android.hardware.face.FaceManager;
 import android.hardware.fingerprint.FingerprintManager;
-import android.util.Log;
 import android.util.Slog;
 
-import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.server.biometrics.Utils;
@@ -43,61 +37,16 @@
 
     public static final String TAG = "BiometricLogger";
     public static final boolean DEBUG = false;
-    private static final Object sLock = new Object();
-
-    @GuardedBy("sLock")
-    private static int sAlsCounter;
 
     private final int mStatsModality;
     private final int mStatsAction;
     private final int mStatsClient;
     private final BiometricFrameworkStatsLogger mSink;
-    @NonNull private final SensorManager mSensorManager;
+    @NonNull private final ALSProbe mALSProbe;
 
     private long mFirstAcquireTimeMs;
-    private boolean mLightSensorEnabled = false;
     private boolean mShouldLogMetrics = true;
 
-    private class ALSProbe implements Probe {
-        private boolean mDestroyed = false;
-
-        @Override
-        public synchronized void enable() {
-            if (!mDestroyed) {
-                setLightSensorLoggingEnabled(getAmbientLightSensor(mSensorManager));
-            }
-        }
-
-        @Override
-        public synchronized void disable() {
-            if (!mDestroyed) {
-                setLightSensorLoggingEnabled(null);
-            }
-        }
-
-        @Override
-        public synchronized void destroy() {
-            disable();
-            mDestroyed = true;
-        }
-    }
-
-    // report only the most recent value
-    // consider com.android.server.display.utils.AmbientFilter or similar if need arises
-    private volatile float mLastAmbientLux = 0;
-
-    private final SensorEventListener mLightSensorListener = new SensorEventListener() {
-        @Override
-        public void onSensorChanged(SensorEvent event) {
-            mLastAmbientLux = event.values[0];
-        }
-
-        @Override
-        public void onAccuracyChanged(Sensor sensor, int accuracy) {
-            // Not used.
-        }
-    };
-
     /** Get a new logger with all unknown fields (for operations that do not require logs). */
     public static BiometricLogger ofUnknown(@NonNull Context context) {
         return new BiometricLogger(context, BiometricsProtoEnums.MODALITY_UNKNOWN,
@@ -105,6 +54,11 @@
     }
 
     /**
+     * Creates a new logger for an instance of a biometric operation.
+     *
+     * Do not reuse across operations. Instead, create a new one or use
+     * {@link #swapAction(Context, int)}.
+     *
      * @param context system_server context
      * @param statsModality One of {@link BiometricsProtoEnums} MODALITY_* constants.
      * @param statsAction One of {@link BiometricsProtoEnums} ACTION_* constants.
@@ -125,7 +79,7 @@
         mStatsAction = statsAction;
         mStatsClient = statsClient;
         mSink = logSink;
-        mSensorManager = sensorManager;
+        mALSProbe = new ALSProbe(sensorManager);
     }
 
     /** Creates a new logger with the action replaced with the new action. */
@@ -136,6 +90,7 @@
     /** Disable logging metrics and only log critical events, such as system health issues. */
     public void disableMetrics() {
         mShouldLogMetrics = false;
+        mALSProbe.destroy();
     }
 
     /** {@link BiometricsProtoEnums} CLIENT_* constants */
@@ -265,7 +220,7 @@
                     + ", RequireConfirmation: " + requireConfirmation
                     + ", State: " + authState
                     + ", Latency: " + latency
-                    + ", Lux: " + mLastAmbientLux);
+                    + ", Lux: " + mALSProbe.getCurrentLux());
         } else {
             Slog.v(TAG, "Authentication latency: " + latency);
         }
@@ -276,7 +231,7 @@
 
         mSink.authenticate(operationContext, mStatsModality, mStatsAction, mStatsClient,
                 Utils.isDebugEnabled(context, targetUserId),
-                latency, authState, requireConfirmation, targetUserId, mLastAmbientLux);
+                latency, authState, requireConfirmation, targetUserId, mALSProbe.getCurrentLux());
     }
 
     /** Log enrollment outcome. */
@@ -290,7 +245,7 @@
                     + ", User: " + targetUserId
                     + ", Client: " + mStatsClient
                     + ", Latency: " + latency
-                    + ", Lux: " + mLastAmbientLux
+                    + ", Lux: " + mALSProbe.getCurrentLux()
                     + ", Success: " + enrollSuccessful);
         } else {
             Slog.v(TAG, "Enroll latency: " + latency);
@@ -301,7 +256,7 @@
         }
 
         mSink.enroll(mStatsModality, mStatsAction, mStatsClient,
-                targetUserId, latency, enrollSuccessful, mLastAmbientLux);
+                targetUserId, latency, enrollSuccessful, mALSProbe.getCurrentLux());
     }
 
     /** Report unexpected enrollment reported by the HAL. */
@@ -323,7 +278,9 @@
     }
 
     /**
-     * Get a callback to start/stop ALS capture when a client runs.
+     * Get a callback to start/stop ALS capture when the client runs. Do not create
+     * multiple callbacks since there is at most one light sensor (they will all share
+     * a single probe sampling from that sensor).
      *
      * If the probe should not run for the entire operation, do not set startWithClient and
      * start/stop the problem when needed.
@@ -331,53 +288,7 @@
      * @param startWithClient if probe should start automatically when the operation starts.
      */
     @NonNull
-    public CallbackWithProbe<Probe> createALSCallback(boolean startWithClient) {
-        return new CallbackWithProbe<>(new ALSProbe(), startWithClient);
-    }
-
-    /** The sensor to use for ALS logging. */
-    @Nullable
-    protected Sensor getAmbientLightSensor(@NonNull SensorManager sensorManager) {
-        return mShouldLogMetrics ? sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT) : null;
-    }
-
-    private void setLightSensorLoggingEnabled(@Nullable Sensor lightSensor) {
-        if (DEBUG) {
-            Slog.v(TAG, "capturing ambient light using: "
-                    + (lightSensor != null ? lightSensor : "[disabled]"));
-        }
-
-        if (lightSensor != null) {
-            if (!mLightSensorEnabled) {
-                mLightSensorEnabled = true;
-                mLastAmbientLux = 0;
-                int localAlsCounter;
-                synchronized (sLock) {
-                    localAlsCounter = sAlsCounter++;
-                }
-
-                if (localAlsCounter == 0) {
-                    mSensorManager.registerListener(mLightSensorListener, lightSensor,
-                            SensorManager.SENSOR_DELAY_NORMAL);
-                } else {
-                    Slog.e(TAG, "Ignoring request to subscribe to ALSProbe due to non-zero ALS"
-                            + " counter: " + localAlsCounter);
-                    Slog.e(TAG, Log.getStackTraceString(new Throwable()));
-                }
-            }
-        } else {
-            mLightSensorEnabled = false;
-            mLastAmbientLux = 0;
-            mSensorManager.unregisterListener(mLightSensorListener);
-            int localAlsCounter;
-            synchronized (sLock) {
-                localAlsCounter = --sAlsCounter;
-            }
-            if (localAlsCounter != 0) {
-                Slog.e(TAG, "Non-zero ALS counter after unsubscribing from ALSProbe: "
-                        + localAlsCounter);
-                Slog.e(TAG, Log.getStackTraceString(new Throwable()));
-            }
-        }
+    public CallbackWithProbe<Probe> getAmbientLightProbe(boolean startWithClient) {
+        return new CallbackWithProbe<>(mALSProbe, startWithClient);
     }
 }
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java
index d0c58fd..ca4b747 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java
@@ -131,7 +131,7 @@
     @Override
     protected ClientMonitorCallback wrapCallbackForStart(@NonNull ClientMonitorCallback callback) {
         return new ClientMonitorCompositeCallback(
-                getLogger().createALSCallback(true /* startWithClient */), callback);
+                getLogger().getAmbientLightProbe(true /* startWithClient */), callback);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java
index da78536..5d62cde 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java
@@ -115,7 +115,7 @@
     @Override
     protected ClientMonitorCallback wrapCallbackForStart(@NonNull ClientMonitorCallback callback) {
         return new ClientMonitorCompositeCallback(mPreviewHandleDeleterCallback,
-                getLogger().createALSCallback(true /* startWithClient */), callback);
+                getLogger().getAmbientLightProbe(true /* startWithClient */), callback);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceAuthenticationClient.java
index 1935a5b..9baca98 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceAuthenticationClient.java
@@ -101,7 +101,7 @@
     @Override
     protected ClientMonitorCallback wrapCallbackForStart(@NonNull ClientMonitorCallback callback) {
         return new ClientMonitorCompositeCallback(
-                getLogger().createALSCallback(true /* startWithClient */), callback);
+                getLogger().getAmbientLightProbe(true /* startWithClient */), callback);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceEnrollClient.java
index 226e458..16d2f7a 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceEnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceEnrollClient.java
@@ -75,7 +75,7 @@
     @Override
     protected ClientMonitorCallback wrapCallbackForStart(@NonNull ClientMonitorCallback callback) {
         return new ClientMonitorCompositeCallback(
-                getLogger().createALSCallback(true /* startWithClient */), callback);
+                getLogger().getAmbientLightProbe(true /* startWithClient */), callback);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
index b530c8d..1688f96 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
@@ -137,7 +137,7 @@
         mLockoutCache = lockoutCache;
         mSensorOverlays = new SensorOverlays(udfpsOverlayController, sidefpsController);
         mSensorProps = sensorProps;
-        mALSProbeCallback = getLogger().createALSCallback(false /* startWithClient */);
+        mALSProbeCallback = getLogger().getAmbientLightProbe(false /* startWithClient */);
         mHandler = handler;
 
         mWaitForAuthKeyguard =
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
index f4f0a19..7d2cf9d 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java
@@ -94,7 +94,7 @@
         mSensorOverlays = new SensorOverlays(udfpsOverlayController, sidefpsController);
         mMaxTemplatesPerUser = maxTemplatesPerUser;
 
-        mALSProbeCallback = getLogger().createALSCallback(false /* startWithClient */);
+        mALSProbeCallback = getLogger().getAmbientLightProbe(false /* startWithClient */);
 
         mEnrollReason = enrollReason;
         if (enrollReason == FingerprintManager.ENROLL_FIND_SENSOR) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java
index 97fbb5f..7ed1a51 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java
@@ -86,7 +86,7 @@
         mLockoutFrameworkImpl = lockoutTracker;
         mSensorOverlays = new SensorOverlays(udfpsOverlayController, sidefpsController);
         mSensorProps = sensorProps;
-        mALSProbeCallback = getLogger().createALSCallback(false /* startWithClient */);
+        mALSProbeCallback = getLogger().getAmbientLightProbe(false /* startWithClient */);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java
index 2a59c8c..5d9af53 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java
@@ -84,7 +84,7 @@
     @Override
     protected ClientMonitorCallback wrapCallbackForStart(@NonNull ClientMonitorCallback callback) {
         return new ClientMonitorCompositeCallback(
-                getLogger().createALSCallback(true /* startWithClient */), callback);
+                getLogger().getAmbientLightProbe(true /* startWithClient */), callback);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index d426679..4c23ab8 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -326,7 +326,7 @@
 
     public String addAutomaticZenRule(String pkg, AutomaticZenRule automaticZenRule,
             String reason) {
-        if (!isSystemRule(automaticZenRule)) {
+        if (!ZenModeConfig.SYSTEM_AUTHORITY.equals(pkg)) {
             PackageItemInfo component = getServiceInfo(automaticZenRule.getOwner());
             if (component == null) {
                 component = getActivityInfo(automaticZenRule.getConfigurationActivity());
@@ -582,11 +582,6 @@
         }
     }
 
-    private boolean isSystemRule(AutomaticZenRule rule) {
-        return rule.getOwner() != null
-                && ZenModeConfig.SYSTEM_AUTHORITY.equals(rule.getOwner().getPackageName());
-    }
-
     private ServiceInfo getServiceInfo(ComponentName owner) {
         Intent queryIntent = new Intent();
         queryIntent.setComponent(owner);
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 0376974..b64409c 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -3176,8 +3176,12 @@
             if (DEBUG_DISPLAY) Slog.v(TAG_WM, "Removing display=" + this);
             mPointerEventDispatcher.dispose();
             setRotationAnimation(null);
+            // Unlink death from remote to clear the reference from binder -> mRemoteInsetsDeath
+            // -> this DisplayContent.
+            setRemoteInsetsController(null);
             mWmService.mAnimator.removeDisplayLocked(mDisplayId);
             mOverlayLayer.release();
+            mWindowingLayer.release();
             mInputMonitor.onDisplayRemoved();
             mWmService.mDisplayNotificationController.dispatchDisplayRemoved(this);
             mWmService.mAccessibilityController.onDisplayRemoved(mDisplayId);
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java b/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java
new file mode 100644
index 0000000..10f0a5c
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/biometrics/log/ALSProbeTest.java
@@ -0,0 +1,176 @@
+/*
+ * 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.server.biometrics.log;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.hardware.input.InputSensorInfo;
+import android.os.Handler;
+import android.platform.test.annotations.Presubmit;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.testing.TestableLooper.RunWithLooper;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@Presubmit
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@RunWithLooper
+public class ALSProbeTest {
+
+    private static final long TIMEOUT_MS = 1000;
+
+    @Rule
+    public final MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock
+    private SensorManager mSensorManager;
+    @Captor
+    private ArgumentCaptor<SensorEventListener> mSensorEventListenerCaptor;
+
+    private TestableLooper mLooper;
+    private Sensor mLightSensor = new Sensor(
+            new InputSensorInfo("", "", 0, 0, Sensor.TYPE_LIGHT, 0, 0, 0, 0, 0, 0,
+                    "", "", 0, 0, 0));
+
+    private ALSProbe mProbe;
+
+    @Before
+    public void setup() {
+        mLooper = TestableLooper.get(this);
+        when(mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT)).thenReturn(mLightSensor);
+        mProbe = new ALSProbe(mSensorManager, new Handler(mLooper.getLooper()), TIMEOUT_MS - 1);
+        reset(mSensorManager);
+    }
+
+    @Test
+    public void testEnable() {
+        final float value = 2.0f;
+        mProbe.enable();
+        verify(mSensorManager).registerListener(
+                mSensorEventListenerCaptor.capture(), any(), anyInt());
+
+        mSensorEventListenerCaptor.getValue().onSensorChanged(
+                new SensorEvent(mLightSensor, 1, 1, new float[]{4.0f}));
+        mSensorEventListenerCaptor.getValue().onSensorChanged(
+                new SensorEvent(mLightSensor, 1, 2, new float[]{value}));
+
+        assertThat(mProbe.getCurrentLux()).isEqualTo(value);
+    }
+
+    @Test
+    public void testEnableOnlyOnce() {
+        mProbe.enable();
+        mProbe.enable();
+
+        verify(mSensorManager).registerListener(any(), any(), anyInt());
+        verifyNoMoreInteractions(mSensorManager);
+    }
+
+    @Test
+    public void testDisable() {
+        mProbe.enable();
+        verify(mSensorManager).registerListener(
+                mSensorEventListenerCaptor.capture(), any(), anyInt());
+        mProbe.disable();
+
+        verify(mSensorManager).unregisterListener(eq(mSensorEventListenerCaptor.getValue()));
+        verifyNoMoreInteractions(mSensorManager);
+    }
+
+    @Test
+    public void testDestroy() {
+        mProbe.destroy();
+        mProbe.enable();
+
+        verify(mSensorManager, never()).registerListener(any(), any(), anyInt());
+        verifyNoMoreInteractions(mSensorManager);
+    }
+
+    @Test
+    public void testDisabledReportsNegativeValue() {
+        assertThat(mProbe.getCurrentLux()).isLessThan(0f);
+
+        mProbe.enable();
+        verify(mSensorManager).registerListener(
+                mSensorEventListenerCaptor.capture(), any(), anyInt());
+        mSensorEventListenerCaptor.getValue().onSensorChanged(
+                new SensorEvent(mLightSensor, 1, 1, new float[]{4.0f}));
+        mProbe.disable();
+
+        assertThat(mProbe.getCurrentLux()).isLessThan(0f);
+    }
+
+    @Test
+    public void testWatchDog() {
+        mProbe.enable();
+        verify(mSensorManager).registerListener(
+                mSensorEventListenerCaptor.capture(), any(), anyInt());
+        mSensorEventListenerCaptor.getValue().onSensorChanged(
+                new SensorEvent(mLightSensor, 1, 1, new float[]{4.0f}));
+        moveTimeBy(TIMEOUT_MS);
+
+        verify(mSensorManager).unregisterListener(eq(mSensorEventListenerCaptor.getValue()));
+        verifyNoMoreInteractions(mSensorManager);
+        assertThat(mProbe.getCurrentLux()).isLessThan(0f);
+    }
+
+    @Test
+    public void testEnableExtendsWatchDog() {
+        mProbe.enable();
+        verify(mSensorManager).registerListener(any(), any(), anyInt());
+
+        moveTimeBy(TIMEOUT_MS / 2);
+        verify(mSensorManager, never()).unregisterListener(any(SensorEventListener.class));
+
+        mProbe.enable();
+        moveTimeBy(TIMEOUT_MS);
+
+        verify(mSensorManager).unregisterListener(any(SensorEventListener.class));
+        verifyNoMoreInteractions(mSensorManager);
+        assertThat(mProbe.getCurrentLux()).isLessThan(0f);
+    }
+
+    private void moveTimeBy(long millis) {
+        mLooper.moveTimeForward(millis);
+        mLooper.processAllMessages();
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricLoggerTest.java b/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricLoggerTest.java
index 0b8e8ad..60dc2eb 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricLoggerTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/log/BiometricLoggerTest.java
@@ -232,7 +232,7 @@
     public void testALSCallback() {
         mLogger = createLogger();
         final CallbackWithProbe<Probe> callback =
-                mLogger.createALSCallback(true /* startWithClient */);
+                mLogger.getAmbientLightProbe(true /* startWithClient */);
 
         callback.onClientStarted(mClient);
         verify(mSensorManager).registerListener(any(), any(), anyInt());
@@ -242,10 +242,37 @@
     }
 
     @Test
+    public void testALSCallbackWhenLogsDisabled() {
+        mLogger = createLogger();
+        mLogger.disableMetrics();
+        final CallbackWithProbe<Probe> callback =
+                mLogger.getAmbientLightProbe(true /* startWithClient */);
+
+        callback.onClientStarted(mClient);
+        verify(mSensorManager, never()).registerListener(any(), any(), anyInt());
+
+        callback.onClientFinished(mClient, true /* success */);
+        verify(mSensorManager, never()).unregisterListener(any(SensorEventListener.class));
+    }
+
+    @Test
+    public void testALSCallbackWhenDisabledAfterStarting() {
+        mLogger = createLogger();
+        final CallbackWithProbe<Probe> callback =
+                mLogger.getAmbientLightProbe(true /* startWithClient */);
+
+        callback.onClientStarted(mClient);
+        verify(mSensorManager).registerListener(any(), any(), anyInt());
+
+        mLogger.disableMetrics();
+        verify(mSensorManager).unregisterListener(any(SensorEventListener.class));
+    }
+
+    @Test
     public void testALSCallbackDoesNotStart() {
         mLogger = createLogger();
         final CallbackWithProbe<Probe> callback =
-                mLogger.createALSCallback(false /* startWithClient */);
+                mLogger.getAmbientLightProbe(false /* startWithClient */);
 
         callback.onClientStarted(mClient);
         callback.onClientFinished(mClient, true /* success */);
@@ -256,7 +283,7 @@
     public void testALSCallbackDestroyed() {
         mLogger = createLogger();
         final CallbackWithProbe<Probe> callback =
-                mLogger.createALSCallback(true /* startWithClient */);
+                mLogger.getAmbientLightProbe(true /* startWithClient */);
 
         callback.onClientStarted(mClient);
         callback.onClientFinished(mClient, false /* success */);
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
index a149f3e..1ed52fc 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
@@ -137,7 +137,7 @@
     @Before
     public void setup() {
         mContext.addMockSystemService(BiometricManager.class, mBiometricManager);
-        when(mBiometricLogger.createALSCallback(anyBoolean())).thenAnswer(i ->
+        when(mBiometricLogger.getAmbientLightProbe(anyBoolean())).thenAnswer(i ->
                 new CallbackWithProbe<>(mLuxProbe, i.getArgument(0)));
         when(mBiometricContext.updateContext(any(), anyBoolean())).thenAnswer(
                 i -> i.getArgument(0));
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java
index f77eb0b..97fe9ea 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java
@@ -119,7 +119,7 @@
 
     @Before
     public void setup() {
-        when(mBiometricLogger.createALSCallback(anyBoolean())).thenAnswer(i ->
+        when(mBiometricLogger.getAmbientLightProbe(anyBoolean())).thenAnswer(i ->
                 new CallbackWithProbe<>(mLuxProbe, i.getArgument(0)));
         when(mBiometricContext.updateContext(any(), anyBoolean())).thenAnswer(
                 i -> i.getArgument(0));
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index 4550b56..2ccdcaa 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -1672,6 +1672,36 @@
     }
 
     @Test
+    public void testAddAutomaticZenRule_claimedSystemOwner() {
+        // Make sure anything that claims to have a "system" owner but not actually part of the
+        // system package still gets limited on number of rules
+        for (int i = 0; i < RULE_LIMIT_PER_PACKAGE; i++) {
+            ScheduleInfo si = new ScheduleInfo();
+            si.startHour = i;
+            AutomaticZenRule zenRule = new AutomaticZenRule("name" + i,
+                    new ComponentName("android", "ScheduleConditionProvider" + i),
+                    null, // configuration activity
+                    ZenModeConfig.toScheduleConditionId(si),
+                    new ZenPolicy.Builder().build(),
+                    NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
+            String id = mZenModeHelperSpy.addAutomaticZenRule("pkgname", zenRule, "test");
+            assertNotNull(id);
+        }
+        try {
+            AutomaticZenRule zenRule = new AutomaticZenRule("name",
+                    new ComponentName("android", "ScheduleConditionProviderFinal"),
+                    null, // configuration activity
+                    ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
+                    new ZenPolicy.Builder().build(),
+                    NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
+            String id = mZenModeHelperSpy.addAutomaticZenRule("pkgname", zenRule, "test");
+            fail("allowed too many rules to be created");
+        } catch (IllegalArgumentException e) {
+            // yay
+        }
+    }
+
+    @Test
     public void testAddAutomaticZenRule_CA() {
         AutomaticZenRule zenRule = new AutomaticZenRule("name",
                 null,