Merge "Media - Don't animate during camera gesture"
diff --git a/api/test-current.txt b/api/test-current.txt
index 10a367f..a1d1fa7 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -970,6 +970,7 @@
     method public boolean isSystemApp();
     field public static final int PRIVATE_FLAG_PRIVILEGED = 8; // 0x8
     field public int privateFlags;
+    field public int targetSandboxVersion;
   }
 
   public class LauncherApps {
@@ -1017,6 +1018,7 @@
     field public static final String FEATURE_ADOPTABLE_STORAGE = "android.software.adoptable_storage";
     field public static final String FEATURE_FILE_BASED_ENCRYPTION = "android.software.file_based_encryption";
     field public static final int FLAG_PERMISSION_APPLY_RESTRICTION = 16384; // 0x4000
+    field public static final int FLAG_PERMISSION_GRANTED_BY_DEFAULT = 32; // 0x20
     field public static final int FLAG_PERMISSION_GRANTED_BY_ROLE = 32768; // 0x8000
     field public static final int FLAG_PERMISSION_ONE_TIME = 65536; // 0x10000
     field public static final int FLAG_PERMISSION_POLICY_FIXED = 4; // 0x4
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 6f3e8922..6737972 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -5187,19 +5187,11 @@
             bindHeaderChronometerAndTime(contentView, p);
             bindProfileBadge(contentView, p);
             bindAlertedIcon(contentView, p);
-            bindActivePermissions(contentView, p);
             bindFeedbackIcon(contentView, p);
             bindExpandButton(contentView, p);
             mN.mUsesStandardHeader = true;
         }
 
-        private void bindActivePermissions(RemoteViews contentView, StandardTemplateParams p) {
-            int color = getNeutralColor(p);
-            contentView.setDrawableTint(R.id.camera, false, color, PorterDuff.Mode.SRC_ATOP);
-            contentView.setDrawableTint(R.id.mic, false, color, PorterDuff.Mode.SRC_ATOP);
-            contentView.setDrawableTint(R.id.overlay, false, color, PorterDuff.Mode.SRC_ATOP);
-        }
-
         private void bindFeedbackIcon(RemoteViews contentView, StandardTemplateParams p) {
             int color = getNeutralColor(p);
             contentView.setDrawableTint(R.id.feedback, false, color, PorterDuff.Mode.SRC_ATOP);
diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java
index 043953d..0c6810c 100644
--- a/core/java/android/content/pm/ApplicationInfo.java
+++ b/core/java/android/content/pm/ApplicationInfo.java
@@ -1134,6 +1134,7 @@
      * @hide
      */
     @SystemApi
+    @TestApi
     public int targetSandboxVersion;
 
     /**
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index e08af55..7b2955d 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -3373,6 +3373,7 @@
      * @hide
      */
     @SystemApi
+    @TestApi
     public static final int FLAG_PERMISSION_GRANTED_BY_DEFAULT =  1 << 5;
 
     /**
diff --git a/core/java/android/view/NotificationHeaderView.java b/core/java/android/view/NotificationHeaderView.java
index bf94670..6136a80 100644
--- a/core/java/android/view/NotificationHeaderView.java
+++ b/core/java/android/view/NotificationHeaderView.java
@@ -52,14 +52,12 @@
     private View mHeaderText;
     private View mSecondaryHeaderText;
     private OnClickListener mExpandClickListener;
-    private OnClickListener mAppOpsListener;
     private OnClickListener mFeedbackListener;
     private HeaderTouchListener mTouchListener = new HeaderTouchListener();
     private LinearLayout mTransferChip;
     private NotificationExpandButton mExpandButton;
     private CachingIconView mIcon;
     private View mProfileBadge;
-    private View mAppOps;
     private View mFeedbackIcon;
     private boolean mExpanded;
     private boolean mShowExpandButtonAtEnd;
@@ -117,7 +115,6 @@
         mExpandButton = findViewById(com.android.internal.R.id.expand_button);
         mIcon = findViewById(com.android.internal.R.id.icon);
         mProfileBadge = findViewById(com.android.internal.R.id.profile_badge);
-        mAppOps = findViewById(com.android.internal.R.id.app_ops);
         mFeedbackIcon = findViewById(com.android.internal.R.id.feedback);
     }
 
@@ -146,7 +143,6 @@
             // Icons that should go at the end
             if ((child == mExpandButton && mShowExpandButtonAtEnd)
                     || child == mProfileBadge
-                    || child == mAppOps
                     || child == mFeedbackIcon
                     || child == mTransferChip) {
                 iconWidth += lp.leftMargin + lp.rightMargin + child.getMeasuredWidth();
@@ -212,7 +208,6 @@
             // Icons that should go at the end
             if ((child == mExpandButton && mShowExpandButtonAtEnd)
                     || child == mProfileBadge
-                    || child == mAppOps
                     || child == mFeedbackIcon
                     || child == mTransferChip) {
                 if (end == getMeasuredWidth()) {
@@ -282,7 +277,7 @@
     }
 
     private void updateTouchListener() {
-        if (mExpandClickListener == null && mAppOpsListener == null && mFeedbackListener == null) {
+        if (mExpandClickListener == null && mFeedbackListener == null) {
             setOnTouchListener(null);
             return;
         }
@@ -291,14 +286,6 @@
     }
 
     /**
-     * Sets onclick listener for app ops icons.
-     */
-    public void setAppOpsOnClickListener(OnClickListener l) {
-        mAppOpsListener = l;
-        updateTouchListener();
-    }
-
-    /**
      * Sets onclick listener for feedback icon.
      */
     public void setFeedbackOnClickListener(OnClickListener l) {
@@ -394,7 +381,6 @@
 
         private final ArrayList<Rect> mTouchRects = new ArrayList<>();
         private Rect mExpandButtonRect;
-        private Rect mAppOpsRect;
         private Rect mFeedbackRect;
         private int mTouchSlop;
         private boolean mTrackGesture;
@@ -408,9 +394,7 @@
             mTouchRects.clear();
             addRectAroundView(mIcon);
             mExpandButtonRect = addRectAroundView(mExpandButton);
-            mAppOpsRect = addRectAroundView(mAppOps);
             mFeedbackRect = addRectAroundView(mFeedbackIcon);
-            setTouchDelegate(new TouchDelegate(mAppOpsRect, mAppOps));
             addWidthRect();
             mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
         }
@@ -471,11 +455,7 @@
                     break;
                 case MotionEvent.ACTION_UP:
                     if (mTrackGesture) {
-                        if (mAppOps.isVisibleToUser() && (mAppOpsRect.contains((int) x, (int) y)
-                                || mAppOpsRect.contains((int) mDownX, (int) mDownY))) {
-                            mAppOps.performClick();
-                            return true;
-                        } else if (mFeedbackIcon.isVisibleToUser()
+                        if (mFeedbackIcon.isVisibleToUser()
                                 && (mFeedbackRect.contains((int) x, (int) y))
                                 || mFeedbackRect.contains((int) mDownX, (int) mDownY)) {
                             mFeedbackIcon.performClick();
diff --git a/core/java/com/android/internal/widget/ConversationLayout.java b/core/java/com/android/internal/widget/ConversationLayout.java
index 3332143..289a36f 100644
--- a/core/java/com/android/internal/widget/ConversationLayout.java
+++ b/core/java/com/android/internal/widget/ConversationLayout.java
@@ -168,8 +168,6 @@
     private int mFacePileProtectionWidthExpanded;
     private boolean mImportantConversation;
     private TextView mUnreadBadge;
-    private ViewGroup mAppOps;
-    private Rect mAppOpsTouchRect = new Rect();
     private View mFeedbackIcon;
     private float mMinTouchSize;
     private Icon mConversationIcon;
@@ -214,7 +212,6 @@
         mConversationIconView = findViewById(R.id.conversation_icon);
         mConversationIconContainer = findViewById(R.id.conversation_icon_container);
         mIcon = findViewById(R.id.icon);
-        mAppOps = findViewById(com.android.internal.R.id.app_ops);
         mFeedbackIcon = findViewById(com.android.internal.R.id.feedback);
         mMinTouchSize = 48 * getResources().getDisplayMetrics().density;
         mImportanceRingView = findViewById(R.id.conversation_icon_badge_ring);
@@ -1174,43 +1171,6 @@
             });
         }
         mTouchDelegate.clear();
-        if (mAppOps.getWidth() > 0) {
-
-            // Let's increase the touch size of the app ops view if it's here
-            mAppOpsTouchRect.set(
-                    mAppOps.getLeft(),
-                    mAppOps.getTop(),
-                    mAppOps.getRight(),
-                    mAppOps.getBottom());
-            for (int i = 0; i < mAppOps.getChildCount(); i++) {
-                View child = mAppOps.getChildAt(i);
-                if (child.getVisibility() == GONE) {
-                    continue;
-                }
-                // Make sure each child has at least a minTouchSize touch target around it
-                float childTouchLeft = child.getLeft() + child.getWidth() / 2.0f
-                        - mMinTouchSize / 2.0f;
-                float childTouchRight = childTouchLeft + mMinTouchSize;
-                mAppOpsTouchRect.left = (int) Math.min(mAppOpsTouchRect.left,
-                        mAppOps.getLeft() + childTouchLeft);
-                mAppOpsTouchRect.right = (int) Math.max(mAppOpsTouchRect.right,
-                        mAppOps.getLeft() + childTouchRight);
-            }
-
-            // Increase the height
-            int heightIncrease = 0;
-            if (mAppOpsTouchRect.height() < mMinTouchSize) {
-                heightIncrease = (int) Math.ceil((mMinTouchSize - mAppOpsTouchRect.height())
-                        / 2.0f);
-            }
-            mAppOpsTouchRect.inset(0, -heightIncrease);
-
-            getRelativeTouchRect(mAppOpsTouchRect, mAppOps);
-
-            // Extend the size of the app opps to be at least 48dp
-            mTouchDelegate.add(new TouchDelegate(mAppOpsTouchRect, mAppOps));
-
-        }
         if (mFeedbackIcon.getVisibility() == VISIBLE) {
             updateFeedbackIconMargins();
             float width = Math.max(mMinTouchSize, mFeedbackIcon.getWidth());
@@ -1240,13 +1200,7 @@
 
     private void updateFeedbackIconMargins() {
         MarginLayoutParams lp = (MarginLayoutParams) mFeedbackIcon.getLayoutParams();
-        if (mAppOps.getWidth() == 0) {
-            lp.setMarginStart(mNotificationHeaderSeparatingMargin);
-        } else {
-            float width = Math.max(mMinTouchSize, mFeedbackIcon.getWidth());
-            int horizontalMargin = (int) ((width - mFeedbackIcon.getWidth()) / 2);
-            lp.setMarginStart(horizontalMargin);
-        }
+        lp.setMarginStart(mNotificationHeaderSeparatingMargin);
         mFeedbackIcon.setLayoutParams(lp);
     }
 
diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp
index c73441c..f96ed36 100644
--- a/core/jni/com_android_internal_os_Zygote.cpp
+++ b/core/jni/com_android_internal_os_Zygote.cpp
@@ -1790,8 +1790,7 @@
 #ifdef ANDROID_EXPERIMENTAL_MTE
       SetTagCheckingLevel(PR_MTE_TCF_SYNC);
 #endif
-      // TODO(pcc): Use SYNC here once the allocator supports it.
-      heap_tagging_level = M_HEAP_TAGGING_LEVEL_ASYNC;
+      heap_tagging_level = M_HEAP_TAGGING_LEVEL_SYNC;
       break;
     default:
 #ifdef ANDROID_EXPERIMENTAL_MTE
diff --git a/core/res/res/layout/notification_template_header.xml b/core/res/res/layout/notification_template_header.xml
index d22a19f..9a1b592 100644
--- a/core/res/res/layout/notification_template_header.xml
+++ b/core/res/res/layout/notification_template_header.xml
@@ -160,43 +160,6 @@
         android:visibility="gone"
         android:contentDescription="@string/notification_work_profile_content_description"
         />
-    <LinearLayout
-        android:id="@+id/app_ops"
-        android:layout_height="match_parent"
-        android:layout_width="wrap_content"
-        android:layout_marginStart="6dp"
-        android:background="?android:selectableItemBackgroundBorderless"
-        android:orientation="horizontal">
-        <ImageView
-            android:id="@+id/camera"
-            android:layout_width="?attr/notificationHeaderIconSize"
-            android:layout_height="?attr/notificationHeaderIconSize"
-            android:src="@drawable/ic_camera"
-            android:visibility="gone"
-            android:focusable="false"
-            android:contentDescription="@string/notification_appops_camera_active"
-            />
-        <ImageView
-            android:id="@+id/mic"
-            android:layout_width="?attr/notificationHeaderIconSize"
-            android:layout_height="?attr/notificationHeaderIconSize"
-            android:src="@drawable/ic_mic"
-            android:layout_marginStart="4dp"
-            android:visibility="gone"
-            android:focusable="false"
-            android:contentDescription="@string/notification_appops_microphone_active"
-            />
-        <ImageView
-            android:id="@+id/overlay"
-            android:layout_width="?attr/notificationHeaderIconSize"
-            android:layout_height="?attr/notificationHeaderIconSize"
-            android:src="@drawable/ic_alert_window_layer"
-            android:layout_marginStart="4dp"
-            android:visibility="gone"
-            android:focusable="false"
-            android:contentDescription="@string/notification_appops_overlay_active"
-            />
-    </LinearLayout>
     <include
         layout="@layout/notification_material_media_transfer_action"
         android:id="@+id/media_seamless"
diff --git a/packages/CarSystemUI/src/com/android/systemui/CarSystemUIModule.java b/packages/CarSystemUI/src/com/android/systemui/CarSystemUIModule.java
index 2a96e09..51ad286 100644
--- a/packages/CarSystemUI/src/com/android/systemui/CarSystemUIModule.java
+++ b/packages/CarSystemUI/src/com/android/systemui/CarSystemUIModule.java
@@ -29,8 +29,8 @@
 import com.android.systemui.car.CarDeviceProvisionedController;
 import com.android.systemui.car.CarDeviceProvisionedControllerImpl;
 import com.android.systemui.car.keyguard.CarKeyguardViewController;
+import com.android.systemui.car.notification.NotificationShadeWindowControllerImpl;
 import com.android.systemui.car.statusbar.DozeServiceHost;
-import com.android.systemui.car.statusbar.DummyNotificationShadeWindowController;
 import com.android.systemui.car.volume.CarVolumeDialogComponent;
 import com.android.systemui.dagger.GlobalRootComponent;
 import com.android.systemui.dagger.qualifiers.Background;
@@ -53,12 +53,12 @@
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationLockscreenUserManagerImpl;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.KeyguardEnvironmentImpl;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
-import com.android.systemui.statusbar.phone.NotificationShadeWindowController;
 import com.android.systemui.statusbar.phone.ShadeController;
 import com.android.systemui.statusbar.phone.ShadeControllerImpl;
 import com.android.systemui.statusbar.policy.BatteryController;
@@ -209,6 +209,10 @@
             CarKeyguardViewController carKeyguardViewController);
 
     @Binds
+    abstract NotificationShadeWindowController bindNotificationShadeController(
+            NotificationShadeWindowControllerImpl notificationPanelViewController);
+
+    @Binds
     abstract DeviceProvisionedController bindDeviceProvisionedController(
             CarDeviceProvisionedControllerImpl deviceProvisionedController);
 
@@ -217,9 +221,5 @@
             CarDeviceProvisionedControllerImpl deviceProvisionedController);
 
     @Binds
-    abstract NotificationShadeWindowController bindNotificationShadeWindowController(
-            DummyNotificationShadeWindowController notificationShadeWindowController);
-
-    @Binds
     abstract DozeHost bindDozeHost(DozeServiceHost dozeServiceHost);
 }
diff --git a/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationShadeWindowControllerImpl.java b/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationShadeWindowControllerImpl.java
new file mode 100644
index 0000000..0c064bd
--- /dev/null
+++ b/packages/CarSystemUI/src/com/android/systemui/car/notification/NotificationShadeWindowControllerImpl.java
@@ -0,0 +1,46 @@
+/*
+ * 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.car.notification;
+
+import com.android.systemui.car.window.OverlayViewGlobalStateController;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/** The automotive version of the notification shade window controller. */
+@Singleton
+public class NotificationShadeWindowControllerImpl implements
+        NotificationShadeWindowController {
+
+    private final OverlayViewGlobalStateController mController;
+
+    @Inject
+    public NotificationShadeWindowControllerImpl(OverlayViewGlobalStateController controller) {
+        mController = controller;
+    }
+
+    @Override
+    public void setForceDozeBrightness(boolean forceDozeBrightness) {
+        // No-op since dozing is not supported in Automotive devices.
+    }
+
+    @Override
+    public void setNotificationShadeFocusable(boolean focusable) {
+        mController.setWindowFocusable(focusable);
+    }
+}
diff --git a/packages/CarSystemUI/src/com/android/systemui/car/statusbar/DummyNotificationShadeWindowController.java b/packages/CarSystemUI/src/com/android/systemui/car/statusbar/DummyNotificationShadeWindowController.java
deleted file mode 100644
index 13f2b7e..0000000
--- a/packages/CarSystemUI/src/com/android/systemui/car/statusbar/DummyNotificationShadeWindowController.java
+++ /dev/null
@@ -1,74 +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.car.statusbar;
-
-import android.app.IActivityManager;
-import android.content.Context;
-import android.view.WindowManager;
-
-import com.android.systemui.car.window.SystemUIOverlayWindowController;
-import com.android.systemui.colorextraction.SysuiColorExtractor;
-import com.android.systemui.dump.DumpManager;
-import com.android.systemui.keyguard.KeyguardViewMediator;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.statusbar.phone.BiometricUnlockController;
-import com.android.systemui.statusbar.phone.DozeParameters;
-import com.android.systemui.statusbar.phone.KeyguardBypassController;
-import com.android.systemui.statusbar.phone.NotificationShadeWindowController;
-import com.android.systemui.statusbar.policy.ConfigurationController;
-
-import javax.inject.Inject;
-import javax.inject.Singleton;
-
-/**
- * A dummy implementation of {@link NotificationShadeWindowController}.
- *
- * TODO(b/155711562): This should be replaced with a longer term solution (i.e. separating
- * {@link BiometricUnlockController} from the views it depends on).
- */
-@Singleton
-public class DummyNotificationShadeWindowController extends NotificationShadeWindowController {
-    private final SystemUIOverlayWindowController mOverlayWindowController;
-
-    @Inject
-    public DummyNotificationShadeWindowController(Context context,
-            WindowManager windowManager, IActivityManager activityManager,
-            DozeParameters dozeParameters,
-            StatusBarStateController statusBarStateController,
-            ConfigurationController configurationController,
-            KeyguardViewMediator keyguardViewMediator,
-            KeyguardBypassController keyguardBypassController,
-            SysuiColorExtractor colorExtractor,
-            DumpManager dumpManager,
-            SystemUIOverlayWindowController overlayWindowController) {
-        super(context, windowManager, activityManager, dozeParameters, statusBarStateController,
-                configurationController, keyguardViewMediator, keyguardBypassController,
-                colorExtractor, dumpManager);
-        mOverlayWindowController = overlayWindowController;
-    }
-
-    @Override
-    public void setForceDozeBrightness(boolean forceDozeBrightness) {
-        // No op.
-    }
-
-    @Override
-    public void setNotificationShadeFocusable(boolean focusable) {
-        // The overlay window is the car sysui equivalent of the notification shade.
-        mOverlayWindowController.setWindowFocusable(focusable);
-    }
-}
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java
index b7ae3dc..bc58bfc 100644
--- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java
@@ -189,10 +189,12 @@
                 }
             }
             updateStatusLabel();
+            mCallback.run();
         } else if (action.equals(WifiManager.RSSI_CHANGED_ACTION)) {
             // Default to -200 as its below WifiManager.MIN_RSSI.
             updateRssi(intent.getIntExtra(WifiManager.EXTRA_NEW_RSSI, -200));
             updateStatusLabel();
+            mCallback.run();
         }
     }
 
@@ -218,13 +220,15 @@
             return;
         }
         NetworkCapabilities networkCapabilities;
-        final Network currentWifiNetwork = mWifiManager.getCurrentNetwork();
-        if (currentWifiNetwork != null && currentWifiNetwork.equals(mDefaultNetwork)) {
+        isDefaultNetwork = false;
+        if (mDefaultNetworkCapabilities != null) {
+            isDefaultNetwork = mDefaultNetworkCapabilities.hasTransport(
+                    NetworkCapabilities.TRANSPORT_WIFI);
+        }
+        if (isDefaultNetwork) {
             // Wifi is connected and the default network.
-            isDefaultNetwork = true;
             networkCapabilities = mDefaultNetworkCapabilities;
         } else {
-            isDefaultNetwork = false;
             networkCapabilities = mConnectivityManager.getNetworkCapabilities(
                     mWifiManager.getCurrentNetwork());
         }
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowPlugin.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowPlugin.java
index d2112a0..883f4de 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowPlugin.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowPlugin.java
@@ -75,11 +75,6 @@
     public MenuItem getLongpressMenuItem(Context context);
 
     /**
-     * @return the {@link MenuItem} to display when app ops icons are pressed.
-     */
-    public MenuItem getAppOpsMenuItem(Context context);
-
-    /**
      * @return the {@link MenuItem} to display when feedback icon is pressed.
      */
     public MenuItem getFeedbackMenuItem(Context context);
diff --git a/packages/SystemUI/res/values/config_tv.xml b/packages/SystemUI/res/values/config_tv.xml
index 5cb840f..7451ba8 100644
--- a/packages/SystemUI/res/values/config_tv.xml
+++ b/packages/SystemUI/res/values/config_tv.xml
@@ -22,9 +22,4 @@
     <!-- Whether to enable microphone disclosure indicator
          (com.android.systemui.statusbar.tv.micdisclosure.AudioRecordingDisclosureBar). -->
     <bool name="audio_recording_disclosure_enabled">true</bool>
-
-    <!-- Whether the indicator should expand and show the recording application's label.
-         When disabled (false) the "minimized" indicator would appear on the screen whenever an
-         application is recording, but will not reveal to the user what application this is.  -->
-    <bool name="audio_recording_disclosure_reveal_packages">false</bool>
 </resources>
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index 10f2069..c04775a 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -47,6 +47,8 @@
 import com.android.systemui.keyguard.ScreenLifecycle;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.model.SysUiState;
+import com.android.systemui.navigationbar.NavigationBarController;
+import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.DarkIconDispatcher;
 import com.android.systemui.plugins.PluginDependencyProvider;
@@ -64,11 +66,11 @@
 import com.android.systemui.shared.system.PackageManagerWrapper;
 import com.android.systemui.stackdivider.Divider;
 import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.navigationbar.NavigationBarController;
 import com.android.systemui.statusbar.NotificationListener;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.NotificationViewHierarchyManager;
 import com.android.systemui.statusbar.SmartReplyController;
 import com.android.systemui.statusbar.VibratorHelper;
@@ -85,10 +87,8 @@
 import com.android.systemui.statusbar.phone.LightBarController;
 import com.android.systemui.statusbar.phone.LockscreenGestureLogger;
 import com.android.systemui.statusbar.phone.ManagedProfileController;
-import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.statusbar.phone.NotificationGroupAlertTransferHelper;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
-import com.android.systemui.statusbar.phone.NotificationShadeWindowController;
 import com.android.systemui.statusbar.phone.ShadeController;
 import com.android.systemui.statusbar.phone.StatusBar;
 import com.android.systemui.statusbar.phone.StatusBarIconController;
diff --git a/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java b/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java
index 2deeb12..5f88156 100644
--- a/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java
+++ b/packages/SystemUI/src/com/android/systemui/ForegroundServiceController.java
@@ -39,21 +39,15 @@
  */
 @Singleton
 public class ForegroundServiceController {
-    public static final int[] APP_OPS = new int[] {AppOpsManager.OP_CAMERA,
-            AppOpsManager.OP_SYSTEM_ALERT_WINDOW,
-            AppOpsManager.OP_RECORD_AUDIO,
-            AppOpsManager.OP_COARSE_LOCATION,
-            AppOpsManager.OP_FINE_LOCATION};
+    public static final int[] APP_OPS = new int[] {AppOpsManager.OP_SYSTEM_ALERT_WINDOW};
 
     private final SparseArray<ForegroundServicesUserState> mUserServices = new SparseArray<>();
     private final Object mMutex = new Object();
-    private final NotificationEntryManager mEntryManager;
     private final Handler mMainHandler;
 
     @Inject
-    public ForegroundServiceController(NotificationEntryManager entryManager,
-            AppOpsController appOpsController, @Main Handler mainHandler) {
-        mEntryManager = entryManager;
+    public ForegroundServiceController(AppOpsController appOpsController,
+            @Main Handler mainHandler) {
         mMainHandler = mainHandler;
         appOpsController.addCallback(APP_OPS, (code, uid, packageName, active) -> {
             mMainHandler.post(() -> {
@@ -87,19 +81,6 @@
     }
 
     /**
-     * Returns the keys for notifications from this package using the standard template,
-     * if they exist.
-     */
-    @Nullable
-    public ArraySet<String> getStandardLayoutKeys(int userId, String pkg) {
-        synchronized (mMutex) {
-            final ForegroundServicesUserState services = mUserServices.get(userId);
-            if (services == null) return null;
-            return services.getStandardLayoutKeys(pkg);
-        }
-    }
-
-    /**
      * Gets active app ops for this user and package
      */
     @Nullable
@@ -140,31 +121,6 @@
                 userServices.removeOp(packageName, appOpCode);
             }
         }
-
-        // TODO: (b/145659174) remove when moving to NewNotifPipeline. Replaced by
-        //  AppOpsCoordinator
-        // Update appOps if there are associated pending or visible notifications
-        final Set<String> notificationKeys = getStandardLayoutKeys(userId, packageName);
-        if (notificationKeys != null) {
-            boolean changed = false;
-            for (String key : notificationKeys) {
-                final NotificationEntry entry = mEntryManager.getPendingOrActiveNotif(key);
-                if (entry != null
-                        && uid == entry.getSbn().getUid()
-                        && packageName.equals(entry.getSbn().getPackageName())) {
-                    synchronized (entry.mActiveAppOps) {
-                        if (active) {
-                            changed |= entry.mActiveAppOps.add(appOpCode);
-                        } else {
-                            changed |= entry.mActiveAppOps.remove(appOpCode);
-                        }
-                    }
-                }
-            }
-            if (changed) {
-                mEntryManager.updateNotifications("appOpChanged pkg=" + packageName);
-            }
-        }
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/ForegroundServiceNotificationListener.java b/packages/SystemUI/src/com/android/systemui/ForegroundServiceNotificationListener.java
index bb44583..1515272 100644
--- a/packages/SystemUI/src/com/android/systemui/ForegroundServiceNotificationListener.java
+++ b/packages/SystemUI/src/com/android/systemui/ForegroundServiceNotificationListener.java
@@ -172,24 +172,8 @@
                                     sbn.getPackageName(), sbn.getKey());
                         }
                     }
-                    tagAppOps(entry);
                     return true;
                 },
                 true /* create if not found */);
     }
-
-    // TODO: (b/145659174) remove when moving to NewNotifPipeline. Replaced by
-    //  AppOpsCoordinator
-    private void tagAppOps(NotificationEntry entry) {
-        final StatusBarNotification sbn = entry.getSbn();
-        ArraySet<Integer> activeOps = mForegroundServiceController.getAppOps(
-                sbn.getUserId(),
-                sbn.getPackageName());
-        synchronized (entry.mActiveAppOps) {
-            entry.mActiveAppOps.clear();
-            if (activeOps != null) {
-                entry.mActiveAppOps.addAll(activeOps);
-            }
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index 9c5f9fe..9e9d85a 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -84,7 +84,6 @@
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.internal.statusbar.NotificationVisibility;
 import com.android.systemui.Dumpable;
-import com.android.systemui.bubbles.animation.StackAnimationController;
 import com.android.systemui.bubbles.dagger.BubbleModule;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.model.SysUiState;
@@ -96,6 +95,7 @@
 import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationRemoveInterceptor;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.ScrimView;
 import com.android.systemui.statusbar.notification.NotificationChannelHelper;
 import com.android.systemui.statusbar.notification.NotificationEntryListener;
@@ -109,7 +109,6 @@
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider;
 import com.android.systemui.statusbar.notification.logging.NotificationLogger;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
-import com.android.systemui.statusbar.phone.NotificationShadeWindowController;
 import com.android.systemui.statusbar.phone.ScrimController;
 import com.android.systemui.statusbar.phone.ShadeController;
 import com.android.systemui.statusbar.phone.StatusBar;
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/dagger/BubbleModule.java b/packages/SystemUI/src/com/android/systemui/bubbles/dagger/BubbleModule.java
index 10d301d..fcb215c 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/dagger/BubbleModule.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/dagger/BubbleModule.java
@@ -30,11 +30,11 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
-import com.android.systemui.statusbar.phone.NotificationShadeWindowController;
 import com.android.systemui.statusbar.phone.ShadeController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.ZenModeController;
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java
index 251ce13..4a5d142 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemServicesModule.java
@@ -43,6 +43,7 @@
 import android.hardware.display.DisplayManager;
 import android.media.AudioManager;
 import android.media.MediaRouter2Manager;
+import android.media.session.MediaSessionManager;
 import android.net.ConnectivityManager;
 import android.net.NetworkScoreManager;
 import android.net.wifi.WifiManager;
@@ -226,6 +227,11 @@
     }
 
     @Provides
+    static MediaSessionManager provideMediaSessionManager(Context context) {
+        return context.getSystemService(MediaSessionManager.class);
+    }
+
+    @Provides
     @Singleton
     static NetworkScoreManager provideNetworkScoreManager(Context context) {
         return context.getSystemService(NetworkScoreManager.class);
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIDefaultModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIDefaultModule.java
index 56219c3..b4e6e0e 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIDefaultModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIDefaultModule.java
@@ -45,12 +45,14 @@
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationLockscreenUserManagerImpl;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.phone.DozeServiceHost;
 import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.KeyguardEnvironmentImpl;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
+import com.android.systemui.statusbar.phone.NotificationShadeWindowControllerImpl;
 import com.android.systemui.statusbar.phone.ShadeController;
 import com.android.systemui.statusbar.phone.ShadeControllerImpl;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
@@ -172,5 +174,9 @@
             StatusBarKeyguardViewManager statusBarKeyguardViewManager);
 
     @Binds
+    abstract NotificationShadeWindowController bindNotificationShadeController(
+            NotificationShadeWindowControllerImpl notificationShadeWindowController);
+
+    @Binds
     abstract DozeHost provideDozeHost(DozeServiceHost dozeServiceHost);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
index ef51abb..d213ac1 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
@@ -132,7 +132,7 @@
 import com.android.systemui.plugins.GlobalActionsPanelPlugin;
 import com.android.systemui.settings.CurrentUserContextTracker;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
-import com.android.systemui.statusbar.phone.NotificationShadeWindowController;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.EmergencyDialerConstants;
@@ -148,6 +148,7 @@
 import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
+import javax.inject.Provider;
 
 /**
  * Helper to show the global actions dialog.  Each item is an {@link Action} that may show depending
@@ -401,7 +402,7 @@
                         if (mDialog != null) {
                             if (!mDialog.isShowingControls() && shouldShowControls()) {
                                 mDialog.showControls(mControlsUiControllerOptional.get());
-                            } else if (shouldShowLockMessage()) {
+                            } else if (shouldShowLockMessage(mDialog)) {
                                 mDialog.showLockMessage();
                             }
                         }
@@ -698,19 +699,17 @@
         mPowerAdapter = new MyPowerOptionsAdapter();
 
         mDepthController.setShowingHomeControls(true);
-        GlobalActionsPanelPlugin.PanelViewController walletViewController =
-                getWalletViewController();
         ControlsUiController uiController = null;
         if (mControlsUiControllerOptional.isPresent() && shouldShowControls()) {
             uiController = mControlsUiControllerOptional.get();
         }
         ActionsDialog dialog = new ActionsDialog(mContext, mAdapter, mOverflowAdapter,
-                walletViewController, mDepthController, mSysuiColorExtractor,
+                this::getWalletViewController, mDepthController, mSysuiColorExtractor,
                 mStatusBarService, mNotificationShadeWindowController,
                 controlsAvailable(), uiController,
                 mSysUiState, this::onRotate, mKeyguardShowing, mPowerAdapter);
 
-        if (shouldShowLockMessage()) {
+        if (shouldShowLockMessage(dialog)) {
             dialog.showLockMessage();
         }
         dialog.setCanceledOnTouchOutside(false); // Handled by the custom class.
@@ -2124,7 +2123,8 @@
         private MultiListLayout mGlobalActionsLayout;
         private Drawable mBackgroundDrawable;
         private final SysuiColorExtractor mColorExtractor;
-        private final GlobalActionsPanelPlugin.PanelViewController mWalletViewController;
+        private final Provider<GlobalActionsPanelPlugin.PanelViewController> mWalletFactory;
+        @Nullable private GlobalActionsPanelPlugin.PanelViewController mWalletViewController;
         private boolean mKeyguardShowing;
         private boolean mShowing;
         private float mScrimAlpha;
@@ -2144,7 +2144,7 @@
         private TextView mLockMessage;
 
         ActionsDialog(Context context, MyAdapter adapter, MyOverflowAdapter overflowAdapter,
-                GlobalActionsPanelPlugin.PanelViewController walletViewController,
+                Provider<GlobalActionsPanelPlugin.PanelViewController> walletFactory,
                 NotificationShadeDepthController depthController,
                 SysuiColorExtractor sysuiColorExtractor, IStatusBarService statusBarService,
                 NotificationShadeWindowController notificationShadeWindowController,
@@ -2165,6 +2165,7 @@
             mSysUiState = sysuiState;
             mOnRotateCallback = onRotateCallback;
             mKeyguardShowing = keyguardShowing;
+            mWalletFactory = walletFactory;
 
             // Window initialization
             Window window = getWindow();
@@ -2187,7 +2188,6 @@
             window.getAttributes().setFitInsetsTypes(0 /* types */);
             setTitle(R.string.global_actions);
 
-            mWalletViewController = walletViewController;
             initializeLayout();
         }
 
@@ -2200,8 +2200,13 @@
             mControlsUiController.show(mControlsView, this::dismissForControlsActivity);
         }
 
+        private boolean isWalletViewAvailable() {
+            return mWalletViewController != null && mWalletViewController.getPanelContent() != null;
+        }
+
         private void initializeWalletView() {
-            if (mWalletViewController == null || mWalletViewController.getPanelContent() == null) {
+            mWalletViewController = mWalletFactory.get();
+            if (!isWalletViewAvailable()) {
                 return;
             }
 
@@ -2507,6 +2512,8 @@
         private void dismissWallet() {
             if (mWalletViewController != null) {
                 mWalletViewController.onDismissed();
+                // The wallet controller should not be re-used after being dismissed.
+                mWalletViewController = null;
             }
         }
 
@@ -2648,18 +2655,12 @@
                 && !mControlsServiceInfos.isEmpty();
     }
 
-    private boolean walletViewAvailable() {
-        GlobalActionsPanelPlugin.PanelViewController walletViewController =
-                getWalletViewController();
-        return walletViewController != null && walletViewController.getPanelContent() != null;
-    }
-
-    private boolean shouldShowLockMessage() {
+    private boolean shouldShowLockMessage(ActionsDialog dialog) {
         boolean isLockedAfterBoot = mLockPatternUtils.getStrongAuthForUser(getCurrentUser().id)
                 == STRONG_AUTH_REQUIRED_AFTER_BOOT;
         return !mKeyguardStateController.isUnlocked()
                 && (!mShowLockScreenCardsAndControls || isLockedAfterBoot)
-                && (controlsAvailable() || walletViewAvailable());
+                && (controlsAvailable() || dialog.isWalletViewAvailable());
     }
 
     private void onPowerMenuLockScreenSettingsChanged() {
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
index b993faa..b13be7b 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
@@ -42,7 +42,7 @@
     private val mediaHostStatesManager: MediaHostStatesManager,
     private val activityStarter: ActivityStarter,
     @Main executor: DelayableExecutor,
-    mediaManager: MediaDataFilter,
+    mediaManager: MediaDataManager,
     configurationController: ConfigurationController,
     falsingManager: FalsingManager
 ) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt
index d0642cc..aa3699e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt
@@ -17,65 +17,48 @@
 package com.android.systemui.media
 
 import javax.inject.Inject
-import javax.inject.Singleton
 
 /**
- * Combines updates from [MediaDataManager] with [MediaDeviceManager].
+ * Combines [MediaDataManager.Listener] events with [MediaDeviceManager.Listener] events.
  */
-@Singleton
-class MediaDataCombineLatest @Inject constructor(
-    private val dataSource: MediaDataManager,
-    private val deviceSource: MediaDeviceManager
-) {
+class MediaDataCombineLatest @Inject constructor() : MediaDataManager.Listener,
+        MediaDeviceManager.Listener {
+
     private val listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
     private val entries: MutableMap<String, Pair<MediaData?, MediaDeviceData?>> = mutableMapOf()
 
-    init {
-        dataSource.addListener(object : MediaDataManager.Listener {
-            override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
-                if (oldKey != null && oldKey != key && entries.contains(oldKey)) {
-                    entries[key] = data to entries.remove(oldKey)?.second
-                    update(key, oldKey)
-                } else {
-                    entries[key] = data to entries[key]?.second
-                    update(key, key)
-                }
-            }
-            override fun onMediaDataRemoved(key: String) {
-                remove(key)
-            }
-        })
-        deviceSource.addListener(object : MediaDeviceManager.Listener {
-            override fun onMediaDeviceChanged(
-                key: String,
-                oldKey: String?,
-                data: MediaDeviceData?
-            ) {
-                if (oldKey != null && oldKey != key && entries.contains(oldKey)) {
-                    entries[key] = entries.remove(oldKey)?.first to data
-                    update(key, oldKey)
-                } else {
-                    entries[key] = entries[key]?.first to data
-                    update(key, key)
-                }
-            }
-            override fun onKeyRemoved(key: String) {
-                remove(key)
-            }
-        })
+    override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
+        if (oldKey != null && oldKey != key && entries.contains(oldKey)) {
+            entries[key] = data to entries.remove(oldKey)?.second
+            update(key, oldKey)
+        } else {
+            entries[key] = data to entries[key]?.second
+            update(key, key)
+        }
     }
 
-    /**
-     * Get a map of all non-null data entries
-     */
-    fun getData(): Map<String, MediaData> {
-        return entries.filter {
-            (key, pair) -> pair.first != null && pair.second != null
-        }.mapValues {
-            (key, pair) -> pair.first!!.copy(device = pair.second)
+    override fun onMediaDataRemoved(key: String) {
+        remove(key)
+    }
+
+    override fun onMediaDeviceChanged(
+        key: String,
+        oldKey: String?,
+        data: MediaDeviceData?
+    ) {
+        if (oldKey != null && oldKey != key && entries.contains(oldKey)) {
+            entries[key] = entries.remove(oldKey)?.first to data
+            update(key, oldKey)
+        } else {
+            entries[key] = entries[key]?.first to data
+            update(key, key)
         }
     }
 
+    override fun onKeyRemoved(key: String) {
+        remove(key)
+    }
+
     /**
      * Add a listener for [MediaData] changes that has been combined with latest [MediaDeviceData].
      */
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt
index 24ca970..0664a41 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataFilter.kt
@@ -24,7 +24,6 @@
 import com.android.systemui.statusbar.NotificationLockscreenUserManager
 import java.util.concurrent.Executor
 import javax.inject.Inject
-import javax.inject.Singleton
 
 private const val TAG = "MediaDataFilter"
 private const val DEBUG = true
@@ -33,24 +32,24 @@
  * Filters data updates from [MediaDataCombineLatest] based on the current user ID, and handles user
  * switches (removing entries for the previous user, adding back entries for the current user)
  *
- * This is added downstream of [MediaDataManager] since we may still need to handle callbacks from
- * background users (e.g. timeouts) that UI classes should ignore.
- * Instead, UI classes should listen to this so they can stay in sync with the current user.
+ * This is added at the end of the pipeline since we may still need to handle callbacks from
+ * background users (e.g. timeouts).
  */
-@Singleton
 class MediaDataFilter @Inject constructor(
-    private val dataSource: MediaDataCombineLatest,
     private val broadcastDispatcher: BroadcastDispatcher,
     private val mediaResumeListener: MediaResumeListener,
-    private val mediaDataManager: MediaDataManager,
     private val lockscreenUserManager: NotificationLockscreenUserManager,
     @Main private val executor: Executor
 ) : MediaDataManager.Listener {
     private val userTracker: CurrentUserTracker
-    private val listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
+    private val _listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
+    internal val listeners: Set<MediaDataManager.Listener>
+        get() = _listeners.toSet()
+    internal lateinit var mediaDataManager: MediaDataManager
 
-    // The filtered mediaEntries, which will be a subset of all mediaEntries in MediaDataManager
-    private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
+    private val allEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
+    // The filtered userEntries, which will be a subset of all userEntries in MediaDataManager
+    private val userEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
 
     init {
         userTracker = object : CurrentUserTracker(broadcastDispatcher) {
@@ -60,31 +59,34 @@
             }
         }
         userTracker.startTracking()
-        dataSource.addListener(this)
     }
 
     override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
+        if (oldKey != null && oldKey != key) {
+            allEntries.remove(oldKey)
+        }
+        allEntries.put(key, data)
+
         if (!lockscreenUserManager.isCurrentProfile(data.userId)) {
             return
         }
 
-        if (oldKey != null) {
-            mediaEntries.remove(oldKey)
+        if (oldKey != null && oldKey != key) {
+            userEntries.remove(oldKey)
         }
-        mediaEntries.put(key, data)
+        userEntries.put(key, data)
 
         // Notify listeners
-        val listenersCopy = listeners.toSet()
-        listenersCopy.forEach {
+        listeners.forEach {
             it.onMediaDataLoaded(key, oldKey, data)
         }
     }
 
     override fun onMediaDataRemoved(key: String) {
-        mediaEntries.remove(key)?.let {
+        allEntries.remove(key)
+        userEntries.remove(key)?.let {
             // Only notify listeners if something actually changed
-            val listenersCopy = listeners.toSet()
-            listenersCopy.forEach {
+            listeners.forEach {
                 it.onMediaDataRemoved(key)
             }
         }
@@ -93,11 +95,11 @@
     @VisibleForTesting
     internal fun handleUserSwitched(id: Int) {
         // If the user changes, remove all current MediaData objects and inform listeners
-        val listenersCopy = listeners.toSet()
-        val keyCopy = mediaEntries.keys.toMutableList()
+        val listenersCopy = listeners
+        val keyCopy = userEntries.keys.toMutableList()
         // Clear the list first, to make sure callbacks from listeners if we have any entries
         // are up to date
-        mediaEntries.clear()
+        userEntries.clear()
         keyCopy.forEach {
             if (DEBUG) Log.d(TAG, "Removing $it after user change")
             listenersCopy.forEach { listener ->
@@ -105,10 +107,10 @@
             }
         }
 
-        dataSource.getData().forEach { (key, data) ->
+        allEntries.forEach { (key, data) ->
             if (lockscreenUserManager.isCurrentProfile(data.userId)) {
                 if (DEBUG) Log.d(TAG, "Re-adding $key after user change")
-                mediaEntries.put(key, data)
+                userEntries.put(key, data)
                 listenersCopy.forEach { listener ->
                     listener.onMediaDataLoaded(key, null, data)
                 }
@@ -121,7 +123,7 @@
      */
     fun onSwipeToDismiss() {
         if (DEBUG) Log.d(TAG, "Media carousel swiped away")
-        val mediaKeys = mediaEntries.keys.toSet()
+        val mediaKeys = userEntries.keys.toSet()
         mediaKeys.forEach {
             mediaDataManager.setTimedOut(it, timedOut = true)
         }
@@ -130,7 +132,7 @@
     /**
      * Are there any media notifications active?
      */
-    fun hasActiveMedia() = mediaEntries.any { it.value.active }
+    fun hasActiveMedia() = userEntries.any { it.value.active }
 
     /**
      * Are there any media entries we should display?
@@ -138,7 +140,7 @@
      * If resumption is disabled, we only want to show active players
      */
     fun hasAnyMedia() = if (mediaResumeListener.isResumptionEnabled()) {
-        mediaEntries.isNotEmpty()
+        userEntries.isNotEmpty()
     } else {
         hasActiveMedia()
     }
@@ -146,10 +148,10 @@
     /**
      * Add a listener for filtered [MediaData] changes
      */
-    fun addListener(listener: MediaDataManager.Listener) = listeners.add(listener)
+    fun addListener(listener: MediaDataManager.Listener) = _listeners.add(listener)
 
     /**
      * Remove a listener that was registered with addListener
      */
-    fun removeListener(listener: MediaDataManager.Listener) = listeners.remove(listener)
-}
\ No newline at end of file
+    fun removeListener(listener: MediaDataManager.Listener) = _listeners.remove(listener)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
index bff334e..e239ba9 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
@@ -101,12 +101,23 @@
     dumpManager: DumpManager,
     mediaTimeoutListener: MediaTimeoutListener,
     mediaResumeListener: MediaResumeListener,
+    mediaSessionBasedFilter: MediaSessionBasedFilter,
+    mediaDeviceManager: MediaDeviceManager,
+    mediaDataCombineLatest: MediaDataCombineLatest,
+    private val mediaDataFilter: MediaDataFilter,
     private val activityStarter: ActivityStarter,
     private var useMediaResumption: Boolean,
     private val useQsMediaPlayer: Boolean
 ) : Dumpable {
 
-    private val listeners: MutableSet<Listener> = mutableSetOf()
+    // Internal listeners are part of the internal pipeline. External listeners (those registered
+    // with [MediaDeviceManager.addListener]) receive events after they have propagated through
+    // the internal pipeline.
+    // Another way to think of the distinction between internal and external listeners is the
+    // following. Internal listeners are listeners that MediaDataManager depends on, and external
+    // listeners are listeners that depend on MediaDataManager.
+    // TODO(b/159539991#comment5): Move internal listeners to separate package.
+    private val internalListeners: MutableSet<Listener> = mutableSetOf()
     private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
     internal var appsBlockedFromResume: MutableSet<String> = Utils.getBlockedMediaApps(context)
         set(value) {
@@ -130,9 +141,14 @@
         broadcastDispatcher: BroadcastDispatcher,
         mediaTimeoutListener: MediaTimeoutListener,
         mediaResumeListener: MediaResumeListener,
+        mediaSessionBasedFilter: MediaSessionBasedFilter,
+        mediaDeviceManager: MediaDeviceManager,
+        mediaDataCombineLatest: MediaDataCombineLatest,
+        mediaDataFilter: MediaDataFilter,
         activityStarter: ActivityStarter
     ) : this(context, backgroundExecutor, foregroundExecutor, mediaControllerFactory,
             broadcastDispatcher, dumpManager, mediaTimeoutListener, mediaResumeListener,
+            mediaSessionBasedFilter, mediaDeviceManager, mediaDataCombineLatest, mediaDataFilter,
             activityStarter, Utils.useMediaResumption(context), Utils.useQsMediaPlayer(context))
 
     private val appChangeReceiver = object : BroadcastReceiver() {
@@ -155,12 +171,26 @@
 
     init {
         dumpManager.registerDumpable(TAG, this)
+
+        // Initialize the internal processing pipeline. The listeners at the front of the pipeline
+        // are set as internal listeners so that they receive events. From there, events are
+        // propagated through the pipeline. The end of the pipeline is currently mediaDataFilter,
+        // so it is responsible for dispatching events to external listeners. To achieve this,
+        // external listeners that are registered with [MediaDataManager.addListener] are actually
+        // registered as listeners to mediaDataFilter.
+        addInternalListener(mediaTimeoutListener)
+        addInternalListener(mediaResumeListener)
+        addInternalListener(mediaSessionBasedFilter)
+        mediaSessionBasedFilter.addListener(mediaDeviceManager)
+        mediaSessionBasedFilter.addListener(mediaDataCombineLatest)
+        mediaDeviceManager.addListener(mediaDataCombineLatest)
+        mediaDataCombineLatest.addListener(mediaDataFilter)
+
+        // Set up links back into the pipeline for listeners that need to send events upstream.
         mediaTimeoutListener.timeoutCallback = { token: String, timedOut: Boolean ->
             setTimedOut(token, timedOut) }
-        addListener(mediaTimeoutListener)
-
         mediaResumeListener.setManager(this)
-        addListener(mediaResumeListener)
+        mediaDataFilter.mediaDataManager = this
 
         val suspendFilter = IntentFilter(Intent.ACTION_PACKAGES_SUSPENDED)
         broadcastDispatcher.registerReceiver(appChangeReceiver, suspendFilter, null, UserHandle.ALL)
@@ -198,10 +228,9 @@
 
     private fun removeAllForPackage(packageName: String) {
         Assert.isMainThread()
-        val listenersCopy = listeners.toSet()
         val toRemove = mediaEntries.filter { it.value.packageName == packageName }
         toRemove.forEach {
-            removeEntry(it.key, listenersCopy)
+            removeEntry(it.key)
         }
     }
 
@@ -261,12 +290,45 @@
     /**
      * Add a listener for changes in this class
      */
-    fun addListener(listener: Listener) = listeners.add(listener)
+    fun addListener(listener: Listener) {
+        // mediaDataFilter is the current end of the internal pipeline. Register external
+        // listeners as listeners to it.
+        mediaDataFilter.addListener(listener)
+    }
 
     /**
      * Remove a listener for changes in this class
      */
-    fun removeListener(listener: Listener) = listeners.remove(listener)
+    fun removeListener(listener: Listener) {
+        // Since mediaDataFilter is the current end of the internal pipelie, external listeners
+        // have been registered to it. So, they need to be removed from it too.
+        mediaDataFilter.removeListener(listener)
+    }
+
+    /**
+     * Add a listener for internal events.
+     */
+    private fun addInternalListener(listener: Listener) = internalListeners.add(listener)
+
+    /**
+     * Notify internal listeners of loaded event.
+     *
+     * External listeners registered with [addListener] will be notified after the event propagates
+     * through the internal listener pipeline.
+     */
+    private fun notifyMediaDataLoaded(key: String, oldKey: String?, info: MediaData) {
+        internalListeners.forEach { it.onMediaDataLoaded(key, oldKey, info) }
+    }
+
+    /**
+     * Notify internal listeners of removed event.
+     *
+     * External listeners registered with [addListener] will be notified after the event propagates
+     * through the internal listener pipeline.
+     */
+    private fun notifyMediaDataRemoved(key: String) {
+        internalListeners.forEach { it.onMediaDataRemoved(key) }
+    }
 
     /**
      * Called whenever the player has been paused or stopped for a while, or swiped from QQS.
@@ -284,16 +346,13 @@
         }
     }
 
-    private fun removeEntry(key: String, listenersCopy: Set<Listener>) {
+    private fun removeEntry(key: String) {
         mediaEntries.remove(key)
-        listenersCopy.forEach {
-            it.onMediaDataRemoved(key)
-        }
+        notifyMediaDataRemoved(key)
     }
 
     fun dismissMediaData(key: String, delay: Long) {
-        val listenersCopy = listeners.toSet()
-        foregroundExecutor.executeDelayed({ removeEntry(key, listenersCopy) }, delay)
+        foregroundExecutor.executeDelayed({ removeEntry(key) }, delay)
     }
 
     private fun loadMediaDataInBgForResumption(
@@ -422,7 +481,7 @@
                 val runnable = if (action.actionIntent != null) {
                     Runnable {
                         if (action.isAuthenticationRequired()) {
-                            activityStarter.dismissKeyguardThenExecute ({
+                            activityStarter.dismissKeyguardThenExecute({
                                 var result = sendPendingIntent(action.actionIntent)
                                 result
                             }, {}, true)
@@ -550,10 +609,7 @@
         if (mediaEntries.containsKey(key)) {
             // Otherwise this was removed already
             mediaEntries.put(key, data)
-            val listenersCopy = listeners.toSet()
-            listenersCopy.forEach {
-                it.onMediaDataLoaded(key, oldKey, data)
-            }
+            notifyMediaDataLoaded(key, oldKey, data)
         }
     }
 
@@ -570,31 +626,21 @@
             val pkg = removed?.packageName
             val migrate = mediaEntries.put(pkg, updated) == null
             // Notify listeners of "new" controls when migrating or removed and update when not
-            val listenersCopy = listeners.toSet()
             if (migrate) {
-                listenersCopy.forEach {
-                    it.onMediaDataLoaded(pkg, key, updated)
-                }
+                notifyMediaDataLoaded(pkg, key, updated)
             } else {
                 // Since packageName is used for the key of the resumption controls, it is
                 // possible that another notification has already been reused for the resumption
                 // controls of this package. In this case, rather than renaming this player as
                 // packageName, just remove it and then send a update to the existing resumption
                 // controls.
-                listenersCopy.forEach {
-                    it.onMediaDataRemoved(key)
-                }
-                listenersCopy.forEach {
-                    it.onMediaDataLoaded(pkg, pkg, updated)
-                }
+                notifyMediaDataRemoved(key)
+                notifyMediaDataLoaded(pkg, pkg, updated)
             }
             return
         }
         if (removed != null) {
-            val listenersCopy = listeners.toSet()
-            listenersCopy.forEach {
-                it.onMediaDataRemoved(key)
-            }
+            notifyMediaDataRemoved(key)
         }
     }
 
@@ -614,17 +660,31 @@
 
         if (!useMediaResumption) {
             // Remove any existing resume controls
-            val listenersCopy = listeners.toSet()
             val filtered = mediaEntries.filter { !it.value.active }
             filtered.forEach {
                 mediaEntries.remove(it.key)
-                listenersCopy.forEach { listener ->
-                    listener.onMediaDataRemoved(it.key)
-                }
+                notifyMediaDataRemoved(it.key)
             }
         }
     }
 
+    /**
+     * Invoked when the user has dismissed the media carousel
+     */
+    fun onSwipeToDismiss() = mediaDataFilter.onSwipeToDismiss()
+
+    /**
+     * Are there any media notifications active?
+     */
+    fun hasActiveMedia() = mediaDataFilter.hasActiveMedia()
+
+    /**
+     * Are there any media entries we should display?
+     * If resumption is enabled, this will include inactive players
+     * If resumption is disabled, we only want to show active players
+     */
+    fun hasAnyMedia() = mediaDataFilter.hasAnyMedia()
+
     interface Listener {
 
         /**
@@ -644,7 +704,8 @@
 
     override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) {
         pw.apply {
-            println("listeners: $listeners")
+            println("internalListeners: $internalListeners")
+            println("externalListeners: ${mediaDataFilter.listeners}")
             println("mediaEntries: $mediaEntries")
             println("useMediaResumption: $useMediaResumption")
             println("appsBlockedFromResume: $appsBlockedFromResume")
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
index ae7f66b..102a484 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
@@ -32,26 +32,23 @@
 import java.io.PrintWriter
 import java.util.concurrent.Executor
 import javax.inject.Inject
-import javax.inject.Singleton
 
 /**
  * Provides information about the route (ie. device) where playback is occurring.
  */
-@Singleton
 class MediaDeviceManager @Inject constructor(
     private val context: Context,
     private val localMediaManagerFactory: LocalMediaManagerFactory,
     private val mr2manager: MediaRouter2Manager,
     @Main private val fgExecutor: Executor,
     @Background private val bgExecutor: Executor,
-    private val mediaDataManager: MediaDataManager,
-    private val dumpManager: DumpManager
+    dumpManager: DumpManager
 ) : MediaDataManager.Listener, Dumpable {
+
     private val listeners: MutableSet<Listener> = mutableSetOf()
     private val entries: MutableMap<String, Entry> = mutableMapOf()
 
     init {
-        mediaDataManager.addListener(this)
         dumpManager.registerDumpable(javaClass.name, this)
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
index 3598719..ce184aa 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
@@ -14,7 +14,7 @@
 class MediaHost @Inject constructor(
     private val state: MediaHostStateHolder,
     private val mediaHierarchyManager: MediaHierarchyManager,
-    private val mediaDataFilter: MediaDataFilter,
+    private val mediaDataManager: MediaDataManager,
     private val mediaHostStatesManager: MediaHostStatesManager
 ) : MediaHostState by state {
     lateinit var hostView: UniqueObjectHostView
@@ -79,12 +79,12 @@
                 // be a delay until the views and the controllers are initialized, leaving us
                 // with either a blank view or the controllers not yet initialized and the
                 // measuring wrong
-                mediaDataFilter.addListener(listener)
+                mediaDataManager.addListener(listener)
                 updateViewVisibility()
             }
 
             override fun onViewDetachedFromWindow(v: View?) {
-                mediaDataFilter.removeListener(listener)
+                mediaDataManager.removeListener(listener)
             }
         })
 
@@ -113,9 +113,9 @@
 
     private fun updateViewVisibility() {
         visible = if (showsOnlyActiveMedia) {
-            mediaDataFilter.hasActiveMedia()
+            mediaDataManager.hasActiveMedia()
         } else {
-            mediaDataFilter.hasAnyMedia()
+            mediaDataManager.hasAnyMedia()
         }
         val newVisibility = if (visible) View.VISIBLE else View.GONE
         if (newVisibility != hostView.visibility) {
@@ -289,4 +289,4 @@
      * Get a copy of this view state, deepcopying all appropriate members
      */
     fun copy(): MediaHostState
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaSessionBasedFilter.kt b/packages/SystemUI/src/com/android/systemui/media/MediaSessionBasedFilter.kt
new file mode 100644
index 0000000..f01713f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaSessionBasedFilter.kt
@@ -0,0 +1,163 @@
+/*
+ * 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.media
+
+import android.content.ComponentName
+import android.content.Context
+import android.media.session.MediaController
+import android.media.session.MediaController.PlaybackInfo
+import android.media.session.MediaSession
+import android.media.session.MediaSessionManager
+import android.util.Log
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.statusbar.phone.NotificationListenerWithPlugins
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+private const val TAG = "MediaSessionBasedFilter"
+
+/**
+ * Filters media loaded events for local media sessions while an app is casting.
+ *
+ * When an app is casting there can be one remote media sessions and potentially more local media
+ * sessions. In this situation, there should only be a media object for the remote session. To
+ * achieve this, update events for the local session need to be filtered.
+ */
+class MediaSessionBasedFilter @Inject constructor(
+    context: Context,
+    private val sessionManager: MediaSessionManager,
+    @Main private val foregroundExecutor: Executor,
+    @Background private val backgroundExecutor: Executor
+) : MediaDataManager.Listener {
+
+    private val listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf()
+
+    // Keep track of MediaControllers for a given package to check if an app is casting and it
+    // filter loaded events for local sessions.
+    private val packageControllers: LinkedHashMap<String, MutableList<MediaController>> =
+            LinkedHashMap()
+
+    // Keep track of the key used for the session tokens. This information is used to know when
+    // dispatch a removed event so that a media object for a local session will be removed.
+    private val keyedTokens: MutableMap<String, MutableList<MediaSession.Token>> = mutableMapOf()
+
+    private val sessionListener = object : MediaSessionManager.OnActiveSessionsChangedListener {
+        override fun onActiveSessionsChanged(controllers: List<MediaController>) {
+            handleControllersChanged(controllers)
+        }
+    }
+
+    init {
+        backgroundExecutor.execute {
+            val name = ComponentName(context, NotificationListenerWithPlugins::class.java)
+            sessionManager.addOnActiveSessionsChangedListener(sessionListener, name)
+            handleControllersChanged(sessionManager.getActiveSessions(name))
+        }
+    }
+
+    /**
+     * Add a listener for filtered [MediaData] changes
+     */
+    fun addListener(listener: MediaDataManager.Listener) = listeners.add(listener)
+
+    /**
+     * Remove a listener that was registered with addListener
+     */
+    fun removeListener(listener: MediaDataManager.Listener) = listeners.remove(listener)
+
+    /**
+     * May filter loaded events by not passing them along to listeners.
+     *
+     * If an app has only one session with playback type PLAYBACK_TYPE_REMOTE, then assuming that
+     * the app is casting. Sometimes apps will send redundant updates to a local session with
+     * playback type PLAYBACK_TYPE_LOCAL. These updates should be filtered to improve the usability
+     * of the media controls.
+     */
+    override fun onMediaDataLoaded(key: String, oldKey: String?, info: MediaData) {
+        backgroundExecutor.execute {
+            val isMigration = oldKey != null && key != oldKey
+            if (isMigration) {
+                keyedTokens.remove(oldKey)?.let { removed -> keyedTokens.put(key, removed) }
+            }
+            if (info.token != null) {
+                keyedTokens.get(key)?.let {
+                    tokens ->
+                    tokens.add(info.token)
+                } ?: run {
+                    val tokens = mutableListOf(info.token)
+                    keyedTokens.put(key, tokens)
+                }
+            }
+            // Determine if an app is casting by checking if it has a session with playback type
+            // PLAYBACK_TYPE_REMOTE.
+            val remoteControllers = packageControllers.get(info.packageName)?.filter {
+                it.playbackInfo?.playbackType == PlaybackInfo.PLAYBACK_TYPE_REMOTE
+            }
+            // Limiting search to only apps with a single remote session.
+            val remote = if (remoteControllers?.size == 1) remoteControllers.firstOrNull() else null
+            if (isMigration || remote == null || remote.sessionToken == info.token) {
+                // Not filtering in this case. Passing the event along to listeners.
+                dispatchMediaDataLoaded(key, oldKey, info)
+            } else {
+                // Filtering this event because the app is casting and the loaded events is for a
+                // local session.
+                Log.d(TAG, "filtering key=$key local=${info.token} remote=${remote?.sessionToken}")
+                // If the local session uses a different notification key, then lets go a step
+                // farther and dismiss the media data so that media controls for the local session
+                // don't hang around while casting.
+                if (!keyedTokens.get(key)!!.contains(remote.sessionToken)) {
+                    dispatchMediaDataRemoved(key)
+                }
+            }
+        }
+    }
+
+    override fun onMediaDataRemoved(key: String) {
+        // Queue on background thread to ensure ordering of loaded and removed events is maintained.
+        backgroundExecutor.execute {
+            keyedTokens.remove(key)
+            dispatchMediaDataRemoved(key)
+        }
+    }
+
+    private fun dispatchMediaDataLoaded(key: String, oldKey: String?, info: MediaData) {
+        foregroundExecutor.execute {
+            listeners.toSet().forEach { it.onMediaDataLoaded(key, oldKey, info) }
+        }
+    }
+
+    private fun dispatchMediaDataRemoved(key: String) {
+        foregroundExecutor.execute {
+            listeners.toSet().forEach { it.onMediaDataRemoved(key) }
+        }
+    }
+
+    private fun handleControllersChanged(controllers: List<MediaController>) {
+        packageControllers.clear()
+        controllers.forEach {
+            controller ->
+            packageControllers.get(controller.packageName)?.let {
+                tokens ->
+                tokens.add(controller)
+            } ?: run {
+                val tokens = mutableListOf(controller)
+                packageControllers.put(controller.packageName, tokens)
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index ecdd778..d222489 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -87,7 +87,7 @@
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.stackdivider.Divider;
 import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.phone.NotificationShadeWindowController;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.phone.StatusBar;
 import com.android.systemui.statusbar.phone.StatusBarWindowCallback;
 import com.android.systemui.statusbar.policy.CallbackController;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
index 739d30c..c01bdc4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
@@ -60,7 +60,6 @@
 import com.android.systemui.statusbar.phone.BiometricUnlockController;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.LockscreenWallpaper;
-import com.android.systemui.statusbar.phone.NotificationShadeWindowController;
 import com.android.systemui.statusbar.phone.ScrimController;
 import com.android.systemui.statusbar.phone.ScrimState;
 import com.android.systemui.statusbar.phone.StatusBar;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
index 0445c98..cdf1f10 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt
@@ -38,7 +38,6 @@
 import com.android.systemui.statusbar.phone.BiometricUnlockController
 import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK
 import com.android.systemui.statusbar.phone.DozeParameters
-import com.android.systemui.statusbar.phone.NotificationShadeWindowController
 import com.android.systemui.statusbar.phone.PanelExpansionListener
 import com.android.systemui.statusbar.phone.ScrimController
 import com.android.systemui.statusbar.policy.KeyguardStateController
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
new file mode 100644
index 0000000..1fd0b03
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeWindowController.java
@@ -0,0 +1,184 @@
+/*
+ * 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;
+
+import android.view.ViewGroup;
+
+import androidx.annotation.Nullable;
+
+import com.android.systemui.statusbar.phone.StatusBarWindowCallback;
+
+import java.util.function.Consumer;
+
+/**
+ * Interface to control the state of the notification shade window. Not all methods of this
+ * interface will be used by each implementation of {@link NotificationShadeWindowController}.
+ */
+public interface NotificationShadeWindowController extends RemoteInputController.Callback {
+
+    /**
+     * Registers a {@link StatusBarWindowCallback} to receive notifications about status bar
+     * window state changes.
+     */
+    default void registerCallback(StatusBarWindowCallback callback) {}
+
+    /** Notifies the registered {@link StatusBarWindowCallback} instances. */
+    default void notifyStateChangedCallbacks() {}
+
+    /**
+     * Registers a listener to monitor scrims visibility.
+     *
+     * @param listener A listener to monitor scrims visibility
+     */
+    default void setScrimsVisibilityListener(Consumer<Integer> listener) {}
+
+    /**
+     * Adds the notification shade view to the window manager.
+     */
+    default void attach() {}
+
+    /** Sets the notification shade view. */
+    default void setNotificationShadeView(ViewGroup view) {}
+
+    /** Gets the notification shade view. */
+    @Nullable
+    default ViewGroup getNotificationShadeView() {
+        return null;
+    }
+
+    /** Sets the state of whether the keyguard is currently showing or not. */
+    default void setKeyguardShowing(boolean showing) {}
+
+    /** Sets the state of whether the keyguard is currently occluded or not. */
+    default void setKeyguardOccluded(boolean occluded) {}
+
+    /** Sets the state of whether the keyguard is currently needs input or not. */
+    default void setKeyguardNeedsInput(boolean needsInput) {}
+
+    /** Sets the state of whether the notification shade panel is currently visible or not. */
+    default void setPanelVisible(boolean visible) {}
+
+    /** Sets the state of whether the notification shade is focusable or not. */
+    default void setNotificationShadeFocusable(boolean focusable) {}
+
+    /** Sets the state of whether the bouncer is showing or not. */
+    default void setBouncerShowing(boolean showing) {}
+
+    /** Sets the state of whether the backdrop is showing or not. */
+    default void setBackdropShowing(boolean showing) {}
+
+    /** Sets the state of whether the keyguard is fading away or not. */
+    default void setKeyguardFadingAway(boolean keyguardFadingAway) {}
+
+    /** Sets the state of whether the quick settings is expanded or not. */
+    default void setQsExpanded(boolean expanded) {}
+
+    /** Sets the state of whether the user activities are forced or not. */
+    default void setForceUserActivity(boolean forceUserActivity) {}
+
+    /** Sets the state of whether the user activities are forced or not. */
+    default void setLaunchingActivity(boolean launching) {}
+
+    /** Sets the state of whether the scrim is visible or not. */
+    default void setScrimsVisibility(int scrimsVisibility) {}
+
+    /** Sets the background blur radius of the notification shade window. */
+    default void setBackgroundBlurRadius(int backgroundBlurRadius) {}
+
+    /** Sets the state of whether heads up is showing or not. */
+    default void setHeadsUpShowing(boolean showing) {}
+
+    /** Sets whether the wallpaper supports ambient mode or not. */
+    default void setWallpaperSupportsAmbientMode(boolean supportsAmbientMode) {}
+
+    /** Gets whether the wallpaper is showing or not. */
+    default boolean isShowingWallpaper() {
+        return false;
+    }
+
+    /** Sets whether the window was collapsed by force or not. */
+    default void setForceWindowCollapsed(boolean force) {}
+
+    /** Sets whether panel is expanded or not. */
+    default void setPanelExpanded(boolean isExpanded) {}
+
+    /** Gets whether the panel is expanded or not. */
+    default boolean getPanelExpanded() {
+        return false;
+    }
+
+    /** Sets the state of whether the remote input is active or not. */
+    default void onRemoteInputActive(boolean remoteInputActive) {}
+
+    /** Sets the screen brightness level for when the device is dozing. */
+    default void setDozeScreenBrightness(int value) {}
+
+    /**
+     * Sets whether the screen brightness is forced to the value we use for doze mode by the status
+     * bar window. No-op if the device does not support dozing.
+     */
+    default void setForceDozeBrightness(boolean forceDozeBrightness) {}
+
+    /** Sets the state of whether sysui is dozing or not. */
+    default void setDozing(boolean dozing) {}
+
+    /** Sets the state of whether plugin open is forced or not. */
+    default void setForcePluginOpen(boolean forcePluginOpen) {}
+
+    /** Gets whether we are forcing plugin open or not. */
+    default boolean getForcePluginOpen() {
+        return false;
+    }
+
+    /** Sets the state of whether the notification shade is touchable or not. */
+    default void setNotTouchable(boolean notTouchable) {}
+
+    /** Sets a {@link OtherwisedCollapsedListener}. */
+    default void setStateListener(OtherwisedCollapsedListener listener) {}
+
+    /** Sets a {@link ForcePluginOpenListener}. */
+    default void setForcePluginOpenListener(ForcePluginOpenListener listener) {}
+
+    /** Sets whether the system is in a state where the keyguard is going away. */
+    default void setKeyguardGoingAway(boolean goingAway) {}
+
+    /**
+     * SystemUI may need top-ui to avoid jank when performing animations.  After the
+     * animation is performed, the component should remove itself from the list of features that
+     * are forcing SystemUI to be top-ui.
+     */
+    default void setRequestTopUi(boolean requestTopUi, String componentTag) {}
+
+    /**
+     * Custom listener to pipe data back to plugins about whether or not the status bar would be
+     * collapsed if not for the plugin.
+     * TODO: Find cleaner way to do this.
+     */
+    interface OtherwisedCollapsedListener {
+        void setWouldOtherwiseCollapse(boolean otherwiseCollapse);
+    }
+
+    /**
+     * Listener to indicate forcePluginOpen has changed
+     */
+    interface ForcePluginOpenListener {
+        /**
+         * Called when mState.forcePluginOpen is changed
+         */
+        void onChange(boolean forceOpen);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java
index 02a8fee..f272be4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java
@@ -491,7 +491,6 @@
                 }
             }
 
-            row.showAppOpsIcons(entry.mActiveAppOps);
             row.showFeedbackIcon(mAssistantFeedbackController.showFeedbackIndicator(entry));
             row.setLastAudiblyAlertedMs(entry.getLastAudiblyAlertedMs());
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
index 992f2da..991b79a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
@@ -33,6 +33,7 @@
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.NotificationViewHierarchyManager;
 import com.android.systemui.statusbar.SmartReplyController;
 import com.android.systemui.statusbar.notification.AssistantFeedbackController;
@@ -44,7 +45,6 @@
 import com.android.systemui.statusbar.notification.stack.ForegroundServiceSectionController;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
-import com.android.systemui.statusbar.phone.NotificationShadeWindowController;
 import com.android.systemui.statusbar.phone.StatusBar;
 import com.android.systemui.statusbar.policy.RemoteInputUriController;
 import com.android.systemui.tracing.ProtoTracer;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/AppOpsCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/AppOpsCoordinator.java
index 68ec6b6..2f12088 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/AppOpsCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/AppOpsCoordinator.java
@@ -82,14 +82,9 @@
         // extend the lifetime of foreground notification services to show for at least 5 seconds
         mNotifPipeline.addNotificationLifetimeExtender(mForegroundLifetimeExtender);
 
-        // listen for new notifications to add appOps
-        mNotifPipeline.addCollectionListener(mNotifCollectionListener);
-
         // filter out foreground service notifications that aren't necessary anymore
         mNotifPipeline.addPreGroupFilter(mNotifFilter);
 
-        // when appOps change, update any relevant notifications to update appOps for
-        mAppOpsController.addCallback(ForegroundServiceController.APP_OPS, this::onAppOpsChanged);
     }
 
     public NotifSection getSection() {
@@ -186,35 +181,6 @@
     };
 
     /**
-     * Adds appOps to incoming and updating notifications
-     */
-    private NotifCollectionListener mNotifCollectionListener = new NotifCollectionListener() {
-        @Override
-        public void onEntryAdded(NotificationEntry entry) {
-            tagAppOps(entry);
-        }
-
-        @Override
-        public void onEntryUpdated(NotificationEntry entry) {
-            tagAppOps(entry);
-        }
-
-        private void tagAppOps(NotificationEntry entry) {
-            final StatusBarNotification sbn = entry.getSbn();
-            // note: requires that the ForegroundServiceController is updating their appOps first
-            ArraySet<Integer> activeOps =
-                    mForegroundServiceController.getAppOps(
-                            sbn.getUser().getIdentifier(),
-                            sbn.getPackageName());
-
-            entry.mActiveAppOps.clear();
-            if (activeOps != null) {
-                entry.mActiveAppOps.addAll(activeOps);
-            }
-        }
-    };
-
-    /**
      * Puts foreground service notifications into its own section.
      */
     private final NotifSection mNotifSection = new NotifSection("ForegroundService") {
@@ -230,53 +196,4 @@
             return false;
         }
     };
-
-    private void onAppOpsChanged(int code, int uid, String packageName, boolean active) {
-        mMainExecutor.execute(() -> handleAppOpsChanged(code, uid, packageName, active));
-    }
-
-    /**
-     * Update the appOp for the posted notification associated with the current foreground service
-     *
-     * @param code code for appOp to add/remove
-     * @param uid of user the notification is sent to
-     * @param packageName package that created the notification
-     * @param active whether the appOpCode is active or not
-     */
-    private void handleAppOpsChanged(int code, int uid, String packageName, boolean active) {
-        Assert.isMainThread();
-
-        int userId = UserHandle.getUserId(uid);
-
-        // Update appOps of the app's posted notifications with standard layouts
-        final ArraySet<String> notifKeys =
-                mForegroundServiceController.getStandardLayoutKeys(userId, packageName);
-        if (notifKeys != null) {
-            boolean changed = false;
-            for (int i = 0; i < notifKeys.size(); i++) {
-                final NotificationEntry entry = findNotificationEntryWithKey(notifKeys.valueAt(i));
-                if (entry != null
-                        && uid == entry.getSbn().getUid()
-                        && packageName.equals(entry.getSbn().getPackageName())) {
-                    if (active) {
-                        changed |= entry.mActiveAppOps.add(code);
-                    } else {
-                        changed |= entry.mActiveAppOps.remove(code);
-                    }
-                }
-            }
-            if (changed) {
-                mNotifFilter.invalidateList();
-            }
-        }
-    }
-
-    private NotificationEntry findNotificationEntryWithKey(String key) {
-        for (NotificationEntry entry : mNotifPipeline.getAllNotifs()) {
-            if (entry.getKey().equals(key)) {
-                return entry;
-            }
-        }
-        return null;
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/AppOpsInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/AppOpsInfo.java
deleted file mode 100644
index 28c53dc..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/AppOpsInfo.java
+++ /dev/null
@@ -1,214 +0,0 @@
-/*
- * Copyright (C) 2018 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.row;
-
-import android.app.AppOpsManager;
-import android.content.Context;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.graphics.drawable.Drawable;
-import android.service.notification.StatusBarNotification;
-import android.util.ArraySet;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.accessibility.AccessibilityEvent;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import com.android.internal.logging.MetricsLogger;
-import com.android.internal.logging.UiEventLogger;
-import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-import com.android.systemui.R;
-
-/**
- * The guts of a notification revealed when performing a long press.
- */
-public class AppOpsInfo extends LinearLayout implements NotificationGuts.GutsContent {
-    private static final String TAG = "AppOpsGuts";
-
-    private PackageManager mPm;
-
-    private String mPkg;
-    private String mAppName;
-    private int mAppUid;
-    private StatusBarNotification mSbn;
-    private ArraySet<Integer> mAppOps;
-    private MetricsLogger mMetricsLogger;
-    private OnSettingsClickListener mOnSettingsClickListener;
-    private NotificationGuts mGutsContainer;
-    private UiEventLogger mUiEventLogger;
-
-    private OnClickListener mOnOk = v -> {
-        mGutsContainer.closeControls(v, false);
-    };
-
-    public AppOpsInfo(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
-
-    public interface OnSettingsClickListener {
-        void onClick(View v, String pkg, int uid, ArraySet<Integer> ops);
-    }
-
-    public void bindGuts(final PackageManager pm,
-            final OnSettingsClickListener onSettingsClick,
-            final StatusBarNotification sbn,
-            final UiEventLogger uiEventLogger,
-            ArraySet<Integer> activeOps) {
-        mPkg = sbn.getPackageName();
-        mSbn = sbn;
-        mPm = pm;
-        mAppName = mPkg;
-        mOnSettingsClickListener = onSettingsClick;
-        mAppOps = activeOps;
-        mUiEventLogger = uiEventLogger;
-
-        bindHeader();
-        bindPrompt();
-        bindButtons();
-
-        logUiEvent(NotificationAppOpsEvent.NOTIFICATION_APP_OPS_OPEN);
-        mMetricsLogger = new MetricsLogger();
-        mMetricsLogger.visibility(MetricsEvent.APP_OPS_GUTS, true);
-    }
-
-    private void bindHeader() {
-        // Package name
-        Drawable pkgicon = null;
-        ApplicationInfo info;
-        try {
-            info = mPm.getApplicationInfo(mPkg,
-                    PackageManager.MATCH_UNINSTALLED_PACKAGES
-                            | PackageManager.MATCH_DISABLED_COMPONENTS
-                            | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
-                            | PackageManager.MATCH_DIRECT_BOOT_AWARE);
-            if (info != null) {
-                mAppUid = mSbn.getUid();
-                mAppName = String.valueOf(mPm.getApplicationLabel(info));
-                pkgicon = mPm.getApplicationIcon(info);
-            }
-        } catch (PackageManager.NameNotFoundException e) {
-            // app is gone, just show package name and generic icon
-            pkgicon = mPm.getDefaultActivityIcon();
-        }
-        ((ImageView) findViewById(R.id.pkgicon)).setImageDrawable(pkgicon);
-        ((TextView) findViewById(R.id.pkgname)).setText(mAppName);
-    }
-
-    private void bindPrompt() {
-        final TextView prompt = findViewById(R.id.prompt);
-        prompt.setText(getPrompt());
-    }
-
-    private void bindButtons() {
-        View settings =  findViewById(R.id.settings);
-        settings.setOnClickListener((View view) -> {
-            mOnSettingsClickListener.onClick(view, mPkg, mAppUid, mAppOps);
-        });
-        TextView ok = findViewById(R.id.ok);
-        ok.setOnClickListener(mOnOk);
-        ok.setAccessibilityDelegate(mGutsContainer.getAccessibilityDelegate());
-    }
-
-    private String getPrompt() {
-        if (mAppOps == null || mAppOps.size() == 0) {
-            return "";
-        } else if (mAppOps.size() == 1) {
-            if (mAppOps.contains(AppOpsManager.OP_CAMERA)) {
-                return mContext.getString(R.string.appops_camera);
-            } else if (mAppOps.contains(AppOpsManager.OP_RECORD_AUDIO)) {
-                return mContext.getString(R.string.appops_microphone);
-            } else {
-                return mContext.getString(R.string.appops_overlay);
-            }
-        } else if (mAppOps.size() == 2) {
-            if (mAppOps.contains(AppOpsManager.OP_CAMERA)) {
-                if (mAppOps.contains(AppOpsManager.OP_RECORD_AUDIO)) {
-                    return mContext.getString(R.string.appops_camera_mic);
-                } else {
-                    return mContext.getString(R.string.appops_camera_overlay);
-                }
-            } else {
-                return mContext.getString(R.string.appops_mic_overlay);
-            }
-        } else {
-            return mContext.getString(R.string.appops_camera_mic_overlay);
-        }
-    }
-
-    @Override
-    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
-        super.onInitializeAccessibilityEvent(event);
-        if (mGutsContainer != null &&
-                event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
-            if (mGutsContainer.isExposed()) {
-                event.getText().add(mContext.getString(
-                        R.string.notification_channel_controls_opened_accessibility, mAppName));
-            } else {
-                event.getText().add(mContext.getString(
-                        R.string.notification_channel_controls_closed_accessibility, mAppName));
-            }
-        }
-    }
-
-    @Override
-    public void setGutsParent(NotificationGuts guts) {
-        mGutsContainer = guts;
-    }
-
-    @Override
-    public boolean willBeRemoved() {
-        return false;
-    }
-
-    @Override
-    public boolean shouldBeSaved() {
-        return false;
-    }
-
-    @Override
-    public boolean needsFalsingProtection() {
-        return false;
-    }
-
-    @Override
-    public View getContentView() {
-        return this;
-    }
-
-    @Override
-    public boolean handleCloseControls(boolean save, boolean force) {
-        logUiEvent(NotificationAppOpsEvent.NOTIFICATION_APP_OPS_CLOSE);
-        if (mMetricsLogger != null) {
-            mMetricsLogger.visibility(MetricsEvent.APP_OPS_GUTS, false);
-        }
-        return false;
-    }
-
-    @Override
-    public int getActualHeight() {
-        return getHeight();
-    }
-
-    private void logUiEvent(NotificationAppOpsEvent event) {
-        if (mSbn != null) {
-            mUiEventLogger.logWithInstanceId(event,
-                    mSbn.getUid(), mSbn.getPackageName(), mSbn.getInstanceId());
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 22d0357..d7fa54f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -239,7 +239,7 @@
     private boolean mShowNoBackground;
     private ExpandableNotificationRow mNotificationParent;
     private OnExpandClickListener mOnExpandClickListener;
-    private View.OnClickListener mOnAppOpsClickListener;
+    private View.OnClickListener mOnAppClickListener;
     private View.OnClickListener mOnFeedbackClickListener;
 
     // Listener will be called when receiving a long click event.
@@ -1139,7 +1139,6 @@
             items.add(NotificationMenuRow.createPartialConversationItem(mContext));
             items.add(NotificationMenuRow.createInfoItem(mContext));
             items.add(NotificationMenuRow.createSnoozeItem(mContext));
-            items.add(NotificationMenuRow.createAppOpsItem(mContext));
             mMenuRow.setMenuItems(items);
         }
         if (existed) {
@@ -1598,7 +1597,6 @@
             RowContentBindStage rowContentBindStage,
             OnExpandClickListener onExpandClickListener,
             NotificationMediaManager notificationMediaManager,
-            CoordinateOnClickListener onAppOpsClickListener,
             CoordinateOnClickListener onFeedbackClickListener,
             FalsingManager falsingManager,
             StatusBarStateController statusBarStateController,
@@ -1619,7 +1617,6 @@
         mRowContentBindStage = rowContentBindStage;
         mOnExpandClickListener = onExpandClickListener;
         mMediaManager = notificationMediaManager;
-        setAppOpsOnClickListener(onAppOpsClickListener);
         setOnFeedbackClickListener(onFeedbackClickListener);
         mFalsingManager = falsingManager;
         mStatusbarStateController = statusBarStateController;
@@ -1677,14 +1674,6 @@
         requestLayout();
     }
 
-    public void showAppOpsIcons(ArraySet<Integer> activeOps) {
-        if (mIsSummaryWithChildren) {
-            mChildrenContainer.showAppOpsIcons(activeOps);
-        }
-        mPrivateLayout.showAppOpsIcons(activeOps);
-        mPublicLayout.showAppOpsIcons(activeOps);
-    }
-
     public void showFeedbackIcon(boolean show) {
         if (mIsSummaryWithChildren) {
             mChildrenContainer.showFeedbackIcon(show);
@@ -1721,24 +1710,6 @@
         mPublicLayout.setRecentlyAudiblyAlerted(audiblyAlertedRecently);
     }
 
-    public View.OnClickListener getAppOpsOnClickListener() {
-        return mOnAppOpsClickListener;
-    }
-
-    void setAppOpsOnClickListener(CoordinateOnClickListener l) {
-        mOnAppOpsClickListener = v -> {
-            createMenu();
-            NotificationMenuRowPlugin provider = getProvider();
-            if (provider == null) {
-                return;
-            }
-            MenuItem menuItem = provider.getAppOpsMenuItem(mContext);
-            if (menuItem != null) {
-                l.onClick(this, v.getWidth() / 2, v.getHeight() / 2, menuItem);
-            }
-        };
-    }
-
     public View.OnClickListener getFeedbackOnClickListener() {
         return mOnFeedbackClickListener;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
index 1dbec6603..690dce9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java
@@ -69,7 +69,6 @@
 
     private final ExpandableNotificationRow.ExpansionLogger mExpansionLogger =
             this::logNotificationExpansion;
-    private final ExpandableNotificationRow.CoordinateOnClickListener mOnAppOpsClickListener;
     private final ExpandableNotificationRow.CoordinateOnClickListener mOnFeedbackClickListener;
     private final NotificationGutsManager mNotificationGutsManager;
     private final OnDismissCallback mOnDismissCallback;
@@ -110,7 +109,6 @@
         mStatusBarStateController = statusBarStateController;
         mNotificationGutsManager = notificationGutsManager;
         mOnDismissCallback = onDismissCallback;
-        mOnAppOpsClickListener = mNotificationGutsManager::openGuts;
         mOnFeedbackClickListener = mNotificationGutsManager::openGuts;
         mAllowLongPress = allowLongPress;
         mFalsingManager = falsingManager;
@@ -132,7 +130,6 @@
                 mRowContentBindStage,
                 mOnExpandClickListener,
                 mMediaManager,
-                mOnAppOpsClickListener,
                 mOnFeedbackClickListener,
                 mFalsingManager,
                 mStatusBarStateController,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index 2986b9b..c7e44c5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -1568,18 +1568,6 @@
         return header;
     }
 
-    public void showAppOpsIcons(ArraySet<Integer> activeOps) {
-        if (mContractedChild != null) {
-            mContractedWrapper.showAppOpsIcons(activeOps);
-        }
-        if (mExpandedChild != null) {
-            mExpandedWrapper.showAppOpsIcons(activeOps);
-        }
-        if (mHeadsUpChild != null) {
-            mHeadsUpWrapper.showAppOpsIcons(activeOps);
-        }
-    }
-
     public void showFeedbackIcon(boolean show) {
         if (mContractedChild != null) {
             mContractedWrapper.showFeedbackIcon(show);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
index 729b131..05d44f6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
@@ -268,8 +268,6 @@
         try {
             if (gutsView instanceof NotificationSnooze) {
                 initializeSnoozeView(row, (NotificationSnooze) gutsView);
-            } else if (gutsView instanceof AppOpsInfo) {
-                initializeAppOpsInfo(row, (AppOpsInfo) gutsView);
             } else if (gutsView instanceof NotificationInfo) {
                 initializeNotificationInfo(row, (NotificationInfo) gutsView);
             } else if (gutsView instanceof NotificationConversationInfo) {
@@ -309,36 +307,6 @@
     }
 
     /**
-     * Sets up the {@link AppOpsInfo} inside the notification row's guts.
-     *
-     * @param row view to set up the guts for
-     * @param appOpsInfoView view to set up/bind within {@code row}
-     */
-    private void initializeAppOpsInfo(
-            final ExpandableNotificationRow row,
-            AppOpsInfo appOpsInfoView) {
-        NotificationGuts guts = row.getGuts();
-        StatusBarNotification sbn = row.getEntry().getSbn();
-        UserHandle userHandle = sbn.getUser();
-        PackageManager pmUser = StatusBar.getPackageManagerForUser(mContext,
-                userHandle.getIdentifier());
-
-        AppOpsInfo.OnSettingsClickListener onSettingsClick =
-                (View v, String pkg, int uid, ArraySet<Integer> ops) -> {
-                    mUiEventLogger.logWithInstanceId(
-                            NotificationAppOpsEvent.NOTIFICATION_APP_OPS_SETTINGS_CLICK,
-                            sbn.getUid(), sbn.getPackageName(), sbn.getInstanceId());
-                    mMetricsLogger.action(MetricsProto.MetricsEvent.ACTION_OPS_GUTS_SETTINGS);
-                    guts.resetFalsingCheck();
-                    startAppOpsSettingsActivity(pkg, uid, ops, row);
-        };
-        if (!row.getEntry().mActiveAppOps.isEmpty()) {
-            appOpsInfoView.bindGuts(pmUser, onSettingsClick, sbn, mUiEventLogger,
-                    row.getEntry().mActiveAppOps);
-        }
-    }
-
-    /**
      * Sets up the {@link FeedbackInfo} inside the notification row's guts.
      *
      * @param row view to set up the guts for
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
index d264af9..205cecc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java
@@ -76,7 +76,6 @@
     private Context mContext;
     private FrameLayout mMenuContainer;
     private NotificationMenuItem mInfoItem;
-    private MenuItem mAppOpsItem;
     private MenuItem mFeedbackItem;
     private MenuItem mSnoozeItem;
     private ArrayList<MenuItem> mLeftMenuItems;
@@ -138,11 +137,6 @@
     }
 
     @Override
-    public MenuItem getAppOpsMenuItem(Context context) {
-        return mAppOpsItem;
-    }
-
-    @Override
     public MenuItem getFeedbackMenuItem(Context context) {
         return mFeedbackItem;
     }
@@ -264,7 +258,6 @@
             // Only show snooze for non-foreground notifications, and if the setting is on
             mSnoozeItem = createSnoozeItem(mContext);
         }
-        mAppOpsItem = createAppOpsItem(mContext);
         mFeedbackItem = createFeedbackItem(mContext);
         NotificationEntry entry = mParent.getEntry();
         int personNotifType = mPeopleNotificationIdentifier
@@ -281,7 +274,6 @@
             mRightMenuItems.add(mSnoozeItem);
         }
         mRightMenuItems.add(mInfoItem);
-        mRightMenuItems.add(mAppOpsItem);
         mRightMenuItems.add(mFeedbackItem);
         mLeftMenuItems.addAll(mRightMenuItems);
 
@@ -690,14 +682,6 @@
                 R.drawable.ic_settings);
     }
 
-    static MenuItem createAppOpsItem(Context context) {
-        AppOpsInfo appOpsContent = (AppOpsInfo) LayoutInflater.from(context).inflate(
-                R.layout.app_ops_info, null, false);
-        MenuItem info = new NotificationMenuItem(context, null, appOpsContent,
-                -1 /*don't show in slow swipe menu */);
-        return info;
-    }
-
     static MenuItem createFeedbackItem(Context context) {
         FeedbackInfo feedbackContent = (FeedbackInfo) LayoutInflater.from(context).inflate(
                 R.layout.feedback_info, null, false);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
index 4651771..3f58674 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
@@ -18,7 +18,6 @@
 
 import static com.android.systemui.statusbar.notification.TransformState.TRANSFORM_Y;
 
-import android.app.AppOpsManager;
 import android.app.Notification;
 import android.content.Context;
 import android.util.ArraySet;
@@ -64,10 +63,6 @@
     private TextView mHeaderText;
     private TextView mAppNameText;
     private ImageView mWorkProfileImage;
-    private View mCameraIcon;
-    private View mMicIcon;
-    private View mOverlayIcon;
-    private View mAppOps;
     private View mAudiblyAlertedIcon;
     private FrameLayout mIconContainer;
     private View mFeedbackIcon;
@@ -109,7 +104,6 @@
                     }
                 }, TRANSFORMING_VIEW_TITLE);
         resolveHeaderViews();
-        addAppOpsOnClickListener(row);
         addFeedbackOnClickListener(row);
     }
 
@@ -121,10 +115,6 @@
         mExpandButton = mView.findViewById(com.android.internal.R.id.expand_button);
         mWorkProfileImage = mView.findViewById(com.android.internal.R.id.profile_badge);
         mNotificationHeader = mView.findViewById(com.android.internal.R.id.notification_header);
-        mCameraIcon = mView.findViewById(com.android.internal.R.id.camera);
-        mMicIcon = mView.findViewById(com.android.internal.R.id.mic);
-        mOverlayIcon = mView.findViewById(com.android.internal.R.id.overlay);
-        mAppOps = mView.findViewById(com.android.internal.R.id.app_ops);
         mAudiblyAlertedIcon = mView.findViewById(com.android.internal.R.id.alerted_icon);
         mFeedbackIcon = mView.findViewById(com.android.internal.R.id.feedback);
         if (mNotificationHeader != null) {
@@ -133,38 +123,6 @@
         }
     }
 
-    private void addAppOpsOnClickListener(ExpandableNotificationRow row) {
-        View.OnClickListener listener = row.getAppOpsOnClickListener();
-        if (mNotificationHeader != null) {
-            mNotificationHeader.setAppOpsOnClickListener(listener);
-        }
-        if (mAppOps != null) {
-            mAppOps.setOnClickListener(listener);
-        }
-    }
-
-    /**
-     * Shows or hides 'app op in use' icons based on app usage.
-     */
-    @Override
-    public void showAppOpsIcons(ArraySet<Integer> appOps) {
-        if (appOps == null) {
-            return;
-        }
-        if (mOverlayIcon != null) {
-            mOverlayIcon.setVisibility(appOps.contains(AppOpsManager.OP_SYSTEM_ALERT_WINDOW)
-                    ? View.VISIBLE : View.GONE);
-        }
-        if (mCameraIcon != null) {
-            mCameraIcon.setVisibility(appOps.contains(AppOpsManager.OP_CAMERA)
-                    ? View.VISIBLE : View.GONE);
-        }
-        if (mMicIcon != null) {
-            mMicIcon.setVisibility(appOps.contains(AppOpsManager.OP_RECORD_AUDIO)
-                    ? View.VISIBLE : View.GONE);
-        }
-    }
-
     private void addFeedbackOnClickListener(ExpandableNotificationRow row) {
         View.OnClickListener listener = row.getFeedbackOnClickListener();
         if (mNotificationHeader != null) {
@@ -304,15 +262,6 @@
             mTransformationHelper.addTransformedView(TransformableView.TRANSFORMING_VIEW_TITLE,
                     mHeaderText);
         }
-        if (mCameraIcon != null) {
-            mTransformationHelper.addViewTransformingToSimilar(mCameraIcon);
-        }
-        if (mMicIcon != null) {
-            mTransformationHelper.addViewTransformingToSimilar(mMicIcon);
-        }
-        if (mOverlayIcon != null) {
-            mTransformationHelper.addViewTransformingToSimilar(mOverlayIcon);
-        }
         if (mAudiblyAlertedIcon != null) {
             mTransformationHelper.addViewTransformingToSimilar(mAudiblyAlertedIcon);
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java
index 93d3f3b..33c93905 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMediaTemplateViewWrapper.java
@@ -22,14 +22,12 @@
 import android.app.Notification;
 import android.content.Context;
 import android.content.res.ColorStateList;
-import android.graphics.drawable.Drawable;
 import android.media.MediaMetadata;
 import android.media.session.MediaController;
 import android.media.session.MediaSession;
 import android.media.session.PlaybackState;
 import android.metrics.LogMaker;
 import android.os.Handler;
-import android.service.notification.StatusBarNotification;
 import android.text.format.DateUtils;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -43,13 +41,9 @@
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.internal.widget.MediaNotificationView;
 import com.android.systemui.Dependency;
-import com.android.systemui.qs.QSPanel;
-import com.android.systemui.qs.QuickQSPanel;
 import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.TransformableView;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
-import com.android.systemui.statusbar.phone.NotificationShadeWindowController;
-import com.android.systemui.util.Utils;
 
 import java.util.Timer;
 import java.util.TimerTask;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java
index 605fbc0..42f5e38 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java
@@ -97,14 +97,6 @@
     }
 
     /**
-     * Show a set of app opp icons in the layout.
-     *
-     * @param appOps which app ops to show
-     */
-    public void showAppOpsIcons(ArraySet<Integer> appOps) {
-    }
-
-    /**
      * Shows or hides feedback icon.
      */
     public void showFeedbackIcon(boolean show) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
index 93c2377..7e4266c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
@@ -1303,20 +1303,6 @@
     }
 
     /**
-     * Show a set of app opp icons in the layout.
-     *
-     * @param appOps which app ops to show
-     */
-    public void showAppOpsIcons(ArraySet<Integer> appOps) {
-        if (mNotificationHeaderWrapper != null) {
-            mNotificationHeaderWrapper.showAppOpsIcons(appOps);
-        }
-        if (mNotificationHeaderWrapperLowPriority != null) {
-            mNotificationHeaderWrapperLowPriority.showAppOpsIcons(appOps);
-        }
-    }
-
-    /**
      * Shows or hides feedback icon.
      */
     public void showFeedbackIcon(boolean show) {
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 0e76c904..9d920c3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
@@ -47,6 +47,7 @@
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationMediaManager;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
 import java.io.FileDescriptor;
@@ -157,11 +158,11 @@
     private DozeScrimController mDozeScrimController;
     private KeyguardViewMediator mKeyguardViewMediator;
     private ScrimController mScrimController;
-    private StatusBar mStatusBar;
     private PendingAuthenticated mPendingAuthenticated = null;
     private boolean mPendingShowBouncer;
     private boolean mHasScreenTurnedOnSinceAuthenticating;
     private boolean mFadedAwayAfterWakeAndUnlock;
+    private BiometricModeListener mBiometricModeListener;
 
     private final MetricsLogger mMetricsLogger;
 
@@ -243,7 +244,7 @@
     @Inject
     public BiometricUnlockController(Context context, DozeScrimController dozeScrimController,
             KeyguardViewMediator keyguardViewMediator, ScrimController scrimController,
-            StatusBar statusBar, ShadeController shadeController,
+            ShadeController shadeController,
             NotificationShadeWindowController notificationShadeWindowController,
             KeyguardStateController keyguardStateController, Handler handler,
             KeyguardUpdateMonitor keyguardUpdateMonitor,
@@ -264,7 +265,6 @@
         mDozeScrimController = dozeScrimController;
         mKeyguardViewMediator = keyguardViewMediator;
         mScrimController = scrimController;
-        mStatusBar = statusBar;
         mKeyguardStateController = keyguardStateController;
         mHandler = handler;
         mWakeUpDelay = resources.getInteger(com.android.internal.R.integer.config_wakeUpDelayDoze);
@@ -278,6 +278,11 @@
         mKeyguardViewController = keyguardViewController;
     }
 
+    /** Sets a {@link BiometricModeListener}. */
+    public void setBiometricModeListener(BiometricModeListener biometricModeListener) {
+        mBiometricModeListener = biometricModeListener;
+    }
+
     private final Runnable mReleaseBiometricWakeLockRunnable = new Runnable() {
         @Override
         public void run() {
@@ -434,19 +439,25 @@
                 } else {
                     mKeyguardViewMediator.onWakeAndUnlocking();
                 }
-                if (mStatusBar.getNavigationBarView() != null) {
-                    mStatusBar.getNavigationBarView().setWakeAndUnlocking(true);
-                }
                 Trace.endSection();
                 break;
             case MODE_ONLY_WAKE:
             case MODE_NONE:
                 break;
         }
-        mStatusBar.notifyBiometricAuthModeChanged();
+        onModeChanged(mMode);
+        if (mBiometricModeListener != null) {
+            mBiometricModeListener.notifyBiometricAuthModeChanged();
+        }
         Trace.endSection();
     }
 
+    private void onModeChanged(@WakeAndUnlockMode int mode) {
+        if (mBiometricModeListener != null) {
+            mBiometricModeListener.onModeChanged(mode);
+        }
+    }
+
     private void showBouncer() {
         if (mMode == MODE_SHOW_BOUNCER) {
             mKeyguardViewController.showBouncer(false);
@@ -619,10 +630,10 @@
         mMode = MODE_NONE;
         mBiometricType = null;
         mNotificationShadeWindowController.setForceDozeBrightness(false);
-        if (mStatusBar.getNavigationBarView() != null) {
-            mStatusBar.getNavigationBarView().setWakeAndUnlocking(false);
+        if (mBiometricModeListener != null) {
+            mBiometricModeListener.onResetMode();
+            mBiometricModeListener.notifyBiometricAuthModeChanged();
         }
-        mStatusBar.notifyBiometricAuthModeChanged();
     }
 
     @VisibleForTesting
@@ -702,4 +713,14 @@
                 return 3;
         }
     }
+
+    /** An interface to interact with the {@link BiometricUnlockController}. */
+    public interface BiometricModeListener {
+        /** Called when {@code mMode} is reset to {@link #MODE_NONE}. */
+        void onResetMode();
+        /** Called when {@code mMode} has changed in {@link #startWakeAndUnlock(int)}. */
+        void onModeChanged(@WakeAndUnlockMode int mode);
+        /** Called after processing {@link #onModeChanged(int)}. */
+        void notifyBiometricAuthModeChanged();
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
index 4afeba8..de74d4e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeServiceHost.java
@@ -36,6 +36,7 @@
 import com.android.systemui.doze.DozeReceiver;
 import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.PulseExpansionHandler;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java
similarity index 96%
rename from packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowController.java
rename to packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java
index bc73be1..07b0a4b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java
@@ -47,7 +47,7 @@
 import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
-import com.android.systemui.statusbar.RemoteInputController.Callback;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -72,8 +72,8 @@
  * Encapsulates all logic for the notification shade window state management.
  */
 @Singleton
-public class NotificationShadeWindowController implements Callback, Dumpable,
-        ConfigurationListener {
+public class NotificationShadeWindowControllerImpl implements NotificationShadeWindowController,
+        Dumpable, ConfigurationListener {
 
     private static final String TAG = "NotificationShadeWindowController";
     private static final boolean DEBUG = false;
@@ -103,7 +103,7 @@
     private final SysuiColorExtractor mColorExtractor;
 
     @Inject
-    public NotificationShadeWindowController(Context context, WindowManager windowManager,
+    public NotificationShadeWindowControllerImpl(Context context, WindowManager windowManager,
             IActivityManager activityManager, DozeParameters dozeParameters,
             StatusBarStateController statusBarStateController,
             ConfigurationController configurationController,
@@ -147,6 +147,7 @@
     /**
      * Register to receive notifications about status bar window state changes.
      */
+    @Override
     public void registerCallback(StatusBarWindowCallback callback) {
         // Prevent adding duplicate callbacks
         for (int i = 0; i < mCallbacks.size(); i++) {
@@ -161,6 +162,7 @@
      * Register a listener to monitor scrims visibility
      * @param listener A listener to monitor scrims visibility
      */
+    @Override
     public void setScrimsVisibilityListener(Consumer<Integer> listener) {
         if (listener != null && mScrimsVisibilityListener != listener) {
             mScrimsVisibilityListener = listener;
@@ -176,6 +178,7 @@
     /**
      * Adds the notification shade view to the window manager.
      */
+    @Override
     public void attach() {
         // Now that the notification shade encompasses the sliding panel and its
         // translucent backdrop, the entire thing is made TRANSLUCENT and is
@@ -214,14 +217,17 @@
         }
     }
 
+    @Override
     public void setNotificationShadeView(ViewGroup view) {
         mNotificationShadeView = view;
     }
 
+    @Override
     public ViewGroup getNotificationShadeView() {
         return mNotificationShadeView;
     }
 
+    @Override
     public void setDozeScreenBrightness(int value) {
         mScreenBrightnessDoze = value / 255f;
     }
@@ -406,6 +412,7 @@
         notifyStateChangedCallbacks();
     }
 
+    @Override
     public void notifyStateChangedCallbacks() {
         for (int i = 0; i < mCallbacks.size(); i++) {
             StatusBarWindowCallback cb = mCallbacks.get(i).get();
@@ -445,62 +452,74 @@
         }
     }
 
+    @Override
     public void setKeyguardShowing(boolean showing) {
         mCurrentState.mKeyguardShowing = showing;
         apply(mCurrentState);
     }
 
+    @Override
     public void setKeyguardOccluded(boolean occluded) {
         mCurrentState.mKeyguardOccluded = occluded;
         apply(mCurrentState);
     }
 
+    @Override
     public void setKeyguardNeedsInput(boolean needsInput) {
         mCurrentState.mKeyguardNeedsInput = needsInput;
         apply(mCurrentState);
     }
 
+    @Override
     public void setPanelVisible(boolean visible) {
         mCurrentState.mPanelVisible = visible;
         mCurrentState.mNotificationShadeFocusable = visible;
         apply(mCurrentState);
     }
 
+    @Override
     public void setNotificationShadeFocusable(boolean focusable) {
         mCurrentState.mNotificationShadeFocusable = focusable;
         apply(mCurrentState);
     }
 
+    @Override
     public void setBouncerShowing(boolean showing) {
         mCurrentState.mBouncerShowing = showing;
         apply(mCurrentState);
     }
 
+    @Override
     public void setBackdropShowing(boolean showing) {
         mCurrentState.mBackdropShowing = showing;
         apply(mCurrentState);
     }
 
+    @Override
     public void setKeyguardFadingAway(boolean keyguardFadingAway) {
         mCurrentState.mKeyguardFadingAway = keyguardFadingAway;
         apply(mCurrentState);
     }
 
+    @Override
     public void setQsExpanded(boolean expanded) {
         mCurrentState.mQsExpanded = expanded;
         apply(mCurrentState);
     }
 
+    @Override
     public void setForceUserActivity(boolean forceUserActivity) {
         mCurrentState.mForceUserActivity = forceUserActivity;
         apply(mCurrentState);
     }
 
-    void setLaunchingActivity(boolean launching) {
+    @Override
+    public void setLaunchingActivity(boolean launching) {
         mCurrentState.mLaunchingActivity = launching;
         apply(mCurrentState);
     }
 
+    @Override
     public void setScrimsVisibility(int scrimsVisibility) {
         mCurrentState.mScrimsVisibility = scrimsVisibility;
         apply(mCurrentState);
@@ -512,6 +531,7 @@
      * {@link com.android.systemui.statusbar.NotificationShadeDepthController}.
      * @param backgroundBlurRadius Radius in pixels.
      */
+    @Override
     public void setBackgroundBlurRadius(int backgroundBlurRadius) {
         if (mCurrentState.mBackgroundBlurRadius == backgroundBlurRadius) {
             return;
@@ -520,11 +540,13 @@
         apply(mCurrentState);
     }
 
+    @Override
     public void setHeadsUpShowing(boolean showing) {
         mCurrentState.mHeadsUpShowing = showing;
         apply(mCurrentState);
     }
 
+    @Override
     public void setWallpaperSupportsAmbientMode(boolean supportsAmbientMode) {
         mCurrentState.mWallpaperSupportsAmbientMode = supportsAmbientMode;
         apply(mCurrentState);
@@ -543,11 +565,13 @@
      * Used for when a heads-up comes in but we still need to wait for the touchable regions to
      * be computed.
      */
+    @Override
     public void setForceWindowCollapsed(boolean force) {
         mCurrentState.mForceCollapsed = force;
         apply(mCurrentState);
     }
 
+    @Override
     public void setPanelExpanded(boolean isExpanded) {
         mCurrentState.mPanelExpanded = isExpanded;
         apply(mCurrentState);
@@ -563,16 +587,19 @@
      * Set whether the screen brightness is forced to the value we use for doze mode by the status
      * bar window.
      */
+    @Override
     public void setForceDozeBrightness(boolean forceDozeBrightness) {
         mCurrentState.mForceDozeBrightness = forceDozeBrightness;
         apply(mCurrentState);
     }
 
+    @Override
     public void setDozing(boolean dozing) {
         mCurrentState.mDozing = dozing;
         apply(mCurrentState);
     }
 
+    @Override
     public void setForcePluginOpen(boolean forcePluginOpen) {
         mCurrentState.mForcePluginOpen = forcePluginOpen;
         apply(mCurrentState);
@@ -584,10 +611,12 @@
     /**
      * The forcePluginOpen state for the status bar.
      */
+    @Override
     public boolean getForcePluginOpen() {
         return mCurrentState.mForcePluginOpen;
     }
 
+    @Override
     public void setNotTouchable(boolean notTouchable) {
         mCurrentState.mNotTouchable = notTouchable;
         apply(mCurrentState);
@@ -596,24 +625,29 @@
     /**
      * Whether the status bar panel is expanded or not.
      */
+    @Override
     public boolean getPanelExpanded() {
         return mCurrentState.mPanelExpanded;
     }
 
+    @Override
     public void setStateListener(OtherwisedCollapsedListener listener) {
         mListener = listener;
     }
 
+    @Override
     public void setForcePluginOpenListener(ForcePluginOpenListener listener) {
         mForcePluginOpenListener = listener;
     }
 
+    @Override
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         pw.println(TAG + ":");
         pw.println("  mKeyguardDisplayMode=" + mKeyguardDisplayMode);
         pw.println(mCurrentState);
     }
 
+    @Override
     public boolean isShowingWallpaper() {
         return !mCurrentState.mBackdropShowing;
     }
@@ -632,6 +666,7 @@
     /**
      * When keyguard will be dismissed but didn't start animation yet.
      */
+    @Override
     public void setKeyguardGoingAway(boolean goingAway) {
         mCurrentState.mKeyguardGoingAway = goingAway;
         apply(mCurrentState);
@@ -642,6 +677,7 @@
      * animation is performed, the component should remove itself from the list of features that
      * are forcing SystemUI to be top-ui.
      */
+    @Override
     public void setRequestTopUi(boolean requestTopUi, String componentTag) {
         if (requestTopUi) {
             mCurrentState.mComponentsForcingTopUi.add(componentTag);
@@ -725,23 +761,4 @@
             setDozing(isDozing);
         }
     };
-
-    /**
-     * Custom listener to pipe data back to plugins about whether or not the status bar would be
-     * collapsed if not for the plugin.
-     * TODO: Find cleaner way to do this.
-     */
-    public interface OtherwisedCollapsedListener {
-        void setWouldOtherwiseCollapse(boolean otherwiseCollapse);
-    }
-
-    /**
-     * Listener to indicate forcePluginOpen has changed
-     */
-    public interface ForcePluginOpenListener {
-        /**
-         * Called when mState.forcePluginOpen is changed
-         */
-        void onChange(boolean forceOpen);
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
index 42222d7..53cc267 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewController.java
@@ -44,6 +44,7 @@
 import com.android.systemui.statusbar.DragDownHelper;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.PulseExpansionHandler;
 import com.android.systemui.statusbar.SuperStatusBarViewFactory;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ShadeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ShadeControllerImpl.java
index 3330615..921bd4f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ShadeControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ShadeControllerImpl.java
@@ -26,6 +26,7 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationPresenter;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.StatusBarState;
 
 import java.util.ArrayList;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index 4fbd49a..5b251be 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -191,6 +191,7 @@
 import com.android.systemui.statusbar.NotificationPresenter;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.NotificationShelfController;
 import com.android.systemui.statusbar.NotificationViewHierarchyManager;
 import com.android.systemui.statusbar.PulseExpansionHandler;
@@ -1473,6 +1474,34 @@
     protected void startKeyguard() {
         Trace.beginSection("StatusBar#startKeyguard");
         mBiometricUnlockController = mBiometricUnlockControllerLazy.get();
+        mBiometricUnlockController.setBiometricModeListener(
+                new BiometricUnlockController.BiometricModeListener() {
+                    @Override
+                    public void onResetMode() {
+                        setWakeAndUnlocking(false);
+                    }
+
+                    @Override
+                    public void onModeChanged(int mode) {
+                        switch (mode) {
+                            case BiometricUnlockController.MODE_WAKE_AND_UNLOCK_FROM_DREAM:
+                            case BiometricUnlockController.MODE_WAKE_AND_UNLOCK_PULSING:
+                            case BiometricUnlockController.MODE_WAKE_AND_UNLOCK:
+                                setWakeAndUnlocking(true);
+                        }
+                    }
+
+                    @Override
+                    public void notifyBiometricAuthModeChanged() {
+                        StatusBar.this.notifyBiometricAuthModeChanged();
+                    }
+
+                    private void setWakeAndUnlocking(boolean wakeAndUnlocking) {
+                        if (getNavigationBarView() != null) {
+                            getNavigationBarView().setWakeAndUnlocking(wakeAndUnlocking);
+                        }
+                    }
+                });
         mStatusBarKeyguardViewManager.registerStatusBar(
                 /* statusBar= */ this, getBouncerContainer(),
                 mNotificationPanelViewController, mBiometricUnlockController,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index f3a7c50..e159fad 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -56,6 +56,7 @@
 import com.android.systemui.shared.system.SysUiStatsLog;
 import com.android.systemui.statusbar.CrossFadeHelper;
 import com.android.systemui.statusbar.NotificationMediaManager;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
index f1715be..0ec22b4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
@@ -52,6 +52,7 @@
 import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.NotificationPresenter;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.NotificationViewHierarchyManager;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
index 9c7f490..b79bb70 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java
@@ -32,6 +32,7 @@
 import com.android.systemui.Dumpable;
 import com.android.systemui.R;
 import com.android.systemui.ScreenDecorations;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
 import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java
index dd4af54..d230eca 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.java
@@ -53,6 +53,7 @@
 import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.NotificationViewHierarchyManager;
 import com.android.systemui.statusbar.PulseExpansionHandler;
 import com.android.systemui.statusbar.SuperStatusBarViewFactory;
@@ -80,7 +81,6 @@
 import com.android.systemui.statusbar.phone.LockscreenLockIconController;
 import com.android.systemui.statusbar.phone.LockscreenWallpaper;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
-import com.android.systemui.statusbar.phone.NotificationShadeWindowController;
 import com.android.systemui.statusbar.phone.PhoneStatusBarPolicy;
 import com.android.systemui.statusbar.phone.ScrimController;
 import com.android.systemui.statusbar.phone.ShadeController;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java
index 5257ce4..4ae9665 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/WifiSignalController.java
@@ -84,7 +84,7 @@
                 R.bool.config_showWifiIndicatorWhenEnabled);
         boolean wifiVisible = mCurrentState.enabled && (
                 (mCurrentState.connected && mCurrentState.inetCondition == 1)
-                        || !mHasMobileDataFeature || mWifiTracker.isDefaultNetwork
+                        || !mHasMobileDataFeature || mCurrentState.isDefault
                         || visibleWhenEnabled);
         String wifiDesc = mCurrentState.connected ? mCurrentState.ssid : null;
         boolean ssidPresent = wifiVisible && mCurrentState.ssid != null;
@@ -107,6 +107,7 @@
     public void fetchInitialState() {
         mWifiTracker.fetchInitialState();
         mCurrentState.enabled = mWifiTracker.enabled;
+        mCurrentState.isDefault = mWifiTracker.isDefaultNetwork;
         mCurrentState.connected = mWifiTracker.connected;
         mCurrentState.ssid = mWifiTracker.ssid;
         mCurrentState.rssi = mWifiTracker.rssi;
@@ -121,6 +122,7 @@
     public void handleBroadcast(Intent intent) {
         mWifiTracker.handleBroadcast(intent);
         mCurrentState.enabled = mWifiTracker.enabled;
+        mCurrentState.isDefault = mWifiTracker.isDefaultNetwork;
         mCurrentState.connected = mWifiTracker.connected;
         mCurrentState.ssid = mWifiTracker.ssid;
         mCurrentState.rssi = mWifiTracker.rssi;
@@ -131,6 +133,7 @@
 
     private void handleStatusUpdated() {
         mCurrentState.statusLabel = mWifiTracker.statusLabel;
+        mCurrentState.isDefault = mWifiTracker.isDefaultNetwork;
         notifyListenersIfNecessary();
     }
 
@@ -156,6 +159,7 @@
     static class WifiState extends SignalController.State {
         String ssid;
         boolean isTransient;
+        boolean isDefault;
         String statusLabel;
 
         @Override
@@ -164,6 +168,7 @@
             WifiState state = (WifiState) s;
             ssid = state.ssid;
             isTransient = state.isTransient;
+            isDefault = state.isDefault;
             statusLabel = state.statusLabel;
         }
 
@@ -172,6 +177,7 @@
             super.toString(builder);
             builder.append(",ssid=").append(ssid)
                 .append(",isTransient=").append(isTransient)
+                .append(",isDefault=").append(isDefault)
                 .append(",statusLabel=").append(statusLabel);
         }
 
@@ -183,6 +189,7 @@
             WifiState other = (WifiState) o;
             return Objects.equals(other.ssid, ssid)
                     && other.isTransient == isTransient
+                    && other.isDefault == isDefault
                     && TextUtils.equals(other.statusLabel, statusLabel);
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/micdisclosure/AudioRecordingDisclosureBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/micdisclosure/AudioRecordingDisclosureBar.java
index d4c6f4d..a29db4d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/tv/micdisclosure/AudioRecordingDisclosureBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/micdisclosure/AudioRecordingDisclosureBar.java
@@ -26,8 +26,6 @@
 import android.annotation.IntDef;
 import android.annotation.UiThread;
 import android.content.Context;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
 import android.graphics.PixelFormat;
 import android.provider.DeviceConfig;
 import android.text.TextUtils;
@@ -47,9 +45,7 @@
 import java.lang.annotation.RetentionPolicy;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.LinkedList;
 import java.util.List;
-import java.util.Queue;
 import java.util.Set;
 
 /**
@@ -77,9 +73,6 @@
             STATE_NOT_SHOWN,
             STATE_APPEARING,
             STATE_SHOWN,
-            STATE_MINIMIZING,
-            STATE_MINIMIZED,
-            STATE_MAXIMIZING,
             STATE_DISAPPEARING
     })
     public @interface State {}
@@ -88,13 +81,9 @@
     private static final int STATE_NOT_SHOWN = 0;
     private static final int STATE_APPEARING = 1;
     private static final int STATE_SHOWN = 2;
-    private static final int STATE_MINIMIZING = 3;
-    private static final int STATE_MINIMIZED = 4;
-    private static final int STATE_MAXIMIZING = 5;
-    private static final int STATE_DISAPPEARING = 6;
+    private static final int STATE_DISAPPEARING = 3;
 
     private static final int ANIMATION_DURATION = 600;
-    private static final int MAXIMIZED_DURATION = 3000;
 
     private final Context mContext;
     private boolean mIsEnabled;
@@ -116,30 +105,6 @@
      */
     private AudioActivityObserver[] mAudioActivityObservers;
     /**
-     * Whether the indicator should expand and show the recording application's label.
-     * If disabled ({@code false}) the "minimized" ({@link #STATE_MINIMIZED}) indicator would appear
-     * on the screen whenever an application is recording, but will not reveal to the user what
-     * application this is.
-     */
-    private final boolean mRevealRecordingPackages;
-    /**
-     * Set of applications that we've notified the user about since the indicator came up. Meaning
-     * that if an application is in this list then at some point since the indicator came up, it
-     * was expanded showing this application's title.
-     * Used not to notify the user about the same application again while the indicator is shown.
-     * We empty this set every time the indicator goes off the screen (we always call {@code
-     * mSessionNotifiedPackages.clear()} before calling {@link #hide()}).
-     */
-    private final Set<String> mSessionNotifiedPackages = new ArraySet<>();
-    /**
-     * If an application starts recording while the TV indicator is neither in {@link
-     * #STATE_NOT_SHOWN} nor in {@link #STATE_MINIMIZED}, then we add the application's package
-     * name to the queue, from which we take packages names one by one to disclose the
-     * corresponding applications' titles to the user, whenever the indicator eventually comes to
-     * one of the two aforementioned states.
-     */
-    private final Queue<String> mPendingNotificationPackages = new LinkedList<>();
-    /**
      * Set of applications for which we make an exception and do not show the indicator. This gets
      * populated once - in {@link #AudioRecordingDisclosureBar(Context)}.
      */
@@ -149,8 +114,6 @@
         mContext = context;
 
         // Load configs
-        mRevealRecordingPackages = mContext.getResources().getBoolean(
-                R.bool.audio_recording_disclosure_reveal_packages);
         reloadExemptPackages();
 
         mIsEnabled = DeviceConfig.getBoolean(NAMESPACE_PRIVACY, ENABLED_FLAG, true);
@@ -210,10 +173,6 @@
         if (mState != STATE_NOT_SHOWN) {
             removeIndicatorView();
         }
-
-        // Clean up the state.
-        mSessionNotifiedPackages.clear();
-        mPendingNotificationPackages.clear();
     }
 
     @UiThread
@@ -231,68 +190,29 @@
         }
 
         if (active) {
-            showIndicatorForPackageIfNeeded(packageName);
+            showIfNotShown();
         } else {
             hideIndicatorIfNeeded();
         }
     }
 
     @UiThread
-    private void showIndicatorForPackageIfNeeded(String packageName) {
-        if (DEBUG) Log.d(TAG, "showIndicatorForPackageIfNeeded, packageName=" + packageName);
-        if (!mSessionNotifiedPackages.add(packageName)) {
-            // We've already notified user about this app, no need to do it again.
-            if (DEBUG) Log.d(TAG, "   - already notified");
-            return;
-        }
-
-        switch (mState) {
-            case STATE_NOT_SHOWN:
-                show(packageName);
-                break;
-
-            case STATE_MINIMIZED:
-                if (mRevealRecordingPackages) {
-                    expand(packageName);
-                }
-                break;
-
-            case STATE_DISAPPEARING:
-            case STATE_APPEARING:
-            case STATE_MAXIMIZING:
-            case STATE_SHOWN:
-            case STATE_MINIMIZING:
-                // Currently animating or expanded. Thus add to the pending notifications, and it
-                // will be picked up once the indicator comes to the STATE_MINIMIZED.
-                mPendingNotificationPackages.add(packageName);
-                break;
-        }
-    }
-
-    @UiThread
     private void hideIndicatorIfNeeded() {
-        // If not MINIMIZED, will check whether the indicator should be hidden when the indicator
-        // comes to the STATE_MINIMIZED eventually.
-        if (mState != STATE_MINIMIZED) return;
+        // If not STATE_APPEARING, will check whether the indicator should be hidden when the
+        // indicator comes to the STATE_SHOWN.
+        // If STATE_DISAPPEARING or STATE_SHOWN - nothing else for us to do here.
+        if (mState != STATE_SHOWN) return;
 
-        // If is in the STATE_MINIMIZED, but there are other active recorders - simply ignore.
-        for (int index = mAudioActivityObservers.length - 1; index >= 0; index--) {
-            for (String activePackage : mAudioActivityObservers[index].getActivePackages()) {
-                if (mExemptPackages.contains(activePackage)) continue;
-                return;
-            }
+        // If is in the STATE_SHOWN and there are no active recorders - hide.
+        if (!hasActiveRecorders()) {
+            hide();
         }
-
-        // Clear the state and hide the indicator.
-        mSessionNotifiedPackages.clear();
-        hide();
     }
 
     @UiThread
-    private void show(String packageName) {
-        if (DEBUG) {
-            Log.d(TAG, "Showing indicator");
-        }
+    private void showIfNotShown() {
+        if (mState != STATE_NOT_SHOWN) return;
+        if (DEBUG) Log.d(TAG, "Showing indicator");
 
         mIsLtr = mContext.getResources().getConfiguration().getLayoutDirection()
                 == View.LAYOUT_DIRECTION_LTR;
@@ -308,30 +228,14 @@
         mTextView = mTextsContainers.findViewById(R.id.text);
         mBgEnd = mIndicatorView.findViewById(R.id.bg_end);
 
-        // Set up the notification text
-        if (mRevealRecordingPackages) {
-            // Swap background drawables depending on layout directions (both drawables have rounded
-            // corners only on one side)
-            if (mIsLtr) {
-                mBgEnd.setBackgroundResource(R.drawable.tv_rect_dark_right_rounded);
-                mIconContainerBg.setBackgroundResource(R.drawable.tv_rect_dark_left_rounded);
-            } else {
-                mBgEnd.setBackgroundResource(R.drawable.tv_rect_dark_left_rounded);
-                mIconContainerBg.setBackgroundResource(R.drawable.tv_rect_dark_right_rounded);
-            }
-
-            final String label = getApplicationLabel(packageName);
-            mTextView.setText(mContext.getString(R.string.app_accessed_mic, label));
-        } else {
-            mTextsContainers.setVisibility(View.GONE);
-            mIconContainerBg.setVisibility(View.GONE);
-            mTextView.setVisibility(View.GONE);
-            mBgEnd.setVisibility(View.GONE);
-            mTextsContainers = null;
-            mIconContainerBg = null;
-            mTextView = null;
-            mBgEnd = null;
-        }
+        mTextsContainers.setVisibility(View.GONE);
+        mIconContainerBg.setVisibility(View.GONE);
+        mTextView.setVisibility(View.GONE);
+        mBgEnd.setVisibility(View.GONE);
+        mTextsContainers = null;
+        mIconContainerBg = null;
+        mTextView = null;
+        mBgEnd = null;
 
         // Initially change the visibility to INVISIBLE, wait until and receives the size and
         // then animate it moving from "off" the screen correctly
@@ -366,9 +270,7 @@
                                             @Override
                                             public void onAnimationStart(Animator animation,
                                                     boolean isReverse) {
-                                                if (mState == STATE_STOPPED) {
-                                                    return;
-                                                }
+                                                if (mState == STATE_STOPPED) return;
 
                                                 // Indicator is INVISIBLE at the moment, change it.
                                                 mIndicatorView.setVisibility(View.VISIBLE);
@@ -376,11 +278,7 @@
 
                                             @Override
                                             public void onAnimationEnd(Animator animation) {
-                                                if (mRevealRecordingPackages) {
-                                                    onExpanded();
-                                                } else {
-                                                    onMinimized();
-                                                }
+                                                onAppeared();
                                             }
                                         });
                                 set.start();
@@ -404,60 +302,9 @@
     }
 
     @UiThread
-    private void expand(String packageName) {
-        assertRevealingRecordingPackages();
-
-        final String label = getApplicationLabel(packageName);
-        mTextView.setText(mContext.getString(R.string.app_accessed_mic, label));
-
-        final AnimatorSet set = new AnimatorSet();
-        set.playTogether(
-                ObjectAnimator.ofFloat(mIconTextsContainer, View.TRANSLATION_X, 0),
-                ObjectAnimator.ofFloat(mIconContainerBg, View.ALPHA, 1f),
-                ObjectAnimator.ofFloat(mTextsContainers, View.ALPHA, 1f),
-                ObjectAnimator.ofFloat(mBgEnd, View.ALPHA, 1f));
-        set.setDuration(ANIMATION_DURATION);
-        set.addListener(
-                new AnimatorListenerAdapter() {
-                    @Override
-                    public void onAnimationEnd(Animator animation) {
-                        onExpanded();
-                    }
-                });
-        set.start();
-
-        mState = STATE_MAXIMIZING;
-    }
-
-    @UiThread
-    private void minimize() {
-        assertRevealingRecordingPackages();
-
-        final int targetOffset = (mIsLtr ? 1 : -1) * mTextsContainers.getWidth();
-        final AnimatorSet set = new AnimatorSet();
-        set.playTogether(
-                ObjectAnimator.ofFloat(mIconTextsContainer, View.TRANSLATION_X, targetOffset),
-                ObjectAnimator.ofFloat(mIconContainerBg, View.ALPHA, 0f),
-                ObjectAnimator.ofFloat(mTextsContainers, View.ALPHA, 0f),
-                ObjectAnimator.ofFloat(mBgEnd, View.ALPHA, 0f));
-        set.setDuration(ANIMATION_DURATION);
-        set.addListener(
-                new AnimatorListenerAdapter() {
-                    @Override
-                    public void onAnimationEnd(Animator animation) {
-                        onMinimized();
-                    }
-                });
-        set.start();
-
-        mState = STATE_MINIMIZING;
-    }
-
-    @UiThread
     private void hide() {
-        if (DEBUG) {
-            Log.d(TAG, "Hide indicator");
-        }
+        if (DEBUG) Log.d(TAG, "Hide indicator");
+
         final int targetOffset = (mIsLtr ? 1 : -1) * (mIndicatorView.getWidth()
                 - (int) mIconTextsContainer.getTranslationX());
         final AnimatorSet set = new AnimatorSet();
@@ -477,51 +324,39 @@
         mState = STATE_DISAPPEARING;
     }
 
-    @UiThread
-    private void onExpanded() {
-        if (mState == STATE_STOPPED) {
-            return;
-        }
 
-        assertRevealingRecordingPackages();
+    @UiThread
+    private void onAppeared() {
+        if (mState == STATE_STOPPED) return;
 
         mState = STATE_SHOWN;
 
-        mIndicatorView.postDelayed(this::minimize, MAXIMIZED_DURATION);
-    }
-
-    @UiThread
-    private void onMinimized() {
-        if (mState == STATE_STOPPED) {
-            return;
-        }
-
-        mState = STATE_MINIMIZED;
-
-        if (mRevealRecordingPackages && !mPendingNotificationPackages.isEmpty()) {
-            // There is a new application that started recording, tell the user about it.
-            expand(mPendingNotificationPackages.poll());
-        } else {
-            hideIndicatorIfNeeded();
-        }
+        hideIndicatorIfNeeded();
     }
 
     @UiThread
     private void onHidden() {
-        if (mState == STATE_STOPPED) {
-            return;
-        }
+        if (mState == STATE_STOPPED) return;
 
         removeIndicatorView();
         mState = STATE_NOT_SHOWN;
 
-        // Check if anybody started recording while we were in STATE_DISAPPEARING
-        if (!mPendingNotificationPackages.isEmpty()) {
-            // There is a new application that started recording, tell the user about it.
-            show(mPendingNotificationPackages.poll());
+        if (hasActiveRecorders()) {
+            // Got new recorders, show again.
+            showIfNotShown();
         }
     }
 
+    private boolean hasActiveRecorders() {
+        for (int index = mAudioActivityObservers.length - 1; index >= 0; index--) {
+            for (String activePackage : mAudioActivityObservers[index].getActivePackages()) {
+                if (mExemptPackages.contains(activePackage)) continue;
+                return true;
+            }
+        }
+        return false;
+    }
+
     private void removeIndicatorView() {
         final WindowManager windowManager = (WindowManager) mContext.getSystemService(
                 Context.WINDOW_SERVICE);
@@ -536,26 +371,6 @@
         mBgEnd = null;
     }
 
-    private String getApplicationLabel(String packageName) {
-        assertRevealingRecordingPackages();
-
-        final PackageManager pm = mContext.getPackageManager();
-        final ApplicationInfo appInfo;
-        try {
-            appInfo = pm.getApplicationInfo(packageName, 0);
-        } catch (PackageManager.NameNotFoundException e) {
-            return packageName;
-        }
-        return pm.getApplicationLabel(appInfo).toString();
-    }
-
-    private void assertRevealingRecordingPackages() {
-        if (!mRevealRecordingPackages) {
-            Log.e(TAG, "Not revealing recording packages",
-                    DEBUG ? new RuntimeException("Should not be called") : null);
-        }
-    }
-
     private static List<String> splitByComma(String string) {
         return TextUtils.isEmpty(string) ? Collections.emptyList() : Arrays.asList(
                 string.split(","));
diff --git a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
index 0baecd5..228b2ea 100644
--- a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
@@ -45,12 +45,14 @@
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationLockscreenUserManagerImpl;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.phone.DozeServiceHost;
 import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.KeyguardEnvironmentImpl;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
+import com.android.systemui.statusbar.phone.NotificationShadeWindowControllerImpl;
 import com.android.systemui.statusbar.phone.ShadeController;
 import com.android.systemui.statusbar.phone.ShadeControllerImpl;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
@@ -162,5 +164,9 @@
             StatusBarKeyguardViewManager statusBarKeyguardViewManager);
 
     @Binds
+    abstract NotificationShadeWindowController bindNotificationShadeController(
+            NotificationShadeWindowControllerImpl notificationShadeWindowController);
+
+    @Binds
     abstract DozeHost provideDozeHost(DozeServiceHost dozeServiceHost);
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java
index 60f0cd9..e967a5d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ForegroundServiceControllerTest.java
@@ -82,8 +82,7 @@
         allowTestableLooperAsMainThread();
 
         MockitoAnnotations.initMocks(this);
-        mFsc = new ForegroundServiceController(
-                mEntryManager, mAppOpsController, mMainHandler);
+        mFsc = new ForegroundServiceController(mAppOpsController, mMainHandler);
         mListener = new ForegroundServiceNotificationListener(
                 mContext, mFsc, mEntryManager, mNotifPipeline,
                 mock(ForegroundServiceLifetimeExtender.class), mClock);
@@ -115,85 +114,6 @@
     }
 
     @Test
-    public void testAppOps_appOpChangedBeforeNotificationExists() {
-        // GIVEN app op exists, but notification doesn't exist in NEM yet
-        NotificationEntry entry = createFgEntry();
-        mFsc.onAppOpChanged(
-                AppOpsManager.OP_CAMERA,
-                entry.getSbn().getUid(),
-                entry.getSbn().getPackageName(),
-                true);
-        assertFalse(entry.mActiveAppOps.contains(AppOpsManager.OP_CAMERA));
-
-        // WHEN the notification is added
-        mEntryListener.onPendingEntryAdded(entry);
-
-        // THEN the app op is added to the entry
-        Assert.assertTrue(entry.mActiveAppOps.contains(AppOpsManager.OP_CAMERA));
-    }
-
-    @Test
-    public void testAppOps_appOpAddedToForegroundNotif() {
-        // GIVEN a notification associated with a foreground service
-        NotificationEntry entry = addFgEntry();
-        when(mEntryManager.getPendingOrActiveNotif(entry.getKey())).thenReturn(entry);
-
-        // WHEN we are notified of a new app op for this notification
-        mFsc.onAppOpChanged(
-                AppOpsManager.OP_CAMERA,
-                entry.getSbn().getUid(),
-                entry.getSbn().getPackageName(),
-                true);
-
-        // THEN the app op is added to the entry
-        Assert.assertTrue(entry.mActiveAppOps.contains(AppOpsManager.OP_CAMERA));
-
-        // THEN notification views are updated since the notification is visible
-        verify(mEntryManager, times(1)).updateNotifications(anyString());
-    }
-
-    @Test
-    public void testAppOpsAlreadyAdded() {
-        // GIVEN a foreground service associated notification that already has the correct app op
-        NotificationEntry entry = addFgEntry();
-        entry.mActiveAppOps.add(AppOpsManager.OP_CAMERA);
-        when(mEntryManager.getPendingOrActiveNotif(entry.getKey())).thenReturn(entry);
-
-        // WHEN we are notified of the same app op for this notification
-        mFsc.onAppOpChanged(
-                AppOpsManager.OP_CAMERA,
-                entry.getSbn().getUid(),
-                entry.getSbn().getPackageName(),
-                true);
-
-        // THEN the app op still exists in the notification entry
-        Assert.assertTrue(entry.mActiveAppOps.contains(AppOpsManager.OP_CAMERA));
-
-        // THEN notification views aren't updated since nothing changed
-        verify(mEntryManager, never()).updateNotifications(anyString());
-    }
-
-    @Test
-    public void testAppOps_appOpNotAddedToUnrelatedNotif() {
-        // GIVEN no notification entries correspond to the newly updated appOp
-        NotificationEntry entry = addFgEntry();
-        when(mEntryManager.getPendingOrActiveNotif(entry.getKey())).thenReturn(null);
-
-        // WHEN a new app op is detected
-        mFsc.onAppOpChanged(
-                AppOpsManager.OP_CAMERA,
-                entry.getSbn().getUid(),
-                entry.getSbn().getPackageName(),
-                true);
-
-        // THEN we won't see appOps on the entry
-        Assert.assertFalse(entry.mActiveAppOps.contains(AppOpsManager.OP_CAMERA));
-
-        // THEN notification views aren't updated since nothing changed
-        verify(mEntryManager, never()).updateNotifications(anyString());
-    }
-
-    @Test
     public void testAppOpsCRUD() {
         // no crash on remove that doesn't exist
         mFsc.onAppOpChanged(9, 1000, "pkg1", false);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
index 5c86fcb..a7808ad 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
@@ -60,7 +60,6 @@
 
 import com.android.internal.colorextraction.ColorExtractor;
 import com.android.internal.statusbar.IStatusBarService;
-import com.android.systemui.SystemUIFactory;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dump.DumpManager;
@@ -71,9 +70,7 @@
 import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationRemoveInterceptor;
-import com.android.systemui.statusbar.NotificationShelf;
 import com.android.systemui.statusbar.RankingBuilder;
-import com.android.systemui.statusbar.SuperStatusBarViewFactory;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.notification.NotificationEntryListener;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
@@ -87,7 +84,7 @@
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.LockscreenLockIconController;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
-import com.android.systemui.statusbar.phone.NotificationShadeWindowController;
+import com.android.systemui.statusbar.phone.NotificationShadeWindowControllerImpl;
 import com.android.systemui.statusbar.phone.NotificationShadeWindowView;
 import com.android.systemui.statusbar.phone.ShadeController;
 import com.android.systemui.statusbar.policy.BatteryController;
@@ -95,7 +92,6 @@
 import com.android.systemui.statusbar.policy.HeadsUpManager;
 import com.android.systemui.statusbar.policy.ZenModeController;
 import com.android.systemui.util.FloatingContentCoordinator;
-import com.android.systemui.util.InjectionInflationController;
 
 import com.google.common.collect.ImmutableList;
 
@@ -159,7 +155,7 @@
     private ArgumentCaptor<NotificationRemoveInterceptor> mRemoveInterceptorCaptor;
 
     private TestableBubbleController mBubbleController;
-    private NotificationShadeWindowController mNotificationShadeWindowController;
+    private NotificationShadeWindowControllerImpl mNotificationShadeWindowController;
     private NotificationEntryListener mEntryListener;
     private NotificationRemoveInterceptor mRemoveInterceptor;
 
@@ -199,8 +195,6 @@
 
     private TestableLooper mTestableLooper;
 
-    private SuperStatusBarViewFactory mSuperStatusBarViewFactory;
-
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
@@ -210,25 +204,8 @@
         mContext.addMockSystemService(FaceManager.class, mFaceManager);
         when(mColorExtractor.getNeutralColors()).thenReturn(mGradientColors);
 
-        mSuperStatusBarViewFactory = new SuperStatusBarViewFactory(mContext,
-                new InjectionInflationController(SystemUIFactory.getInstance().getRootComponent()
-                        .createViewInstanceCreatorFactory()),
-                new NotificationShelfComponent.Builder() {
-                    @Override
-                    public NotificationShelfComponent.Builder notificationShelf(
-                            NotificationShelf view) {
-                        return this;
-                    }
-
-                    @Override
-                    public NotificationShelfComponent build() {
-                        return mNotificationShelfComponent;
-                    }
-                },
-                mLockIconController);
-
         // Bubbles get added to status bar window view
-        mNotificationShadeWindowController = new NotificationShadeWindowController(mContext,
+        mNotificationShadeWindowController = new NotificationShadeWindowControllerImpl(mContext,
                 mWindowManager, mActivityManager, mDozeParameters, mStatusBarStateController,
                 mConfigurationController, mKeyguardViewMediator, mKeyguardBypassController,
                 mColorExtractor, mDumpManager);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java
index 196aa65..3df0df6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/NewNotifPipelineBubbleControllerTest.java
@@ -83,7 +83,7 @@
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.LockscreenLockIconController;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
-import com.android.systemui.statusbar.phone.NotificationShadeWindowController;
+import com.android.systemui.statusbar.phone.NotificationShadeWindowControllerImpl;
 import com.android.systemui.statusbar.phone.NotificationShadeWindowView;
 import com.android.systemui.statusbar.phone.ShadeController;
 import com.android.systemui.statusbar.policy.BatteryController;
@@ -153,7 +153,7 @@
     @Captor
     private ArgumentCaptor<NotifCollectionListener> mNotifListenerCaptor;
     private TestableBubbleController mBubbleController;
-    private NotificationShadeWindowController mNotificationShadeWindowController;
+    private NotificationShadeWindowControllerImpl mNotificationShadeWindowController;
     private NotifCollectionListener mEntryListener;
     private NotificationTestHelper mNotificationTestHelper;
     private ExpandableNotificationRow mRow;
@@ -218,7 +218,7 @@
                 mLockIconController);
 
         // Bubbles get added to status bar window view
-        mNotificationShadeWindowController = new NotificationShadeWindowController(mContext,
+        mNotificationShadeWindowController = new NotificationShadeWindowControllerImpl(mContext,
                 mWindowManager, mActivityManager, mDozeParameters, mStatusBarStateController,
                 mConfigurationController, mKeyguardViewMediator, mKeyguardBypassController,
                 mColorExtractor, mDumpManager);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/TestableBubbleController.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/TestableBubbleController.java
index 0a6d071..51ca2a4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/TestableBubbleController.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/TestableBubbleController.java
@@ -27,11 +27,11 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.FeatureFlags;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
-import com.android.systemui.statusbar.phone.NotificationShadeWindowController;
 import com.android.systemui.statusbar.phone.ShadeController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.ZenModeController;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogTest.java
index 4c54954..c8e0f49 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogTest.java
@@ -71,7 +71,7 @@
 import com.android.systemui.plugins.GlobalActionsPanelPlugin;
 import com.android.systemui.settings.CurrentUserContextTracker;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
-import com.android.systemui.statusbar.phone.NotificationShadeWindowController;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.RingerModeLiveData;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java
index 2e794a4..89538ac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java
@@ -20,7 +20,6 @@
 
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
@@ -34,19 +33,23 @@
 import com.android.systemui.SysuiTestCase;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
 
 import java.util.ArrayList;
-import java.util.Map;
 
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper
 public class MediaDataCombineLatestTest extends SysuiTestCase {
 
+    @Rule public MockitoRule mockito = MockitoJUnit.rule();
+
     private static final String KEY = "TEST_KEY";
     private static final String OLD_KEY = "TEST_KEY_OLD";
     private static final String APP = "APP";
@@ -59,27 +62,14 @@
 
     private MediaDataCombineLatest mManager;
 
-    @Mock private MediaDataManager mDataSource;
-    @Mock private MediaDeviceManager mDeviceSource;
     @Mock private MediaDataManager.Listener mListener;
 
-    private MediaDataManager.Listener mDataListener;
-    private MediaDeviceManager.Listener mDeviceListener;
-
     private MediaData mMediaData;
     private MediaDeviceData mDeviceData;
 
     @Before
     public void setUp() {
-        mDataSource = mock(MediaDataManager.class);
-        mDeviceSource = mock(MediaDeviceManager.class);
-        mListener = mock(MediaDataManager.Listener.class);
-
-        mManager = new MediaDataCombineLatest(mDataSource, mDeviceSource);
-
-        mDataListener = captureDataListener();
-        mDeviceListener = captureDeviceListener();
-
+        mManager = new MediaDataCombineLatest();
         mManager.addListener(mListener);
 
         mMediaData = new MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null,
@@ -91,7 +81,7 @@
     @Test
     public void eventNotEmittedWithoutDevice() {
         // WHEN data source emits an event without device data
-        mDataListener.onMediaDataLoaded(KEY, null, mMediaData);
+        mManager.onMediaDataLoaded(KEY, null, mMediaData);
         // THEN an event isn't emitted
         verify(mListener, never()).onMediaDataLoaded(eq(KEY), any(), any());
     }
@@ -99,7 +89,7 @@
     @Test
     public void eventNotEmittedWithoutMedia() {
         // WHEN device source emits an event without media data
-        mDeviceListener.onMediaDeviceChanged(KEY, null, mDeviceData);
+        mManager.onMediaDeviceChanged(KEY, null, mDeviceData);
         // THEN an event isn't emitted
         verify(mListener, never()).onMediaDataLoaded(eq(KEY), any(), any());
     }
@@ -107,9 +97,9 @@
     @Test
     public void emitEventAfterDeviceFirst() {
         // GIVEN that a device event has already been received
-        mDeviceListener.onMediaDeviceChanged(KEY, null, mDeviceData);
+        mManager.onMediaDeviceChanged(KEY, null, mDeviceData);
         // WHEN media event is received
-        mDataListener.onMediaDataLoaded(KEY, null, mMediaData);
+        mManager.onMediaDataLoaded(KEY, null, mMediaData);
         // THEN the listener receives a combined event
         ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
         verify(mListener).onMediaDataLoaded(eq(KEY), any(), captor.capture());
@@ -119,9 +109,9 @@
     @Test
     public void emitEventAfterMediaFirst() {
         // GIVEN that media event has already been received
-        mDataListener.onMediaDataLoaded(KEY, null, mMediaData);
+        mManager.onMediaDataLoaded(KEY, null, mMediaData);
         // WHEN device event is received
-        mDeviceListener.onMediaDeviceChanged(KEY, null, mDeviceData);
+        mManager.onMediaDeviceChanged(KEY, null, mDeviceData);
         // THEN the listener receives a combined event
         ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
         verify(mListener).onMediaDataLoaded(eq(KEY), any(), captor.capture());
@@ -131,11 +121,11 @@
     @Test
     public void migrateKeyMediaFirst() {
         // GIVEN that media and device info has already been received
-        mDataListener.onMediaDataLoaded(OLD_KEY, null, mMediaData);
-        mDeviceListener.onMediaDeviceChanged(OLD_KEY, null, mDeviceData);
+        mManager.onMediaDataLoaded(OLD_KEY, null, mMediaData);
+        mManager.onMediaDeviceChanged(OLD_KEY, null, mDeviceData);
         reset(mListener);
         // WHEN a key migration event is received
-        mDataListener.onMediaDataLoaded(KEY, OLD_KEY, mMediaData);
+        mManager.onMediaDataLoaded(KEY, OLD_KEY, mMediaData);
         // THEN the listener receives a combined event
         ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
         verify(mListener).onMediaDataLoaded(eq(KEY), eq(OLD_KEY), captor.capture());
@@ -145,11 +135,11 @@
     @Test
     public void migrateKeyDeviceFirst() {
         // GIVEN that media and device info has already been received
-        mDataListener.onMediaDataLoaded(OLD_KEY, null, mMediaData);
-        mDeviceListener.onMediaDeviceChanged(OLD_KEY, null, mDeviceData);
+        mManager.onMediaDataLoaded(OLD_KEY, null, mMediaData);
+        mManager.onMediaDeviceChanged(OLD_KEY, null, mDeviceData);
         reset(mListener);
         // WHEN a key migration event is received
-        mDeviceListener.onMediaDeviceChanged(KEY, OLD_KEY, mDeviceData);
+        mManager.onMediaDeviceChanged(KEY, OLD_KEY, mDeviceData);
         // THEN the listener receives a combined event
         ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
         verify(mListener).onMediaDataLoaded(eq(KEY), eq(OLD_KEY), captor.capture());
@@ -159,12 +149,12 @@
     @Test
     public void migrateKeyMediaAfter() {
         // GIVEN that media and device info has already been received
-        mDataListener.onMediaDataLoaded(OLD_KEY, null, mMediaData);
-        mDeviceListener.onMediaDeviceChanged(OLD_KEY, null, mDeviceData);
-        mDeviceListener.onMediaDeviceChanged(KEY, OLD_KEY, mDeviceData);
+        mManager.onMediaDataLoaded(OLD_KEY, null, mMediaData);
+        mManager.onMediaDeviceChanged(OLD_KEY, null, mDeviceData);
+        mManager.onMediaDeviceChanged(KEY, OLD_KEY, mDeviceData);
         reset(mListener);
         // WHEN a second key migration event is received for media
-        mDataListener.onMediaDataLoaded(KEY, OLD_KEY, mMediaData);
+        mManager.onMediaDataLoaded(KEY, OLD_KEY, mMediaData);
         // THEN the key has already been migrated
         ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
         verify(mListener).onMediaDataLoaded(eq(KEY), eq(KEY), captor.capture());
@@ -174,12 +164,12 @@
     @Test
     public void migrateKeyDeviceAfter() {
         // GIVEN that media and device info has already been received
-        mDataListener.onMediaDataLoaded(OLD_KEY, null, mMediaData);
-        mDeviceListener.onMediaDeviceChanged(OLD_KEY, null, mDeviceData);
-        mDataListener.onMediaDataLoaded(KEY, OLD_KEY, mMediaData);
+        mManager.onMediaDataLoaded(OLD_KEY, null, mMediaData);
+        mManager.onMediaDeviceChanged(OLD_KEY, null, mDeviceData);
+        mManager.onMediaDataLoaded(KEY, OLD_KEY, mMediaData);
         reset(mListener);
         // WHEN a second key migration event is received for the device
-        mDeviceListener.onMediaDeviceChanged(KEY, OLD_KEY, mDeviceData);
+        mManager.onMediaDeviceChanged(KEY, OLD_KEY, mDeviceData);
         // THEN the key has already be migrated
         ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
         verify(mListener).onMediaDataLoaded(eq(KEY), eq(KEY), captor.capture());
@@ -189,60 +179,34 @@
     @Test
     public void mediaDataRemoved() {
         // WHEN media data is removed without first receiving device or data
-        mDataListener.onMediaDataRemoved(KEY);
+        mManager.onMediaDataRemoved(KEY);
         // THEN a removed event isn't emitted
         verify(mListener, never()).onMediaDataRemoved(eq(KEY));
     }
 
     @Test
     public void mediaDataRemovedAfterMediaEvent() {
-        mDataListener.onMediaDataLoaded(KEY, null, mMediaData);
-        mDataListener.onMediaDataRemoved(KEY);
+        mManager.onMediaDataLoaded(KEY, null, mMediaData);
+        mManager.onMediaDataRemoved(KEY);
         verify(mListener).onMediaDataRemoved(eq(KEY));
     }
 
     @Test
     public void mediaDataRemovedAfterDeviceEvent() {
-        mDeviceListener.onMediaDeviceChanged(KEY, null, mDeviceData);
-        mDataListener.onMediaDataRemoved(KEY);
+        mManager.onMediaDeviceChanged(KEY, null, mDeviceData);
+        mManager.onMediaDataRemoved(KEY);
         verify(mListener).onMediaDataRemoved(eq(KEY));
     }
 
     @Test
     public void mediaDataKeyUpdated() {
         // GIVEN that device and media events have already been received
-        mDataListener.onMediaDataLoaded(KEY, null, mMediaData);
-        mDeviceListener.onMediaDeviceChanged(KEY, null, mDeviceData);
+        mManager.onMediaDataLoaded(KEY, null, mMediaData);
+        mManager.onMediaDeviceChanged(KEY, null, mDeviceData);
         // WHEN the key is changed
-        mDataListener.onMediaDataLoaded("NEW_KEY", KEY, mMediaData);
+        mManager.onMediaDataLoaded("NEW_KEY", KEY, mMediaData);
         // THEN the listener gets a load event with the correct keys
         ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
         verify(mListener).onMediaDataLoaded(eq("NEW_KEY"), any(), captor.capture());
     }
-
-    @Test
-    public void getDataIncludesDevice() {
-        // GIVEN that device and media events have been received
-        mDeviceListener.onMediaDeviceChanged(KEY, null, mDeviceData);
-        mDataListener.onMediaDataLoaded(KEY, null, mMediaData);
-
-        // THEN the result of getData includes device info
-        Map<String, MediaData> results = mManager.getData();
-        assertThat(results.get(KEY)).isNotNull();
-        assertThat(results.get(KEY).getDevice()).isEqualTo(mDeviceData);
-    }
-
-    private MediaDataManager.Listener captureDataListener() {
-        ArgumentCaptor<MediaDataManager.Listener> captor = ArgumentCaptor.forClass(
-                MediaDataManager.Listener.class);
-        verify(mDataSource).addListener(captor.capture());
-        return captor.getValue();
-    }
-
-    private MediaDeviceManager.Listener captureDeviceListener() {
-        ArgumentCaptor<MediaDeviceManager.Listener> captor = ArgumentCaptor.forClass(
-                MediaDeviceManager.Listener.class);
-        verify(mDeviceSource).addListener(captor.capture());
-        return captor.getValue();
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt
index afb64a7..36b6527 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataFilterTest.kt
@@ -32,6 +32,7 @@
 import org.mockito.Mockito
 import org.mockito.Mockito.`when`
 import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 import java.util.concurrent.Executor
@@ -56,8 +57,6 @@
 class MediaDataFilterTest : SysuiTestCase() {
 
     @Mock
-    private lateinit var combineLatest: MediaDataCombineLatest
-    @Mock
     private lateinit var listener: MediaDataManager.Listener
     @Mock
     private lateinit var broadcastDispatcher: BroadcastDispatcher
@@ -78,8 +77,9 @@
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
-        mediaDataFilter = MediaDataFilter(combineLatest, broadcastDispatcher, mediaResumeListener,
-            mediaDataManager, lockscreenUserManager, executor)
+        mediaDataFilter = MediaDataFilter(broadcastDispatcher, mediaResumeListener,
+                lockscreenUserManager, executor)
+        mediaDataFilter.mediaDataManager = mediaDataManager
         mediaDataFilter.addListener(listener)
 
         // Start all tests as main user
@@ -152,8 +152,9 @@
     @Test
     fun testOnUserSwitched_addsNewUserControls() {
         // GIVEN that we had some media for both users
-        val dataMap = mapOf(KEY to dataMain, KEY_ALT to dataGuest)
-        `when`(combineLatest.getData()).thenReturn(dataMap)
+        mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain)
+        mediaDataFilter.onMediaDataLoaded(KEY_ALT, null, dataGuest)
+        reset(listener)
 
         // and we switch to guest user
         setUser(USER_GUEST)
@@ -213,4 +214,4 @@
 
         verify(mediaDataManager).setTimedOut(eq(KEY), eq(true))
     }
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
index 457d559..84c1bf9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
@@ -16,6 +16,7 @@
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.statusbar.SbnBuilder
 import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
@@ -24,9 +25,13 @@
 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.Mockito
 import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
 import org.mockito.Mockito.verify
 import org.mockito.junit.MockitoJUnit
 import org.mockito.Mockito.`when` as whenever
@@ -48,6 +53,7 @@
 @RunWith(AndroidTestingRunner::class)
 class MediaDataManagerTest : SysuiTestCase() {
 
+    @JvmField @Rule val mockito = MockitoJUnit.rule()
     @Mock lateinit var mediaControllerFactory: MediaControllerFactory
     @Mock lateinit var controller: MediaController
     lateinit var session: MediaSession
@@ -58,20 +64,38 @@
     @Mock lateinit var broadcastDispatcher: BroadcastDispatcher
     @Mock lateinit var mediaTimeoutListener: MediaTimeoutListener
     @Mock lateinit var mediaResumeListener: MediaResumeListener
+    @Mock lateinit var mediaSessionBasedFilter: MediaSessionBasedFilter
+    @Mock lateinit var mediaDeviceManager: MediaDeviceManager
+    @Mock lateinit var mediaDataCombineLatest: MediaDataCombineLatest
+    @Mock lateinit var mediaDataFilter: MediaDataFilter
+    @Mock lateinit var listener: MediaDataManager.Listener
     @Mock lateinit var pendingIntent: PendingIntent
     @Mock lateinit var activityStarter: ActivityStarter
-    @JvmField @Rule val mockito = MockitoJUnit.rule()
     lateinit var mediaDataManager: MediaDataManager
     lateinit var mediaNotification: StatusBarNotification
+    @Captor lateinit var mediaDataCaptor: ArgumentCaptor<MediaData>
 
     @Before
     fun setup() {
         foregroundExecutor = FakeExecutor(FakeSystemClock())
         backgroundExecutor = FakeExecutor(FakeSystemClock())
-        mediaDataManager = MediaDataManager(context, backgroundExecutor, foregroundExecutor,
-                mediaControllerFactory, broadcastDispatcher, dumpManager,
-                mediaTimeoutListener, mediaResumeListener, activityStarter,
-                useMediaResumption = true, useQsMediaPlayer = true)
+        mediaDataManager = MediaDataManager(
+            context = context,
+            backgroundExecutor = backgroundExecutor,
+            foregroundExecutor = foregroundExecutor,
+            mediaControllerFactory = mediaControllerFactory,
+            broadcastDispatcher = broadcastDispatcher,
+            dumpManager = dumpManager,
+            mediaTimeoutListener = mediaTimeoutListener,
+            mediaResumeListener = mediaResumeListener,
+            mediaSessionBasedFilter = mediaSessionBasedFilter,
+            mediaDeviceManager = mediaDeviceManager,
+            mediaDataCombineLatest = mediaDataCombineLatest,
+            mediaDataFilter = mediaDataFilter,
+            activityStarter = activityStarter,
+            useMediaResumption = true,
+            useQsMediaPlayer = true
+        )
         session = MediaSession(context, "MediaDataManagerTestSession")
         mediaNotification = SbnBuilder().run {
             setPkg(PACKAGE_NAME)
@@ -86,6 +110,12 @@
             putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
         }
         whenever(mediaControllerFactory.create(eq(session.sessionToken))).thenReturn(controller)
+
+        // This is an ugly hack for now. The mediaSessionBasedFilter is one of the internal
+        // listeners in the internal processing pipeline. It receives events, but ince it is a
+        // mock, it doesn't pass those events along the chain to the external listeners. So, just
+        // treat mediaSessionBasedFilter as a listener for testing.
+        listener = mediaSessionBasedFilter
     }
 
     @After
@@ -115,8 +145,6 @@
 
     @Test
     fun testOnMetaDataLoaded_callsListener() {
-        val listener = mock(MediaDataManager.Listener::class.java)
-        mediaDataManager.addListener(listener)
         mediaDataManager.onNotificationAdded(KEY, mediaNotification)
         mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = mock(MediaData::class.java))
         verify(listener).onMediaDataLoaded(eq(KEY), eq(null), anyObject())
@@ -124,90 +152,81 @@
 
     @Test
     fun testOnMetaDataLoaded_conservesActiveFlag() {
-        val listener = TestListener()
         whenever(mediaControllerFactory.create(anyObject())).thenReturn(controller)
         whenever(controller.metadata).thenReturn(metadataBuilder.build())
         mediaDataManager.addListener(listener)
         mediaDataManager.onNotificationAdded(KEY, mediaNotification)
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
         assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
-        assertThat(listener.data!!.active).isTrue()
+        verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor))
+        assertThat(mediaDataCaptor.value!!.active).isTrue()
     }
 
     @Test
     fun testOnNotificationRemoved_callsListener() {
-        val listener = mock(MediaDataManager.Listener::class.java)
-        mediaDataManager.addListener(listener)
         mediaDataManager.onNotificationAdded(KEY, mediaNotification)
         mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = mock(MediaData::class.java))
         mediaDataManager.onNotificationRemoved(KEY)
-
         verify(listener).onMediaDataRemoved(eq(KEY))
     }
 
     @Test
     fun testOnNotificationRemoved_withResumption() {
         // GIVEN that the manager has a notification with a resume action
-        val listener = TestListener()
-        mediaDataManager.addListener(listener)
         whenever(controller.metadata).thenReturn(metadataBuilder.build())
         mediaDataManager.onNotificationAdded(KEY, mediaNotification)
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
         assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
-        val data = listener.data!!
+        verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor))
+        val data = mediaDataCaptor.value
         assertThat(data.resumption).isFalse()
         mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
         // WHEN the notification is removed
         mediaDataManager.onNotificationRemoved(KEY)
         // THEN the media data indicates that it is for resumption
-        assertThat(listener.data!!.resumption).isTrue()
-        // AND the new key is the package name
-        assertThat(listener.key!!).isEqualTo(PACKAGE_NAME)
-        assertThat(listener.oldKey!!).isEqualTo(KEY)
-        assertThat(listener.removedKey).isNull()
+        verify(listener).onMediaDataLoaded(eq(PACKAGE_NAME), eq(KEY), capture(mediaDataCaptor))
+        assertThat(mediaDataCaptor.value.resumption).isTrue()
     }
 
     @Test
     fun testOnNotificationRemoved_twoWithResumption() {
         // GIVEN that the manager has two notifications with resume actions
-        val listener = TestListener()
-        mediaDataManager.addListener(listener)
         whenever(controller.metadata).thenReturn(metadataBuilder.build())
         mediaDataManager.onNotificationAdded(KEY, mediaNotification)
         mediaDataManager.onNotificationAdded(KEY_2, mediaNotification)
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(2)
         assertThat(foregroundExecutor.runAllReady()).isEqualTo(2)
-        val data = listener.data!!
+        verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor))
+        val data = mediaDataCaptor.value
         assertThat(data.resumption).isFalse()
         val resumableData = data.copy(resumeAction = Runnable {})
         mediaDataManager.onMediaDataLoaded(KEY, null, resumableData)
         mediaDataManager.onMediaDataLoaded(KEY_2, null, resumableData)
+        reset(listener)
         // WHEN the first is removed
         mediaDataManager.onNotificationRemoved(KEY)
         // THEN the data is for resumption and the key is migrated to the package name
-        assertThat(listener.data!!.resumption).isTrue()
-        assertThat(listener.key!!).isEqualTo(PACKAGE_NAME)
-        assertThat(listener.oldKey!!).isEqualTo(KEY)
-        assertThat(listener.removedKey).isNull()
+        verify(listener).onMediaDataLoaded(eq(PACKAGE_NAME), eq(KEY), capture(mediaDataCaptor))
+        assertThat(mediaDataCaptor.value.resumption).isTrue()
+        verify(listener, never()).onMediaDataRemoved(eq(KEY))
         // WHEN the second is removed
         mediaDataManager.onNotificationRemoved(KEY_2)
         // THEN the data is for resumption and the second key is removed
-        assertThat(listener.data!!.resumption).isTrue()
-        assertThat(listener.key!!).isEqualTo(PACKAGE_NAME)
-        assertThat(listener.oldKey!!).isEqualTo(PACKAGE_NAME)
-        assertThat(listener.removedKey!!).isEqualTo(KEY_2)
+        verify(listener).onMediaDataLoaded(eq(PACKAGE_NAME), eq(PACKAGE_NAME),
+                capture(mediaDataCaptor))
+        assertThat(mediaDataCaptor.value.resumption).isTrue()
+        verify(listener).onMediaDataRemoved(eq(KEY_2))
     }
 
     @Test
     fun testAppBlockedFromResumption() {
         // GIVEN that the manager has a notification with a resume action
-        val listener = TestListener()
-        mediaDataManager.addListener(listener)
         whenever(controller.metadata).thenReturn(metadataBuilder.build())
         mediaDataManager.onNotificationAdded(KEY, mediaNotification)
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
         assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
-        val data = listener.data!!
+        verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor))
+        val data = mediaDataCaptor.value
         assertThat(data.resumption).isFalse()
         mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
 
@@ -219,7 +238,7 @@
         mediaDataManager.onNotificationRemoved(KEY)
 
         // THEN the media data is removed
-        assertThat(listener.removedKey!!).isEqualTo(KEY)
+        verify(listener).onMediaDataRemoved(eq(KEY))
     }
 
     @Test
@@ -229,13 +248,12 @@
         mediaDataManager.appsBlockedFromResume = blocked
 
         // and GIVEN that the manager has a notification from that app with a resume action
-        val listener = TestListener()
-        mediaDataManager.addListener(listener)
         whenever(controller.metadata).thenReturn(metadataBuilder.build())
         mediaDataManager.onNotificationAdded(KEY, mediaNotification)
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
         assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
-        val data = listener.data!!
+        verify(listener).onMediaDataLoaded(eq(KEY), eq(null), capture(mediaDataCaptor))
+        val data = mediaDataCaptor.value
         assertThat(data.resumption).isFalse()
         mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
 
@@ -246,14 +264,11 @@
         mediaDataManager.onNotificationRemoved(KEY)
 
         // THEN the entry will stay as a resume control
-        assertThat(listener.key!!).isEqualTo(PACKAGE_NAME)
-        assertThat(listener.oldKey!!).isEqualTo(KEY)
+        verify(listener).onMediaDataLoaded(eq(PACKAGE_NAME), eq(KEY), capture(mediaDataCaptor))
     }
 
     @Test
     fun testAddResumptionControls() {
-        val listener = TestListener()
-        mediaDataManager.addListener(listener)
         // WHEN resumption controls are added`
         val desc = MediaDescription.Builder().run {
             setTitle(SESSION_TITLE)
@@ -264,7 +279,8 @@
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
         assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
         // THEN the media data indicates that it is for resumption
-        val data = listener.data!!
+        verify(listener).onMediaDataLoaded(eq(PACKAGE_NAME), eq(null), capture(mediaDataCaptor))
+        val data = mediaDataCaptor.value
         assertThat(data.resumption).isTrue()
         assertThat(data.song).isEqualTo(SESSION_TITLE)
         assertThat(data.app).isEqualTo(APP_NAME)
@@ -273,8 +289,6 @@
 
     @Test
     fun testDismissMedia_listenerCalled() {
-        val listener = mock(MediaDataManager.Listener::class.java)
-        mediaDataManager.addListener(listener)
         mediaDataManager.onNotificationAdded(KEY, mediaNotification)
         mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = mock(MediaData::class.java))
         mediaDataManager.dismissMediaData(KEY, 0L)
@@ -284,26 +298,4 @@
 
         verify(listener).onMediaDataRemoved(eq(KEY))
     }
-
-    /**
-     * Simple implementation of [MediaDataManager.Listener] for the test.
-     *
-     * Giving up on trying to get a mock Listener and ArgumentCaptor to work.
-     */
-    private class TestListener : MediaDataManager.Listener {
-        var data: MediaData? = null
-        var key: String? = null
-        var oldKey: String? = null
-        var removedKey: String? = null
-
-        override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
-            this.key = key
-            this.oldKey = oldKey
-            this.data = data
-        }
-
-        override fun onMediaDataRemoved(key: String) {
-            removedKey = key
-        }
-    }
 }
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 7bc15dd..fdb432c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
@@ -68,7 +68,6 @@
 public class MediaDeviceManagerTest : SysuiTestCase() {
 
     private lateinit var manager: MediaDeviceManager
-    @Mock private lateinit var mediaDataManager: MediaDataManager
     @Mock private lateinit var lmmFactory: LocalMediaManagerFactory
     @Mock private lateinit var lmm: LocalMediaManager
     @Mock private lateinit var mr2: MediaRouter2Manager
@@ -91,7 +90,7 @@
         fakeFgExecutor = FakeExecutor(FakeSystemClock())
         fakeBgExecutor = FakeExecutor(FakeSystemClock())
         manager = MediaDeviceManager(context, lmmFactory, mr2, fakeFgExecutor, fakeBgExecutor,
-                mediaDataManager, dumpster)
+                dumpster)
         manager.addListener(listener)
 
         // Configure mocks.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaSessionBasedFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaSessionBasedFilterTest.kt
new file mode 100644
index 0000000..887cc77
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaSessionBasedFilterTest.kt
@@ -0,0 +1,383 @@
+/*
+ * 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.media
+
+import android.graphics.Color
+import android.media.session.MediaController
+import android.media.session.MediaController.PlaybackInfo
+import android.media.session.MediaSession
+import android.media.session.MediaSessionManager
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.time.FakeSystemClock
+
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.any
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+import org.mockito.junit.MockitoJUnit
+import org.mockito.Mockito.`when` as whenever
+
+private const val PACKAGE = "PKG"
+private const val KEY = "TEST_KEY"
+private const val NOTIF_KEY = "TEST_KEY"
+private const val SESSION_ARTIST = "SESSION_ARTIST"
+private const val SESSION_TITLE = "SESSION_TITLE"
+private const val APP_NAME = "APP_NAME"
+private const val USER_ID = 0
+
+private val info = MediaData(
+    userId = USER_ID,
+    initialized = true,
+    backgroundColor = Color.DKGRAY,
+    app = APP_NAME,
+    appIcon = null,
+    artist = SESSION_ARTIST,
+    song = SESSION_TITLE,
+    artwork = null,
+    actions = emptyList(),
+    actionsToShowInCompact = emptyList(),
+    packageName = PACKAGE,
+    token = null,
+    clickIntent = null,
+    device = null,
+    active = true,
+    resumeAction = null,
+    resumption = false,
+    notificationKey = NOTIF_KEY,
+    hasCheckedForResume = false
+)
+
+private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper
+public class MediaSessionBasedFilterTest : SysuiTestCase() {
+
+    @JvmField @Rule val mockito = MockitoJUnit.rule()
+
+    // Unit to be tested
+    private lateinit var filter: MediaSessionBasedFilter
+
+    private lateinit var sessionListener: MediaSessionManager.OnActiveSessionsChangedListener
+    @Mock private lateinit var mediaListener: MediaDataManager.Listener
+
+    // MediaSessionBasedFilter dependencies
+    @Mock private lateinit var mediaSessionManager: MediaSessionManager
+    private lateinit var fgExecutor: FakeExecutor
+    private lateinit var bgExecutor: FakeExecutor
+
+    @Mock private lateinit var controller1: MediaController
+    @Mock private lateinit var controller2: MediaController
+    @Mock private lateinit var controller3: MediaController
+    @Mock private lateinit var controller4: MediaController
+
+    private lateinit var token1: MediaSession.Token
+    private lateinit var token2: MediaSession.Token
+    private lateinit var token3: MediaSession.Token
+    private lateinit var token4: MediaSession.Token
+
+    @Mock private lateinit var remotePlaybackInfo: PlaybackInfo
+    @Mock private lateinit var localPlaybackInfo: PlaybackInfo
+
+    private lateinit var session1: MediaSession
+    private lateinit var session2: MediaSession
+    private lateinit var session3: MediaSession
+    private lateinit var session4: MediaSession
+
+    private lateinit var mediaData1: MediaData
+    private lateinit var mediaData2: MediaData
+    private lateinit var mediaData3: MediaData
+    private lateinit var mediaData4: MediaData
+
+    @Before
+    fun setUp() {
+        fgExecutor = FakeExecutor(FakeSystemClock())
+        bgExecutor = FakeExecutor(FakeSystemClock())
+        filter = MediaSessionBasedFilter(context, mediaSessionManager, fgExecutor, bgExecutor)
+
+        // Configure mocks.
+        whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(emptyList())
+
+        session1 = MediaSession(context, "MediaSessionBasedFilter1")
+        session2 = MediaSession(context, "MediaSessionBasedFilter2")
+        session3 = MediaSession(context, "MediaSessionBasedFilter3")
+        session4 = MediaSession(context, "MediaSessionBasedFilter4")
+
+        token1 = session1.sessionToken
+        token2 = session2.sessionToken
+        token3 = session3.sessionToken
+        token4 = session4.sessionToken
+
+        whenever(controller1.getSessionToken()).thenReturn(token1)
+        whenever(controller2.getSessionToken()).thenReturn(token2)
+        whenever(controller3.getSessionToken()).thenReturn(token3)
+        whenever(controller4.getSessionToken()).thenReturn(token4)
+
+        whenever(controller1.getPackageName()).thenReturn(PACKAGE)
+        whenever(controller2.getPackageName()).thenReturn(PACKAGE)
+        whenever(controller3.getPackageName()).thenReturn(PACKAGE)
+        whenever(controller4.getPackageName()).thenReturn(PACKAGE)
+
+        mediaData1 = info.copy(token = token1)
+        mediaData2 = info.copy(token = token2)
+        mediaData3 = info.copy(token = token3)
+        mediaData4 = info.copy(token = token4)
+
+        whenever(remotePlaybackInfo.getPlaybackType()).thenReturn(PlaybackInfo.PLAYBACK_TYPE_REMOTE)
+        whenever(localPlaybackInfo.getPlaybackType()).thenReturn(PlaybackInfo.PLAYBACK_TYPE_LOCAL)
+
+        whenever(controller1.getPlaybackInfo()).thenReturn(localPlaybackInfo)
+        whenever(controller2.getPlaybackInfo()).thenReturn(localPlaybackInfo)
+        whenever(controller3.getPlaybackInfo()).thenReturn(localPlaybackInfo)
+        whenever(controller4.getPlaybackInfo()).thenReturn(localPlaybackInfo)
+
+        // Capture listener
+        bgExecutor.runAllReady()
+        val listenerCaptor = ArgumentCaptor.forClass(
+                MediaSessionManager.OnActiveSessionsChangedListener::class.java)
+        verify(mediaSessionManager).addOnActiveSessionsChangedListener(
+                listenerCaptor.capture(), any())
+        sessionListener = listenerCaptor.value
+
+        filter.addListener(mediaListener)
+    }
+
+    @After
+    fun tearDown() {
+        session1.release()
+        session2.release()
+        session3.release()
+        session4.release()
+    }
+
+    @Test
+    fun noMediaSession_loadedEventNotFiltered() {
+        filter.onMediaDataLoaded(KEY, null, mediaData1)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1))
+    }
+
+    @Test
+    fun noMediaSession_removedEventNotFiltered() {
+        filter.onMediaDataRemoved(KEY)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        verify(mediaListener).onMediaDataRemoved(eq(KEY))
+    }
+
+    @Test
+    fun matchingMediaSession_loadedEventNotFiltered() {
+        // GIVEN an active session
+        val controllers = listOf(controller1)
+        whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
+        sessionListener.onActiveSessionsChanged(controllers)
+        // WHEN a loaded event is received that matches the session
+        filter.onMediaDataLoaded(KEY, null, mediaData1)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        // THEN the event is not filtered
+        verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1))
+    }
+
+    @Test
+    fun matchingMediaSession_removedEventNotFiltered() {
+        // GIVEN an active session
+        val controllers = listOf(controller1)
+        whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
+        sessionListener.onActiveSessionsChanged(controllers)
+        // WHEN a removed event is received
+        filter.onMediaDataRemoved(KEY)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        // THEN the event is not filtered
+        verify(mediaListener).onMediaDataRemoved(eq(KEY))
+    }
+
+    @Test
+    fun remoteSession_loadedEventNotFiltered() {
+        // GIVEN a remove session
+        whenever(controller1.getPlaybackInfo()).thenReturn(remotePlaybackInfo)
+        val controllers = listOf(controller1)
+        whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
+        sessionListener.onActiveSessionsChanged(controllers)
+        // WHEN a loaded event is received that matche the session
+        filter.onMediaDataLoaded(KEY, null, mediaData1)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        // THEN the event is not filtered
+        verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1))
+    }
+
+    @Test
+    fun remoteAndLocalSessions_localLoadedEventFiltered() {
+        // GIVEN remote and local sessions
+        whenever(controller1.getPlaybackInfo()).thenReturn(remotePlaybackInfo)
+        val controllers = listOf(controller1, controller2)
+        whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
+        sessionListener.onActiveSessionsChanged(controllers)
+        // WHEN a loaded event is received that matches the remote session
+        filter.onMediaDataLoaded(KEY, null, mediaData1)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        // THEN the event is not filtered
+        verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1))
+        // WHEN a loaded event is received that matches the local session
+        filter.onMediaDataLoaded(KEY, null, mediaData2)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        // THEN the event is filtered
+        verify(mediaListener, never()).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData2))
+    }
+
+    @Test
+    fun remoteAndLocalHaveDifferentKeys_localLoadedEventFiltered() {
+        // GIVEN remote and local sessions
+        val key1 = "KEY_1"
+        val key2 = "KEY_2"
+        whenever(controller1.getPlaybackInfo()).thenReturn(remotePlaybackInfo)
+        val controllers = listOf(controller1, controller2)
+        whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
+        sessionListener.onActiveSessionsChanged(controllers)
+        // WHEN a loaded event is received that matches the remote session
+        filter.onMediaDataLoaded(key1, null, mediaData1)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        // THEN the event is not filtered
+        verify(mediaListener).onMediaDataLoaded(eq(key1), eq(null), eq(mediaData1))
+        // WHEN a loaded event is received that matches the local session
+        filter.onMediaDataLoaded(key2, null, mediaData2)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        // THEN the event is filtered
+        verify(mediaListener, never()).onMediaDataLoaded(eq(key2), eq(null), eq(mediaData2))
+        // AND there should be a removed event for key2
+        verify(mediaListener).onMediaDataRemoved(eq(key2))
+    }
+
+    @Test
+    fun multipleRemoteSessions_loadedEventNotFiltered() {
+        // GIVEN two remote sessions
+        whenever(controller1.getPlaybackInfo()).thenReturn(remotePlaybackInfo)
+        whenever(controller2.getPlaybackInfo()).thenReturn(remotePlaybackInfo)
+        val controllers = listOf(controller1, controller2)
+        whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
+        sessionListener.onActiveSessionsChanged(controllers)
+        // WHEN a loaded event is received that matches the remote session
+        filter.onMediaDataLoaded(KEY, null, mediaData1)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        // THEN the event is not filtered
+        verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1))
+        // WHEN a loaded event is received that matches the local session
+        filter.onMediaDataLoaded(KEY, null, mediaData2)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        // THEN the event is not filtered
+        verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData2))
+    }
+
+    @Test
+    fun multipleOtherSessions_loadedEventNotFiltered() {
+        // GIVEN multiple active sessions from other packages
+        val controllers = listOf(controller1, controller2, controller3, controller4)
+        whenever(controller1.getPackageName()).thenReturn("PKG_1")
+        whenever(controller2.getPackageName()).thenReturn("PKG_2")
+        whenever(controller3.getPackageName()).thenReturn("PKG_3")
+        whenever(controller4.getPackageName()).thenReturn("PKG_4")
+        whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
+        sessionListener.onActiveSessionsChanged(controllers)
+        // WHEN a loaded event is received
+        filter.onMediaDataLoaded(KEY, null, mediaData1)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        // THEN the event is not filtered
+        verify(mediaListener).onMediaDataLoaded(eq(KEY), eq(null), eq(mediaData1))
+    }
+
+    @Test
+    fun doNotFilterDuringKeyMigration() {
+        val key1 = "KEY_1"
+        val key2 = "KEY_2"
+        // GIVEN a loaded event
+        filter.onMediaDataLoaded(key1, null, mediaData2)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        reset(mediaListener)
+        // GIVEN remote and local sessions
+        whenever(controller1.getPlaybackInfo()).thenReturn(remotePlaybackInfo)
+        val controllers = listOf(controller1, controller2)
+        whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
+        sessionListener.onActiveSessionsChanged(controllers)
+        // WHEN a loaded event is received that matches the local session but it is a key migration
+        filter.onMediaDataLoaded(key2, key1, mediaData2)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        // THEN the key migration event is fired
+        verify(mediaListener).onMediaDataLoaded(eq(key2), eq(key1), eq(mediaData2))
+    }
+
+    @Test
+    fun filterAfterKeyMigration() {
+        val key1 = "KEY_1"
+        val key2 = "KEY_2"
+        // GIVEN a loaded event
+        filter.onMediaDataLoaded(key1, null, mediaData1)
+        filter.onMediaDataLoaded(key1, null, mediaData2)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        reset(mediaListener)
+        // GIVEN remote and local sessions
+        whenever(controller1.getPlaybackInfo()).thenReturn(remotePlaybackInfo)
+        val controllers = listOf(controller1, controller2)
+        whenever(mediaSessionManager.getActiveSessions(any())).thenReturn(controllers)
+        sessionListener.onActiveSessionsChanged(controllers)
+        // GIVEN that the keys have been migrated
+        filter.onMediaDataLoaded(key2, key1, mediaData1)
+        filter.onMediaDataLoaded(key2, key1, mediaData2)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        reset(mediaListener)
+        // WHEN a loaded event is received that matches the local session
+        filter.onMediaDataLoaded(key2, null, mediaData2)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        // THEN the key migration event is filtered
+        verify(mediaListener, never()).onMediaDataLoaded(eq(key2), eq(null), eq(mediaData2))
+        // WHEN a loaded event is received that matches the remote session
+        filter.onMediaDataLoaded(key2, null, mediaData1)
+        bgExecutor.runAllReady()
+        fgExecutor.runAllReady()
+        // THEN the key migration event is fired
+        verify(mediaListener).onMediaDataLoaded(eq(key2), eq(null), eq(mediaData1))
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java
index 644ed3d..8089561 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java
@@ -34,7 +34,6 @@
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager.OnSettingsClickListener;
 import com.android.systemui.statusbar.notification.row.NotificationInfo.CheckSaveListener;
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
-import com.android.systemui.statusbar.phone.NotificationShadeWindowController;
 import com.android.systemui.statusbar.phone.ShadeController;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
index 56df193..a36a4c4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt
@@ -29,7 +29,6 @@
 import com.android.systemui.statusbar.notification.ActivityLaunchAnimator
 import com.android.systemui.statusbar.phone.BiometricUnlockController
 import com.android.systemui.statusbar.phone.DozeParameters
-import com.android.systemui.statusbar.phone.NotificationShadeWindowController
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.mockito.eq
 import org.junit.Before
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java
index 6f46923..c35ada7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java
@@ -212,19 +212,6 @@
     }
 
     @Test
-    public void testUpdateNotificationViews_appOps() throws Exception {
-        NotificationEntry entry0 = createEntry();
-        entry0.setRow(spy(entry0.getRow()));
-        when(mEntryManager.getVisibleNotifications()).thenReturn(
-                Lists.newArrayList(entry0));
-        mListContainer.addContainerView(entry0.getRow());
-
-        mViewHierarchyManager.updateNotificationViews();
-
-        verify(entry0.getRow(), times(1)).showAppOpsIcons(any());
-    }
-
-    @Test
     public void testReentrantCallsToOnDynamicPrivacyChangedPostForLater() {
         // GIVEN a ListContainer that will make a re-entrant call to updateNotificationViews()
         mMadeReentrantCall = false;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/AppOpsCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/AppOpsCoordinatorTest.java
index 960ea79..ce8ce2e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/AppOpsCoordinatorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/AppOpsCoordinatorTest.java
@@ -77,8 +77,6 @@
     private NotificationEntryBuilder mEntryBuilder;
     private AppOpsCoordinator mAppOpsCoordinator;
     private NotifFilter mForegroundFilter;
-    private NotifCollectionListener mNotifCollectionListener;
-    private AppOpsController.Callback mAppOpsCallback;
     private NotifLifetimeExtender mForegroundNotifLifetimeExtender;
     private NotifSection mFgsSection;
 
@@ -113,19 +111,6 @@
                 lifetimeExtenderCaptor.capture());
         mForegroundNotifLifetimeExtender = lifetimeExtenderCaptor.getValue();
 
-        // capture notifCollectionListener
-        ArgumentCaptor<NotifCollectionListener> notifCollectionCaptor =
-                ArgumentCaptor.forClass(NotifCollectionListener.class);
-        verify(mNotifPipeline, times(1)).addCollectionListener(
-                notifCollectionCaptor.capture());
-        mNotifCollectionListener = notifCollectionCaptor.getValue();
-
-        // capture app ops callback
-        ArgumentCaptor<AppOpsController.Callback> appOpsCaptor =
-                ArgumentCaptor.forClass(AppOpsController.Callback.class);
-        verify(mAppOpsController).addCallback(any(int[].class), appOpsCaptor.capture());
-        mAppOpsCallback = appOpsCaptor.getValue();
-
         mFgsSection = mAppOpsCoordinator.getSection();
     }
 
@@ -230,136 +215,6 @@
     }
 
     @Test
-    public void testAppOpsUpdateOnlyAppliedToRelevantNotificationWithStandardLayout() {
-        // GIVEN three current notifications, two with the same key but from different users
-        NotificationEntry entry1 = new NotificationEntryBuilder()
-                .setUser(new UserHandle(NOTIF_USER_ID))
-                .setPkg(TEST_PKG)
-                .setId(1)
-                .build();
-        NotificationEntry entry2 = new NotificationEntryBuilder()
-                .setUser(new UserHandle(NOTIF_USER_ID))
-                .setPkg(TEST_PKG)
-                .setId(2)
-                .build();
-        NotificationEntry entry3_diffUser = new NotificationEntryBuilder()
-                .setUser(new UserHandle(NOTIF_USER_ID + 1))
-                .setPkg(TEST_PKG)
-                .setId(2)
-                .build();
-        when(mNotifPipeline.getAllNotifs()).thenReturn(List.of(entry1, entry2, entry3_diffUser));
-
-        // GIVEN that only entry2 has a standard layout
-        when(mForegroundServiceController.getStandardLayoutKeys(NOTIF_USER_ID, TEST_PKG))
-                .thenReturn(new ArraySet<>(List.of(entry2.getKey())));
-
-        // WHEN a new app ops code comes in
-        mAppOpsCallback.onActiveStateChanged(47, NOTIF_USER_ID, TEST_PKG, true);
-        mExecutor.runAllReady();
-
-        // THEN entry2's app ops are updated, but no one else's are
-        assertEquals(
-                new ArraySet<>(),
-                entry1.mActiveAppOps);
-        assertEquals(
-                new ArraySet<>(List.of(47)),
-                entry2.mActiveAppOps);
-        assertEquals(
-                new ArraySet<>(),
-                entry3_diffUser.mActiveAppOps);
-    }
-
-    @Test
-    public void testAppOpsUpdateAppliedToAllNotificationsWithStandardLayouts() {
-        // GIVEN three notifications with standard layouts
-        NotificationEntry entry1 = new NotificationEntryBuilder()
-                .setUser(new UserHandle(NOTIF_USER_ID))
-                .setPkg(TEST_PKG)
-                .setId(1)
-                .build();
-        NotificationEntry entry2 = new NotificationEntryBuilder()
-                .setUser(new UserHandle(NOTIF_USER_ID))
-                .setPkg(TEST_PKG)
-                .setId(2)
-                .build();
-        NotificationEntry entry3 = new NotificationEntryBuilder()
-                .setUser(new UserHandle(NOTIF_USER_ID))
-                .setPkg(TEST_PKG)
-                .setId(3)
-                .build();
-        when(mNotifPipeline.getAllNotifs()).thenReturn(List.of(entry1, entry2, entry3));
-        when(mForegroundServiceController.getStandardLayoutKeys(NOTIF_USER_ID, TEST_PKG))
-                .thenReturn(new ArraySet<>(List.of(entry1.getKey(), entry2.getKey(),
-                        entry3.getKey())));
-
-        // WHEN a new app ops code comes in
-        mAppOpsCallback.onActiveStateChanged(47, NOTIF_USER_ID, TEST_PKG, true);
-        mExecutor.runAllReady();
-
-        // THEN all entries get updated
-        assertEquals(
-                new ArraySet<>(List.of(47)),
-                entry1.mActiveAppOps);
-        assertEquals(
-                new ArraySet<>(List.of(47)),
-                entry2.mActiveAppOps);
-        assertEquals(
-                new ArraySet<>(List.of(47)),
-                entry3.mActiveAppOps);
-    }
-
-    @Test
-    public void testAppOpsAreRemoved() {
-        // GIVEN One notification which is associated with app ops
-        NotificationEntry entry = new NotificationEntryBuilder()
-                .setUser(new UserHandle(NOTIF_USER_ID))
-                .setPkg(TEST_PKG)
-                .setId(2)
-                .build();
-        when(mNotifPipeline.getAllNotifs()).thenReturn(List.of(entry));
-        when(mForegroundServiceController.getStandardLayoutKeys(0, TEST_PKG))
-                .thenReturn(new ArraySet<>(List.of(entry.getKey())));
-
-        // GIVEN that the notification's app ops are already [47, 33]
-        mAppOpsCallback.onActiveStateChanged(47, NOTIF_USER_ID, TEST_PKG, true);
-        mAppOpsCallback.onActiveStateChanged(33, NOTIF_USER_ID, TEST_PKG, true);
-        mExecutor.runAllReady();
-        assertEquals(
-                new ArraySet<>(List.of(47, 33)),
-                entry.mActiveAppOps);
-
-        // WHEN one of the app ops is removed
-        mAppOpsCallback.onActiveStateChanged(47, NOTIF_USER_ID, TEST_PKG, false);
-        mExecutor.runAllReady();
-
-        // THEN the entry's active app ops are updated as well
-        assertEquals(
-                new ArraySet<>(List.of(33)),
-                entry.mActiveAppOps);
-    }
-
-    @Test
-    public void testNullAppOps() {
-        // GIVEN one notification with app ops
-        NotificationEntry entry = new NotificationEntryBuilder()
-                .setUser(new UserHandle(NOTIF_USER_ID))
-                .setPkg(TEST_PKG)
-                .setId(2)
-                .build();
-        entry.mActiveAppOps.clear();
-        entry.mActiveAppOps.addAll(List.of(47, 33));
-
-        // WHEN the notification is updated and the foreground service controller returns null for
-        // this notification
-        when(mForegroundServiceController.getAppOps(entry.getSbn().getUser().getIdentifier(),
-                entry.getSbn().getPackageName())).thenReturn(null);
-        mNotifCollectionListener.onEntryUpdated(entry);
-
-        // THEN the entry's active app ops is updated to empty
-        assertTrue(entry.mActiveAppOps.isEmpty());
-    }
-
-    @Test
     public void testIncludeFGSInSection_importanceDefault() {
         // GIVEN the notification represents a colorized foreground service with > min importance
         mEntryBuilder
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/AppOpsInfoTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/AppOpsInfoTest.java
deleted file mode 100644
index 43d8b50..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/AppOpsInfoTest.java
+++ /dev/null
@@ -1,230 +0,0 @@
-/*
- * Copyright (C) 2018 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.row;
-
-import static android.app.AppOpsManager.OP_CAMERA;
-import static android.app.AppOpsManager.OP_RECORD_AUDIO;
-import static android.app.AppOpsManager.OP_SYSTEM_ALERT_WINDOW;
-
-import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertTrue;
-
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.anyBoolean;
-import static org.mockito.Mockito.anyInt;
-import static org.mockito.Mockito.anyString;
-import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.app.Notification;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.graphics.drawable.Drawable;
-import android.os.UserHandle;
-import android.service.notification.StatusBarNotification;
-import android.test.suitebuilder.annotation.SmallTest;
-import android.testing.AndroidTestingRunner;
-import android.testing.UiThreadTest;
-import android.util.ArraySet;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import com.android.internal.logging.testing.UiEventLoggerFake;
-import com.android.systemui.R;
-import com.android.systemui.SysuiTestCase;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.concurrent.CountDownLatch;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@UiThreadTest
-public class AppOpsInfoTest extends SysuiTestCase {
-    private static final String TEST_PACKAGE_NAME = "test_package";
-    private static final int TEST_UID = 1;
-
-    private AppOpsInfo mAppOpsInfo;
-    private final PackageManager mMockPackageManager = mock(PackageManager.class);
-    private final NotificationGuts mGutsParent = mock(NotificationGuts.class);
-    private StatusBarNotification mSbn;
-    private UiEventLoggerFake mUiEventLogger = new UiEventLoggerFake();
-
-    @Before
-    public void setUp() throws Exception {
-        // Inflate the layout
-        final LayoutInflater layoutInflater = LayoutInflater.from(mContext);
-        mAppOpsInfo = (AppOpsInfo) layoutInflater.inflate(R.layout.app_ops_info, null);
-        mAppOpsInfo.setGutsParent(mGutsParent);
-
-        // PackageManager must return a packageInfo and applicationInfo.
-        final PackageInfo packageInfo = new PackageInfo();
-        packageInfo.packageName = TEST_PACKAGE_NAME;
-        when(mMockPackageManager.getPackageInfo(eq(TEST_PACKAGE_NAME), anyInt()))
-                .thenReturn(packageInfo);
-        final ApplicationInfo applicationInfo = new ApplicationInfo();
-        applicationInfo.uid = TEST_UID;  // non-zero
-        when(mMockPackageManager.getApplicationInfo(anyString(), anyInt())).thenReturn(
-                applicationInfo);
-
-        mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID, 0,
-                new Notification(), UserHandle.CURRENT, null, 0);
-    }
-
-    @Test
-    public void testBindNotification_SetsTextApplicationName() {
-        when(mMockPackageManager.getApplicationLabel(any())).thenReturn("App Name");
-        mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, mUiEventLogger, new ArraySet<>());
-        final TextView textView = mAppOpsInfo.findViewById(R.id.pkgname);
-        assertTrue(textView.getText().toString().contains("App Name"));
-    }
-
-    @Test
-    public void testBindNotification_SetsPackageIcon() {
-        final Drawable iconDrawable = mock(Drawable.class);
-        when(mMockPackageManager.getApplicationIcon(any(ApplicationInfo.class)))
-                .thenReturn(iconDrawable);
-        mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, mUiEventLogger, new ArraySet<>());
-        final ImageView iconView = mAppOpsInfo.findViewById(R.id.pkgicon);
-        assertEquals(iconDrawable, iconView.getDrawable());
-    }
-
-    @Test
-    public void testBindNotification_SetsOnClickListenerForSettings() throws Exception {
-        ArraySet<Integer> expectedOps = new ArraySet<>();
-        expectedOps.add(OP_CAMERA);
-        final CountDownLatch latch = new CountDownLatch(1);
-        mAppOpsInfo.bindGuts(mMockPackageManager, (View v, String pkg, int uid,
-                ArraySet<Integer> ops) -> {
-            assertEquals(TEST_PACKAGE_NAME, pkg);
-            assertEquals(expectedOps, ops);
-            assertEquals(TEST_UID, uid);
-            latch.countDown();
-        }, mSbn, mUiEventLogger, expectedOps);
-
-        final View settingsButton = mAppOpsInfo.findViewById(R.id.settings);
-        settingsButton.performClick();
-        // Verify that listener was triggered.
-        assertEquals(0, latch.getCount());
-    }
-
-    @Test
-    public void testBindNotification_LogsOpen() throws Exception {
-        mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, mUiEventLogger, new ArraySet<>());
-        assertEquals(1, mUiEventLogger.numLogs());
-        assertEquals(NotificationAppOpsEvent.NOTIFICATION_APP_OPS_OPEN.getId(),
-                mUiEventLogger.eventId(0));
-    }
-
-    @Test
-    public void testOk() {
-        ArraySet<Integer> expectedOps = new ArraySet<>();
-        expectedOps.add(OP_CAMERA);
-        final CountDownLatch latch = new CountDownLatch(1);
-        mAppOpsInfo.bindGuts(mMockPackageManager, (View v, String pkg, int uid,
-                ArraySet<Integer> ops) -> {
-            assertEquals(TEST_PACKAGE_NAME, pkg);
-            assertEquals(expectedOps, ops);
-            assertEquals(TEST_UID, uid);
-            latch.countDown();
-        }, mSbn, mUiEventLogger, expectedOps);
-
-        final View okButton = mAppOpsInfo.findViewById(R.id.ok);
-        okButton.performClick();
-        assertEquals(1, latch.getCount());
-        verify(mGutsParent, times(1)).closeControls(eq(okButton), anyBoolean());
-    }
-
-    @Test
-    public void testPrompt_camera() {
-        ArraySet<Integer> expectedOps = new ArraySet<>();
-        expectedOps.add(OP_CAMERA);
-        mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, mUiEventLogger, expectedOps);
-        TextView prompt = mAppOpsInfo.findViewById(R.id.prompt);
-        assertEquals("This app is using the camera.", prompt.getText());
-    }
-
-    @Test
-    public void testPrompt_mic() {
-        ArraySet<Integer> expectedOps = new ArraySet<>();
-        expectedOps.add(OP_RECORD_AUDIO);
-        mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, mUiEventLogger, expectedOps);
-        TextView prompt = mAppOpsInfo.findViewById(R.id.prompt);
-        assertEquals("This app is using the microphone.", prompt.getText());
-    }
-
-    @Test
-    public void testPrompt_overlay() {
-        ArraySet<Integer> expectedOps = new ArraySet<>();
-        expectedOps.add(OP_SYSTEM_ALERT_WINDOW);
-        mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, mUiEventLogger, expectedOps);
-        TextView prompt = mAppOpsInfo.findViewById(R.id.prompt);
-        assertEquals("This app is displaying over other apps on your screen.", prompt.getText());
-    }
-
-    @Test
-    public void testPrompt_camera_mic() {
-        ArraySet<Integer> expectedOps = new ArraySet<>();
-        expectedOps.add(OP_CAMERA);
-        expectedOps.add(OP_RECORD_AUDIO);
-        mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, mUiEventLogger, expectedOps);
-        TextView prompt = mAppOpsInfo.findViewById(R.id.prompt);
-        assertEquals("This app is using the microphone and camera.", prompt.getText());
-    }
-
-    @Test
-    public void testPrompt_camera_mic_overlay() {
-        ArraySet<Integer> expectedOps = new ArraySet<>();
-        expectedOps.add(OP_CAMERA);
-        expectedOps.add(OP_RECORD_AUDIO);
-        expectedOps.add(OP_SYSTEM_ALERT_WINDOW);
-        mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, mUiEventLogger, expectedOps);
-        TextView prompt = mAppOpsInfo.findViewById(R.id.prompt);
-        assertEquals("This app is displaying over other apps on your screen and using"
-                + " the microphone and camera.", prompt.getText());
-    }
-
-    @Test
-    public void testPrompt_camera_overlay() {
-        ArraySet<Integer> expectedOps = new ArraySet<>();
-        expectedOps.add(OP_CAMERA);
-        expectedOps.add(OP_SYSTEM_ALERT_WINDOW);
-        mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, mUiEventLogger, expectedOps);
-        TextView prompt = mAppOpsInfo.findViewById(R.id.prompt);
-        assertEquals("This app is displaying over other apps on your screen and using"
-                + " the camera.", prompt.getText());
-    }
-
-    @Test
-    public void testPrompt_mic_overlay() {
-        ArraySet<Integer> expectedOps = new ArraySet<>();
-        expectedOps.add(OP_RECORD_AUDIO);
-        expectedOps.add(OP_SYSTEM_ALERT_WINDOW);
-        mAppOpsInfo.bindGuts(mMockPackageManager, null, mSbn, mUiEventLogger, expectedOps);
-        TextView prompt = mAppOpsInfo.findViewById(R.id.prompt);
-        assertEquals("This app is displaying over other apps on your screen and using"
-                + " the microphone.", prompt.getText());
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
index dc4a6ca..f29b46c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
@@ -35,12 +35,10 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.app.AppOpsManager;
 import android.app.NotificationChannel;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.testing.TestableLooper.RunWithLooper;
-import android.util.ArraySet;
 import android.view.View;
 
 import androidx.test.filters.SmallTest;
@@ -213,46 +211,6 @@
     }
 
     @Test
-    public void testShowAppOps_noHeader() {
-        // public notification is custom layout - no header
-        mGroupRow.setSensitive(true, true);
-        mGroupRow.setAppOpsOnClickListener(null);
-        mGroupRow.showAppOpsIcons(null);
-    }
-
-    @Test
-    public void testShowAppOpsIcons_header() {
-        NotificationContentView publicLayout = mock(NotificationContentView.class);
-        mGroupRow.setPublicLayout(publicLayout);
-        NotificationContentView privateLayout = mock(NotificationContentView.class);
-        mGroupRow.setPrivateLayout(privateLayout);
-        NotificationChildrenContainer mockContainer = mock(NotificationChildrenContainer.class);
-        when(mockContainer.getNotificationChildCount()).thenReturn(1);
-        mGroupRow.setChildrenContainer(mockContainer);
-
-        ArraySet<Integer> ops = new ArraySet<>();
-        ops.add(AppOpsManager.OP_ANSWER_PHONE_CALLS);
-        mGroupRow.showAppOpsIcons(ops);
-
-        verify(mockContainer, times(1)).showAppOpsIcons(ops);
-        verify(privateLayout, times(1)).showAppOpsIcons(ops);
-        verify(publicLayout, times(1)).showAppOpsIcons(ops);
-
-    }
-
-    @Test
-    public void testAppOpsOnClick() {
-        ExpandableNotificationRow.CoordinateOnClickListener l = mock(
-                ExpandableNotificationRow.CoordinateOnClickListener.class);
-        View view = mock(View.class);
-
-        mGroupRow.setAppOpsOnClickListener(l);
-
-        mGroupRow.getAppOpsOnClickListener().onClick(view);
-        verify(l, times(1)).onClick(any(), anyInt(), anyInt(), any());
-    }
-
-    @Test
     public void testFeedback_noHeader() {
         // public notification is custom layout - no header
         mGroupRow.setSensitive(true, true);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java
index 6d4a711..c2091da 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java
@@ -76,32 +76,6 @@
 
     @Test
     @UiThreadTest
-    public void testShowAppOpsIcons() {
-        View mockContracted = mock(NotificationHeaderView.class);
-        when(mockContracted.findViewById(com.android.internal.R.id.mic))
-                .thenReturn(mockContracted);
-        View mockExpanded = mock(NotificationHeaderView.class);
-        when(mockExpanded.findViewById(com.android.internal.R.id.mic))
-                .thenReturn(mockExpanded);
-        View mockHeadsUp = mock(NotificationHeaderView.class);
-        when(mockHeadsUp.findViewById(com.android.internal.R.id.mic))
-                .thenReturn(mockHeadsUp);
-
-        mView.setContractedChild(mockContracted);
-        mView.setExpandedChild(mockExpanded);
-        mView.setHeadsUpChild(mockHeadsUp);
-
-        ArraySet<Integer> ops = new ArraySet<>();
-        ops.add(AppOpsManager.OP_RECORD_AUDIO);
-        mView.showAppOpsIcons(ops);
-
-        verify(mockContracted, times(1)).setVisibility(View.VISIBLE);
-        verify(mockExpanded, times(1)).setVisibility(View.VISIBLE);
-        verify(mockHeadsUp, times(1)).setVisibility(View.VISIBLE);
-    }
-
-    @Test
-    @UiThreadTest
     public void testShowFeedbackIcon() {
         View mockContracted = mock(NotificationHeaderView.class);
         when(mockContracted.findViewById(com.android.internal.R.id.feedback))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
index 8ccbb2e..fa2fa04 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
@@ -52,6 +52,7 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.SmartReplyController;
 import com.android.systemui.statusbar.notification.ConversationNotificationProcessor;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -68,7 +69,6 @@
 import com.android.systemui.statusbar.phone.HeadsUpManagerPhone;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
-import com.android.systemui.statusbar.phone.NotificationShadeWindowController;
 import com.android.systemui.statusbar.policy.SmartReplyConstants;
 
 import org.mockito.ArgumentCaptor;
@@ -422,7 +422,6 @@
                 mock(OnExpandClickListener.class),
                 mock(NotificationMediaManager.class),
                 mock(ExpandableNotificationRow.CoordinateOnClickListener.class),
-                mock(ExpandableNotificationRow.CoordinateOnClickListener.class),
                 mock(FalsingManager.class),
                 mStatusBarStateController,
                 mPeopleNotificationIdentifier);
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 64907ee..f1c8ece 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
@@ -43,6 +43,7 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.statusbar.NotificationMediaManager;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
 import org.junit.Before;
@@ -76,7 +77,7 @@
     @Mock
     private ScrimController mScrimController;
     @Mock
-    private StatusBar mStatusBar;
+    private BiometricUnlockController.BiometricModeListener mBiometricModeListener;
     @Mock
     private ShadeController mShadeController;
     @Mock
@@ -105,11 +106,12 @@
         mDependency.injectTestDependency(NotificationMediaManager.class, mMediaManager);
         res.addOverride(com.android.internal.R.integer.config_wakeUpDelayDoze, 0);
         mBiometricUnlockController = new BiometricUnlockController(mContext, mDozeScrimController,
-                mKeyguardViewMediator, mScrimController, mStatusBar, mShadeController,
+                mKeyguardViewMediator, mScrimController, mShadeController,
                 mNotificationShadeWindowController, mKeyguardStateController, mHandler,
                 mUpdateMonitor, res.getResources(), mKeyguardBypassController, mDozeParameters,
                 mMetricsLogger, mDumpManager);
         mBiometricUnlockController.setKeyguardViewController(mStatusBarKeyguardViewManager);
+        mBiometricUnlockController.setBiometricModeListener(mBiometricModeListener);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
index a6e2918..a5f4e51 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
@@ -40,6 +40,7 @@
 import com.android.systemui.doze.DozeLog;
 import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.PulseExpansionHandler;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.StatusBarStateControllerImpl;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java
index 6d642ec..48bf2db 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhoneTest.java
@@ -33,6 +33,7 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.AlertingNotificationManager;
 import com.android.systemui.statusbar.AlertingNotificationManagerTest;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.notification.VisualStabilityManager;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImplTest.java
similarity index 97%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImplTest.java
index 8c37cf1..fcea17c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImplTest.java
@@ -57,7 +57,7 @@
 @RunWith(AndroidTestingRunner.class)
 @RunWithLooper
 @SmallTest
-public class NotificationShadeWindowControllerTest extends SysuiTestCase {
+public class NotificationShadeWindowControllerImplTest extends SysuiTestCase {
 
     @Mock private WindowManager mWindowManager;
     @Mock private DozeParameters mDozeParameters;
@@ -72,7 +72,7 @@
     @Mock private DumpManager mDumpManager;
     @Captor private ArgumentCaptor<WindowManager.LayoutParams> mLayoutParameters;
 
-    private NotificationShadeWindowController mNotificationShadeWindowController;
+    private NotificationShadeWindowControllerImpl mNotificationShadeWindowController;
 
     @Before
     public void setUp() {
@@ -80,7 +80,7 @@
         when(mDozeParameters.getAlwaysOn()).thenReturn(true);
         when(mColorExtractor.getNeutralColors()).thenReturn(mGradientColors);
 
-        mNotificationShadeWindowController = new NotificationShadeWindowController(mContext,
+        mNotificationShadeWindowController = new NotificationShadeWindowControllerImpl(mContext,
                 mWindowManager, mActivityManager, mDozeParameters, mStatusBarStateController,
                 mConfigurationController, mKeyguardViewMediator, mKeyguardBypassController,
                 mColorExtractor, mDumpManager);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewTest.java
index 51900cb..ea57cc9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationShadeWindowViewTest.java
@@ -38,6 +38,7 @@
 import com.android.systemui.statusbar.DragDownHelper;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.PulseExpansionHandler;
 import com.android.systemui.statusbar.SuperStatusBarViewFactory;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index 50d891e..ccc3078 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -47,6 +47,7 @@
 import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.statusbar.NotificationMediaManager;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java
index e4f4812..e46aa04 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java
@@ -43,6 +43,7 @@
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.NotificationViewHierarchyManager;
 import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
index aa09406..8a1d95ea4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
@@ -104,6 +104,7 @@
 import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
+import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.NotificationViewHierarchyManager;
 import com.android.systemui.statusbar.PulseExpansionHandler;
 import com.android.systemui.statusbar.RemoteInputController;
diff --git a/services/core/java/com/android/server/VibratorService.java b/services/core/java/com/android/server/VibratorService.java
index 32d02fb..96d973e 100644
--- a/services/core/java/com/android/server/VibratorService.java
+++ b/services/core/java/com/android/server/VibratorService.java
@@ -127,6 +127,7 @@
     private final int mPreviousVibrationsLimit;
     private final boolean mAllowPriorityVibrationsInLowPowerMode;
     private final List<Integer> mSupportedEffects;
+    private final List<Integer> mSupportedPrimitives;
     private final long mCapabilities;
     private final int mDefaultVibrationAmplitude;
     private final SparseArray<VibrationEffect> mFallbackEffects;
@@ -184,6 +185,8 @@
 
     static native int[] vibratorGetSupportedEffects(long controllerPtr);
 
+    static native int[] vibratorGetSupportedPrimitives(long controllerPtr);
+
     static native long vibratorPerformEffect(
             long controllerPtr, long effect, long strength, Vibration vibration);
 
@@ -397,6 +400,7 @@
         mNativeWrapper.vibratorOff();
 
         mSupportedEffects = asList(mNativeWrapper.vibratorGetSupportedEffects());
+        mSupportedPrimitives = asList(mNativeWrapper.vibratorGetSupportedPrimitives());
         mCapabilities = mNativeWrapper.vibratorGetCapabilities();
 
         mContext = context;
@@ -647,8 +651,11 @@
     @Override // Binder call
     public boolean[] arePrimitivesSupported(int[] primitiveIds) {
         boolean[] supported = new boolean[primitiveIds.length];
-        if (hasCapability(IVibrator.CAP_COMPOSE_EFFECTS)) {
-            Arrays.fill(supported, true);
+        if (!hasCapability(IVibrator.CAP_COMPOSE_EFFECTS) || mSupportedPrimitives == null) {
+            return supported;
+        }
+        for (int i = 0; i < primitiveIds.length; i++) {
+            supported[i] = mSupportedPrimitives.contains(primitiveIds[i]);
         }
         return supported;
     }
@@ -1501,6 +1508,7 @@
             pw.println("  mNotificationIntensity=" + mNotificationIntensity);
             pw.println("  mRingIntensity=" + mRingIntensity);
             pw.println("  mSupportedEffects=" + mSupportedEffects);
+            pw.println("  mSupportedPrimitives=" + mSupportedPrimitives);
             pw.println();
             pw.println("  Previous ring vibrations:");
             for (VibrationInfo info : mPreviousRingVibrations) {
@@ -1759,6 +1767,11 @@
             return VibratorService.vibratorGetSupportedEffects(mNativeControllerPtr);
         }
 
+        /** Returns all compose primitives supported by the device vibrator. */
+        public int[] vibratorGetSupportedPrimitives() {
+            return VibratorService.vibratorGetSupportedPrimitives(mNativeControllerPtr);
+        }
+
         /** Turns vibrator on to perform one of the supported effects. */
         public long vibratorPerformEffect(long effect, long strength, Vibration vibration) {
             return VibratorService.vibratorPerformEffect(
diff --git a/services/core/java/com/android/server/am/UidObserverController.java b/services/core/java/com/android/server/am/UidObserverController.java
index 4c4dd8b..4d9260a 100644
--- a/services/core/java/com/android/server/am/UidObserverController.java
+++ b/services/core/java/com/android/server/am/UidObserverController.java
@@ -86,15 +86,17 @@
                 }
                 mService.mUiHandler.post(this::dispatchUidsChanged);
             }
-            final int NA = mAvailUidChanges.size();
-            if (NA > 0) {
-                pendingChange = mAvailUidChanges.remove(NA-1);
-                if (DEBUG_UID_OBSERVERS) Slog.i(TAG_UID_OBSERVERS,
-                        "Retrieving available item: " + pendingChange);
+            final int size = mAvailUidChanges.size();
+            if (size > 0) {
+                pendingChange = mAvailUidChanges.remove(size - 1);
+                if (DEBUG_UID_OBSERVERS) {
+                    Slog.i(TAG_UID_OBSERVERS, "Retrieving available item: " + pendingChange);
+                }
             } else {
                 pendingChange = new UidRecord.ChangeItem();
-                if (DEBUG_UID_OBSERVERS) Slog.i(TAG_UID_OBSERVERS,
-                        "Allocating new item: " + pendingChange);
+                if (DEBUG_UID_OBSERVERS) {
+                    Slog.i(TAG_UID_OBSERVERS, "Allocating new item: " + pendingChange);
+                }
             }
             if (uidRec != null) {
                 uidRec.pendingChange = pendingChange;
@@ -134,7 +136,8 @@
             }
         }
         pendingChange.change = change;
-        pendingChange.processState = uidRec != null ? uidRec.setProcState : PROCESS_STATE_NONEXISTENT;
+        pendingChange.processState = uidRec != null
+                ? uidRec.setProcState : PROCESS_STATE_NONEXISTENT;
         pendingChange.capability = uidRec != null ? uidRec.setCapability : 0;
         pendingChange.ephemeral = uidRec != null ? uidRec.ephemeral : isEphemeralLocked(uid);
         pendingChange.procStateSeq = uidRec != null ? uidRec.curProcStateSeq : 0;
@@ -165,13 +168,13 @@
 
     @VisibleForTesting
     void dispatchUidsChanged() {
-        int N;
+        int numUidChanges;
         synchronized (mService) {
-            N = mPendingUidChanges.size();
-            if (mActiveUidChanges.length < N) {
-                mActiveUidChanges = new UidRecord.ChangeItem[N];
+            numUidChanges = mPendingUidChanges.size();
+            if (mActiveUidChanges.length < numUidChanges) {
+                mActiveUidChanges = new UidRecord.ChangeItem[numUidChanges];
             }
-            for (int i=0; i<N; i++) {
+            for (int i = 0; i < numUidChanges; i++) {
                 final UidRecord.ChangeItem change = mPendingUidChanges.get(i);
                 mActiveUidChanges[i] = change;
                 if (change.uidRecord != null) {
@@ -180,21 +183,22 @@
                 }
             }
             mPendingUidChanges.clear();
-            if (DEBUG_UID_OBSERVERS) Slog.i(TAG_UID_OBSERVERS,
-                    "*** Delivering " + N + " uid changes");
+            if (DEBUG_UID_OBSERVERS) {
+                Slog.i(TAG_UID_OBSERVERS, "*** Delivering " + numUidChanges + " uid changes");
+            }
         }
 
-        mUidChangeDispatchCount += N;
+        mUidChangeDispatchCount += numUidChanges;
         int i = mUidObservers.beginBroadcast();
         while (i > 0) {
             i--;
             dispatchUidsChangedForObserver(mUidObservers.getBroadcastItem(i),
-                    (UidObserverRegistration) mUidObservers.getBroadcastCookie(i), N);
+                    (UidObserverRegistration) mUidObservers.getBroadcastCookie(i), numUidChanges);
         }
         mUidObservers.finishBroadcast();
 
         if (VALIDATE_UID_STATES && mUidObservers.getRegisteredCallbackCount() > 0) {
-            for (int j = 0; j < N; ++j) {
+            for (int j = 0; j < numUidChanges; ++j) {
                 final UidRecord.ChangeItem item = mActiveUidChanges[j];
                 if ((item.change & UidRecord.CHANGE_GONE) != 0) {
                     mValidateUids.remove(item.uid);
@@ -217,7 +221,7 @@
         }
 
         synchronized (mService) {
-            for (int j = 0; j < N; j++) {
+            for (int j = 0; j < numUidChanges; j++) {
                 mAvailUidChanges.add(mActiveUidChanges[j]);
             }
         }
@@ -232,66 +236,72 @@
             for (int j = 0; j < changesSize; j++) {
                 UidRecord.ChangeItem item = mActiveUidChanges[j];
                 final int change = item.change;
-                if (change == UidRecord.CHANGE_PROCSTATE &&
-                        (reg.which & ActivityManager.UID_OBSERVER_PROCSTATE) == 0) {
+                if (change == UidRecord.CHANGE_PROCSTATE
+                        && (reg.mWhich & ActivityManager.UID_OBSERVER_PROCSTATE) == 0) {
                     // No-op common case: no significant change, the observer is not
                     // interested in all proc state changes.
                     continue;
                 }
                 final long start = SystemClock.uptimeMillis();
                 if ((change & UidRecord.CHANGE_IDLE) != 0) {
-                    if ((reg.which & ActivityManager.UID_OBSERVER_IDLE) != 0) {
-                        if (DEBUG_UID_OBSERVERS) Slog.i(TAG_UID_OBSERVERS,
-                                "UID idle uid=" + item.uid);
+                    if ((reg.mWhich & ActivityManager.UID_OBSERVER_IDLE) != 0) {
+                        if (DEBUG_UID_OBSERVERS) {
+                            Slog.i(TAG_UID_OBSERVERS, "UID idle uid=" + item.uid);
+                        }
                         observer.onUidIdle(item.uid, item.ephemeral);
                     }
                 } else if ((change & UidRecord.CHANGE_ACTIVE) != 0) {
-                    if ((reg.which & ActivityManager.UID_OBSERVER_ACTIVE) != 0) {
-                        if (DEBUG_UID_OBSERVERS) Slog.i(TAG_UID_OBSERVERS,
-                                "UID active uid=" + item.uid);
+                    if ((reg.mWhich & ActivityManager.UID_OBSERVER_ACTIVE) != 0) {
+                        if (DEBUG_UID_OBSERVERS) {
+                            Slog.i(TAG_UID_OBSERVERS, "UID active uid=" + item.uid);
+                        }
                         observer.onUidActive(item.uid);
                     }
                 }
-                if ((reg.which & ActivityManager.UID_OBSERVER_CACHED) != 0) {
+                if ((reg.mWhich & ActivityManager.UID_OBSERVER_CACHED) != 0) {
                     if ((change & UidRecord.CHANGE_CACHED) != 0) {
-                        if (DEBUG_UID_OBSERVERS) Slog.i(TAG_UID_OBSERVERS,
-                                "UID cached uid=" + item.uid);
+                        if (DEBUG_UID_OBSERVERS) {
+                            Slog.i(TAG_UID_OBSERVERS, "UID cached uid=" + item.uid);
+                        }
                         observer.onUidCachedChanged(item.uid, true);
                     } else if ((change & UidRecord.CHANGE_UNCACHED) != 0) {
-                        if (DEBUG_UID_OBSERVERS) Slog.i(TAG_UID_OBSERVERS,
-                                "UID active uid=" + item.uid);
+                        if (DEBUG_UID_OBSERVERS) {
+                            Slog.i(TAG_UID_OBSERVERS, "UID active uid=" + item.uid);
+                        }
                         observer.onUidCachedChanged(item.uid, false);
                     }
                 }
                 if ((change & UidRecord.CHANGE_GONE) != 0) {
-                    if ((reg.which & ActivityManager.UID_OBSERVER_GONE) != 0) {
-                        if (DEBUG_UID_OBSERVERS) Slog.i(TAG_UID_OBSERVERS,
-                                "UID gone uid=" + item.uid);
+                    if ((reg.mWhich & ActivityManager.UID_OBSERVER_GONE) != 0) {
+                        if (DEBUG_UID_OBSERVERS) {
+                            Slog.i(TAG_UID_OBSERVERS, "UID gone uid=" + item.uid);
+                        }
                         observer.onUidGone(item.uid, item.ephemeral);
                     }
-                    if (reg.lastProcStates != null) {
-                        reg.lastProcStates.delete(item.uid);
+                    if (reg.mLastProcStates != null) {
+                        reg.mLastProcStates.delete(item.uid);
                     }
                 } else {
-                    if ((reg.which & ActivityManager.UID_OBSERVER_PROCSTATE) != 0) {
-                        if (DEBUG_UID_OBSERVERS) Slog.i(TAG_UID_OBSERVERS,
-                                "UID CHANGED uid=" + item.uid
-                                        + ": " + item.processState + ": " + item.capability);
+                    if ((reg.mWhich & ActivityManager.UID_OBSERVER_PROCSTATE) != 0) {
+                        if (DEBUG_UID_OBSERVERS) {
+                            Slog.i(TAG_UID_OBSERVERS, "UID CHANGED uid=" + item.uid
+                                    + ": " + item.processState + ": " + item.capability);
+                        }
                         boolean doReport = true;
-                        if (reg.cutpoint >= ActivityManager.MIN_PROCESS_STATE) {
-                            final int lastState = reg.lastProcStates.get(item.uid,
+                        if (reg.mCutpoint >= ActivityManager.MIN_PROCESS_STATE) {
+                            final int lastState = reg.mLastProcStates.get(item.uid,
                                     ActivityManager.PROCESS_STATE_UNKNOWN);
                             if (lastState != ActivityManager.PROCESS_STATE_UNKNOWN) {
-                                final boolean lastAboveCut = lastState <= reg.cutpoint;
-                                final boolean newAboveCut = item.processState <= reg.cutpoint;
+                                final boolean lastAboveCut = lastState <= reg.mCutpoint;
+                                final boolean newAboveCut = item.processState <= reg.mCutpoint;
                                 doReport = lastAboveCut != newAboveCut;
                             } else {
                                 doReport = item.processState != PROCESS_STATE_NONEXISTENT;
                             }
                         }
                         if (doReport) {
-                            if (reg.lastProcStates != null) {
-                                reg.lastProcStates.put(item.uid, item.processState);
+                            if (reg.mLastProcStates != null) {
+                                reg.mLastProcStates.put(item.uid, item.processState);
                             }
                             observer.onUidStateChanged(item.uid, item.processState,
                                     item.procStateSeq, item.capability);
@@ -311,7 +321,7 @@
     }
 
     private boolean isEphemeralLocked(int uid) {
-        String packages[] = mService.mContext.getPackageManager().getPackagesForUid(uid);
+        final String[] packages = mService.mContext.getPackageManager().getPackagesForUid(uid);
         if (packages == null || packages.length != 1) { // Ephemeral apps cannot share uid
             return false;
         }
@@ -321,41 +331,41 @@
 
     @GuardedBy("mService")
     void dump(PrintWriter pw, String dumpPackage) {
-        final int NI = mUidObservers.getRegisteredCallbackCount();
+        final int count = mUidObservers.getRegisteredCallbackCount();
         boolean printed = false;
-        for (int i=0; i<NI; i++) {
+        for (int i = 0; i < count; i++) {
             final UidObserverRegistration reg = (UidObserverRegistration)
                     mUidObservers.getRegisteredCallbackCookie(i);
-            if (dumpPackage == null || dumpPackage.equals(reg.pkg)) {
+            if (dumpPackage == null || dumpPackage.equals(reg.mPkg)) {
                 if (!printed) {
                     pw.println("  mUidObservers:");
                     printed = true;
                 }
-                pw.print("    "); UserHandle.formatUid(pw, reg.uid);
-                pw.print(" "); pw.print(reg.pkg);
+                pw.print("    "); UserHandle.formatUid(pw, reg.mUid);
+                pw.print(" "); pw.print(reg.mPkg);
                 final IUidObserver observer = mUidObservers.getRegisteredCallbackItem(i);
                 pw.print(" "); pw.print(observer.getClass().getTypeName()); pw.print(":");
-                if ((reg.which&ActivityManager.UID_OBSERVER_IDLE) != 0) {
+                if ((reg.mWhich & ActivityManager.UID_OBSERVER_IDLE) != 0) {
                     pw.print(" IDLE");
                 }
-                if ((reg.which&ActivityManager.UID_OBSERVER_ACTIVE) != 0) {
-                    pw.print(" ACT" );
+                if ((reg.mWhich & ActivityManager.UID_OBSERVER_ACTIVE) != 0) {
+                    pw.print(" ACT");
                 }
-                if ((reg.which&ActivityManager.UID_OBSERVER_GONE) != 0) {
+                if ((reg.mWhich & ActivityManager.UID_OBSERVER_GONE) != 0) {
                     pw.print(" GONE");
                 }
-                if ((reg.which&ActivityManager.UID_OBSERVER_PROCSTATE) != 0) {
+                if ((reg.mWhich & ActivityManager.UID_OBSERVER_PROCSTATE) != 0) {
                     pw.print(" STATE");
-                    pw.print(" (cut="); pw.print(reg.cutpoint);
+                    pw.print(" (cut="); pw.print(reg.mCutpoint);
                     pw.print(")");
                 }
                 pw.println();
-                if (reg.lastProcStates != null) {
-                    final int NJ = reg.lastProcStates.size();
-                    for (int j=0; j<NJ; j++) {
+                if (reg.mLastProcStates != null) {
+                    final int size = reg.mLastProcStates.size();
+                    for (int j = 0; j < size; j++) {
                         pw.print("      Last ");
-                        UserHandle.formatUid(pw, reg.lastProcStates.keyAt(j));
-                        pw.print(": "); pw.println(reg.lastProcStates.valueAt(j));
+                        UserHandle.formatUid(pw, reg.mLastProcStates.keyAt(j));
+                        pw.print(": "); pw.println(reg.mLastProcStates.valueAt(j));
                     }
                 }
             }
@@ -366,8 +376,8 @@
         pw.print(mUidChangeDispatchCount);
         pw.println();
         pw.println("  Slow UID dispatches:");
-        final int N = mUidObservers.beginBroadcast();
-        for (int i = 0; i < N; i++) {
+        final int size = mUidObservers.beginBroadcast();
+        for (int i = 0; i < size; i++) {
             UidObserverRegistration r =
                     (UidObserverRegistration) mUidObservers.getBroadcastCookie(i);
             pw.print("    ");
@@ -383,21 +393,21 @@
 
     @GuardedBy("mService")
     void dumpDebug(ProtoOutputStream proto, String dumpPackage) {
-        final int NI = mUidObservers.getRegisteredCallbackCount();
-        for (int i=0; i<NI; i++) {
+        final int count = mUidObservers.getRegisteredCallbackCount();
+        for (int i = 0; i < count; i++) {
             final UidObserverRegistration reg = (UidObserverRegistration)
                     mUidObservers.getRegisteredCallbackCookie(i);
-            if (dumpPackage == null || dumpPackage.equals(reg.pkg)) {
+            if (dumpPackage == null || dumpPackage.equals(reg.mPkg)) {
                 reg.dumpDebug(proto, ActivityManagerServiceDumpProcessesProto.UID_OBSERVERS);
             }
         }
     }
 
-    static final class UidObserverRegistration {
-        final int uid;
-        final String pkg;
-        final int which;
-        final int cutpoint;
+    private static final class UidObserverRegistration {
+        final int mUid;
+        final String mPkg;
+        final int mWhich;
+        final int mCutpoint;
 
         /**
          * Total # of callback calls that took more than {@link #SLOW_UID_OBSERVER_THRESHOLD_MS}.
@@ -408,49 +418,49 @@
         /** Max time it took for each dispatch. */
         int mMaxDispatchTime;
 
-        final SparseIntArray lastProcStates;
+        final SparseIntArray mLastProcStates;
 
         // Please keep the enum lists in sync
-        private static int[] ORIG_ENUMS = new int[]{
+        private static final int[] ORIG_ENUMS = new int[]{
                 ActivityManager.UID_OBSERVER_IDLE,
                 ActivityManager.UID_OBSERVER_ACTIVE,
                 ActivityManager.UID_OBSERVER_GONE,
                 ActivityManager.UID_OBSERVER_PROCSTATE,
         };
-        private static int[] PROTO_ENUMS = new int[]{
+        private static final int[] PROTO_ENUMS = new int[]{
                 ActivityManagerProto.UID_OBSERVER_FLAG_IDLE,
                 ActivityManagerProto.UID_OBSERVER_FLAG_ACTIVE,
                 ActivityManagerProto.UID_OBSERVER_FLAG_GONE,
                 ActivityManagerProto.UID_OBSERVER_FLAG_PROCSTATE,
         };
 
-        UidObserverRegistration(int _uid, String _pkg, int _which, int _cutpoint) {
-            uid = _uid;
-            pkg = _pkg;
-            which = _which;
-            cutpoint = _cutpoint;
+        UidObserverRegistration(int uid, String pkg, int which, int cutpoint) {
+            this.mUid = uid;
+            this.mPkg = pkg;
+            this.mWhich = which;
+            this.mCutpoint = cutpoint;
             if (cutpoint >= ActivityManager.MIN_PROCESS_STATE) {
-                lastProcStates = new SparseIntArray();
+                mLastProcStates = new SparseIntArray();
             } else {
-                lastProcStates = null;
+                mLastProcStates = null;
             }
         }
 
         void dumpDebug(ProtoOutputStream proto, long fieldId) {
             final long token = proto.start(fieldId);
-            proto.write(UidObserverRegistrationProto.UID, uid);
-            proto.write(UidObserverRegistrationProto.PACKAGE, pkg);
+            proto.write(UidObserverRegistrationProto.UID, mUid);
+            proto.write(UidObserverRegistrationProto.PACKAGE, mPkg);
             ProtoUtils.writeBitWiseFlagsToProtoEnum(proto, UidObserverRegistrationProto.FLAGS,
-                    which, ORIG_ENUMS, PROTO_ENUMS);
-            proto.write(UidObserverRegistrationProto.CUT_POINT, cutpoint);
-            if (lastProcStates != null) {
-                final int NI = lastProcStates.size();
-                for (int i=0; i<NI; i++) {
+                    mWhich, ORIG_ENUMS, PROTO_ENUMS);
+            proto.write(UidObserverRegistrationProto.CUT_POINT, mCutpoint);
+            if (mLastProcStates != null) {
+                final int size = mLastProcStates.size();
+                for (int i = 0; i < size; i++) {
                     final long pToken = proto.start(UidObserverRegistrationProto.LAST_PROC_STATES);
                     proto.write(UidObserverRegistrationProto.ProcState.UID,
-                            lastProcStates.keyAt(i));
+                            mLastProcStates.keyAt(i));
                     proto.write(UidObserverRegistrationProto.ProcState.STATE,
-                            lastProcStates.valueAt(i));
+                            mLastProcStates.valueAt(i));
                     proto.end(pToken);
                 }
             }
diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
index 295143e..29ee8eb 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
@@ -4367,7 +4367,7 @@
                 // Flip state because app was explicitly added or removed to denylist.
                 setMeteredNetworkDenylist(uid, (isDenylisted || isRestrictedByAdmin));
                 if (hasRule(oldRule, RULE_REJECT_METERED) && isAllowlisted) {
-                    // Since dneylist prevails over allowlist, we need to handle the special case
+                    // Since denylist prevails over allowlist, we need to handle the special case
                     // where app is allowlisted and denylisted at the same time (although such
                     // scenario should be blocked by the UI), then denylist is removed.
                     setMeteredNetworkAllowlist(uid, isAllowlisted);
diff --git a/services/core/jni/com_android_server_VibratorService.cpp b/services/core/jni/com_android_server_VibratorService.cpp
index 529fb88..b3f3a5e 100644
--- a/services/core/jni/com_android_server_VibratorService.cpp
+++ b/services/core/jni/com_android_server_VibratorService.cpp
@@ -181,6 +181,24 @@
     return effects;
 }
 
+static jintArray vibratorGetSupportedPrimitives(JNIEnv* env, jclass /* clazz */,
+                                                jlong controllerPtr) {
+    vibrator::HalController* controller = reinterpret_cast<vibrator::HalController*>(controllerPtr);
+    if (controller == nullptr) {
+        ALOGE("vibratorGetSupportedPrimitives failed because controller was not initialized");
+        return nullptr;
+    }
+    auto result = controller->getSupportedPrimitives();
+    if (!result.isOk()) {
+        return nullptr;
+    }
+    std::vector<aidl::CompositePrimitive> supportedPrimitives = result.value();
+    jintArray primitives = env->NewIntArray(supportedPrimitives.size());
+    env->SetIntArrayRegion(primitives, 0, supportedPrimitives.size(),
+                           reinterpret_cast<jint*>(supportedPrimitives.data()));
+    return primitives;
+}
+
 static jlong vibratorPerformEffect(JNIEnv* env, jclass /* clazz */, jlong controllerPtr,
                                    jlong effect, jlong strength, jobject vibration) {
     vibrator::HalController* controller = reinterpret_cast<vibrator::HalController*>(controllerPtr);
@@ -259,6 +277,7 @@
          "VibratorService$Vibration;)V",
          (void*)vibratorPerformComposedEffect},
         {"vibratorGetSupportedEffects", "(J)[I", (void*)vibratorGetSupportedEffects},
+        {"vibratorGetSupportedPrimitives", "(J)[I", (void*)vibratorGetSupportedPrimitives},
         {"vibratorSetExternalControl", "(JZ)V", (void*)vibratorSetExternalControl},
         {"vibratorGetCapabilities", "(J)J", (void*)vibratorGetCapabilities},
         {"vibratorAlwaysOnEnable", "(JJJJ)V", (void*)vibratorAlwaysOnEnable},
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 2ab629b..6154bef 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -1579,21 +1579,22 @@
 
     /**
      * Creates a new {@link CallerIdentity} object to represent the caller's identity.
+     * The component name should be an active admin for the calling user.
      */
-    private CallerIdentity getCallerIdentity(@NonNull ComponentName componentName) {
+    private CallerIdentity getCallerIdentity(@NonNull ComponentName adminComponent) {
         final int callerUid = mInjector.binderGetCallingUid();
         final DevicePolicyData policy = getUserData(UserHandle.getUserId(callerUid));
-        ActiveAdmin admin = policy.mAdminMap.get(componentName);
+        ActiveAdmin admin = policy.mAdminMap.get(adminComponent);
 
         if (admin == null) {
-            throw new SecurityException(String.format("No active admin for %s", componentName));
+            throw new SecurityException(String.format("No active admin for %s", adminComponent));
         }
         if (admin.getUid() != callerUid) {
             throw new SecurityException(
-                    String.format("Admin %s is not owned by uid %d", componentName, callerUid));
+                    String.format("Admin %s is not owned by uid %d", adminComponent, callerUid));
         }
 
-        return new CallerIdentity(callerUid, componentName.getPackageName(), componentName);
+        return new CallerIdentity(callerUid, adminComponent.getPackageName(), adminComponent);
     }
 
     /**
@@ -4589,12 +4590,6 @@
         }
     }
 
-    private void enforceDeviceOwner(ComponentName who) {
-        synchronized (getLockObject()) {
-            getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
-        }
-    }
-
     private void enforceProfileOrDeviceOwner(ComponentName who) {
         synchronized (getLockObject()) {
             getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
@@ -5194,20 +5189,23 @@
         Objects.requireNonNull(who, "ComponentName is null");
         Preconditions.checkStringNotEmpty(delegatePackage, "Delegate package is null or empty");
         Preconditions.checkCollectionElementsNotNull(scopeList, "Scopes");
+        final CallerIdentity identity = getCallerIdentity(who);
+
         // Remove possible duplicates.
         final ArrayList<String> scopes = new ArrayList(new ArraySet(scopeList));
         // Ensure given scopes are valid.
         if (scopes.retainAll(Arrays.asList(DELEGATIONS))) {
             throw new IllegalArgumentException("Unexpected delegation scopes");
         }
-        final boolean hasDoDelegation = !Collections.disjoint(scopes, DEVICE_OWNER_DELEGATIONS);
         // Retrieve the user ID of the calling process.
-        final int userId = mInjector.userHandleGetCallingUserId();
+        final int userId = identity.getUserId();
+        final boolean hasDoDelegation = !Collections.disjoint(scopes, DEVICE_OWNER_DELEGATIONS);
         synchronized (getLockObject()) {
             // Ensure calling process is device/profile owner.
             if (hasDoDelegation) {
-                getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
+                Preconditions.checkCallAuthorization(isDeviceOwner(identity));
             } else {
+                // TODO move whole condition out of synchronized block
                 getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
             }
             // Ensure the delegate is installed (skip this for DELEGATION_CERT_INSTALL in pre-N).
@@ -6199,7 +6197,9 @@
 
     @Override
     public void setRecommendedGlobalProxy(ComponentName who, ProxyInfo proxyInfo) {
-        enforceDeviceOwner(who);
+        Objects.requireNonNull(who, "ComponentName is null");
+        final CallerIdentity identity = getCallerIdentity(who);
+        Preconditions.checkCallAuthorization(isDeviceOwner(identity));
         mInjector.binderWithCleanCallingIdentity(
                 () -> mInjector.getConnectivityManager().setGlobalProxy(proxyInfo));
     }
@@ -6620,6 +6620,9 @@
             return;
         }
         Objects.requireNonNull(who, "ComponentName is null");
+        final CallerIdentity identity = getCallerIdentity(who);
+        Preconditions.checkCallAuthorization(isDeviceOwner(identity));
+
         // Allow setting this policy to true only if there is a split system user.
         if (forceEphemeralUsers && !mInjector.userManagerIsSplitSystemUser()) {
             throw new UnsupportedOperationException(
@@ -6627,11 +6630,10 @@
         }
         boolean removeAllUsers = false;
         synchronized (getLockObject()) {
-            final ActiveAdmin deviceOwner =
-                    getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
+            final ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
             if (deviceOwner.forceEphemeralUsers != forceEphemeralUsers) {
                 deviceOwner.forceEphemeralUsers = forceEphemeralUsers;
-                saveSettingsLocked(mInjector.userHandleGetCallingUserId());
+                saveSettingsLocked(identity.getUserId());
                 mUserManagerInternal.setForceEphemeralUsers(forceEphemeralUsers);
                 removeAllUsers = forceEphemeralUsers;
             }
@@ -6647,21 +6649,15 @@
             return false;
         }
         Objects.requireNonNull(who, "ComponentName is null");
+        final CallerIdentity identity = getCallerIdentity(who);
+        Preconditions.checkCallAuthorization(isDeviceOwner(identity));
+
         synchronized (getLockObject()) {
-            final ActiveAdmin deviceOwner =
-                    getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
+            final ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
             return deviceOwner.forceEphemeralUsers;
         }
     }
 
-    private void ensureDeviceOwnerAndAllUsersAffiliated(ComponentName who)
-            throws SecurityException {
-        synchronized (getLockObject()) {
-            getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
-        }
-        ensureAllUsersAffiliated();
-    }
-
     private void ensureAllUsersAffiliated() throws SecurityException {
         synchronized (getLockObject()) {
             if (!areAllUsersAffiliatedWithDeviceLocked()) {
@@ -6676,11 +6672,12 @@
             return false;
         }
         Objects.requireNonNull(who, "ComponentName is null");
-
         // TODO: If an unaffiliated user is removed, the admin will be able to request a bugreport
         // which could still contain data related to that user. Should we disallow that, e.g. until
         // next boot? Might not be needed given that this still requires user consent.
-        ensureDeviceOwnerAndAllUsersAffiliated(who);
+        final CallerIdentity identity = getCallerIdentity(who);
+        Preconditions.checkCallAuthorization(isDeviceOwner(identity));
+        ensureAllUsersAffiliated();
 
         if (mRemoteBugreportServiceIsActive.get()
                 || (getDeviceOwnerRemoteBugreportUri() != null)) {
@@ -8489,6 +8486,7 @@
     @Override
     public void setDefaultSmsApplication(ComponentName admin, String packageName, boolean parent) {
         Objects.requireNonNull(admin, "ComponentName is null");
+        final CallerIdentity identity = getCallerIdentity(admin);
 
         if (parent) {
             ActiveAdmin ap = getActiveAdminForCallerLocked(admin,
@@ -8497,7 +8495,7 @@
             mInjector.binderWithCleanCallingIdentity(() -> enforcePackageIsSystemPackage(
                     packageName, getProfileParentId(mInjector.userHandleGetCallingUserId())));
         } else {
-            enforceDeviceOwner(admin);
+            Preconditions.checkCallAuthorization(isDeviceOwner(identity));
         }
 
         mInjector.binderWithCleanCallingIdentity(() ->
@@ -9259,14 +9257,14 @@
     public boolean removeUser(ComponentName who, UserHandle userHandle) {
         Objects.requireNonNull(who, "ComponentName is null");
         Objects.requireNonNull(userHandle, "UserHandle is null");
-        enforceDeviceOwner(who);
+        final CallerIdentity identity = getCallerIdentity(who);
+        Preconditions.checkCallAuthorization(isDeviceOwner(identity));
 
-        final int callingUserId = mInjector.userHandleGetCallingUserId();
         return mInjector.binderWithCleanCallingIdentity(() -> {
             String restriction = isManagedProfile(userHandle.getIdentifier())
                     ? UserManager.DISALLOW_REMOVE_MANAGED_PROFILE
                     : UserManager.DISALLOW_REMOVE_USER;
-            if (isAdminAffectedByRestriction(who, restriction, callingUserId)) {
+            if (isAdminAffectedByRestriction(who, restriction, identity.getUserId())) {
                 Log.w(LOG_TAG, "The device owner cannot remove a user because "
                         + restriction + " is enabled, and was not set by the device owner");
                 return false;
@@ -9292,10 +9290,10 @@
     @Override
     public boolean switchUser(ComponentName who, UserHandle userHandle) {
         Objects.requireNonNull(who, "ComponentName is null");
+        final CallerIdentity identity = getCallerIdentity(who);
+        Preconditions.checkCallAuthorization(isDeviceOwner(identity));
 
         synchronized (getLockObject()) {
-            getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
-
             long id = mInjector.binderClearCallingIdentity();
             try {
                 int userId = UserHandle.USER_SYSTEM;
@@ -9316,7 +9314,8 @@
     public int startUserInBackground(ComponentName who, UserHandle userHandle) {
         Objects.requireNonNull(who, "ComponentName is null");
         Objects.requireNonNull(userHandle, "UserHandle is null");
-        enforceDeviceOwner(who);
+        final CallerIdentity identity = getCallerIdentity(who);
+        Preconditions.checkCallAuthorization(isDeviceOwner(identity));
 
         final int userId = userHandle.getIdentifier();
         if (isManagedProfile(userId)) {
@@ -9348,7 +9347,8 @@
     public int stopUser(ComponentName who, UserHandle userHandle) {
         Objects.requireNonNull(who, "ComponentName is null");
         Objects.requireNonNull(userHandle, "UserHandle is null");
-        enforceDeviceOwner(who);
+        final CallerIdentity identity = getCallerIdentity(who);
+        Preconditions.checkCallAuthorization(isDeviceOwner(identity));
 
         final int userId = userHandle.getIdentifier();
         if (isManagedProfile(userId)) {
@@ -9416,7 +9416,8 @@
     @Override
     public List<UserHandle> getSecondaryUsers(ComponentName who) {
         Objects.requireNonNull(who, "ComponentName is null");
-        enforceDeviceOwner(who);
+        final CallerIdentity identity = getCallerIdentity(who);
+        Preconditions.checkCallAuthorization(isDeviceOwner(identity));
 
         return mInjector.binderWithCleanCallingIdentity(() -> {
             final List<UserInfo> userInfos = mInjector.getUserManager().getUsers(true
@@ -10378,6 +10379,8 @@
     @Override
     public void setGlobalSetting(ComponentName who, String setting, String value) {
         Objects.requireNonNull(who, "ComponentName is null");
+        final CallerIdentity identity = getCallerIdentity(who);
+        Preconditions.checkCallAuthorization(isDeviceOwner(identity));
 
         DevicePolicyEventLogger
                 .createEvent(DevicePolicyEnums.SET_GLOBAL_SETTING)
@@ -10386,8 +10389,6 @@
                 .write();
 
         synchronized (getLockObject()) {
-            getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
-
             // Some settings are no supported any more. However we do not want to throw a
             // SecurityException to avoid breaking apps.
             if (GLOBAL_SETTINGS_DEPRECATED.contains(setting)) {
@@ -10468,7 +10469,7 @@
 
     @Override
     public void setLocationEnabled(ComponentName who, boolean locationEnabled) {
-        CallerIdentity identity = getCallerIdentity(who);
+        final CallerIdentity identity = getCallerIdentity(who);
         Preconditions.checkCallAuthorization(isDeviceOwner(identity));
 
         mInjector.binderWithCleanCallingIdentity(() -> {
@@ -12008,16 +12009,18 @@
 
     @Override
     public boolean isSystemOnlyUser(ComponentName admin) {
-        enforceDeviceOwner(admin);
-        final int callingUserId = mInjector.userHandleGetCallingUserId();
-        return UserManager.isSplitSystemUser() && callingUserId == UserHandle.USER_SYSTEM;
+        Objects.requireNonNull(admin, "ComponentName is null");
+        final CallerIdentity identity = getCallerIdentity(admin);
+        Preconditions.checkCallAuthorization(isDeviceOwner(identity));
+        return UserManager.isSplitSystemUser() && identity.getUserId() == UserHandle.USER_SYSTEM;
     }
 
     @Override
     public void reboot(ComponentName admin) {
-        Objects.requireNonNull(admin);
-        // Make sure caller has DO.
-        enforceDeviceOwner(admin);
+        Objects.requireNonNull(admin, "ComponentName is null");
+        final CallerIdentity identity = getCallerIdentity(admin);
+        Preconditions.checkCallAuthorization(isDeviceOwner(identity));
+
         mInjector.binderWithCleanCallingIdentity(() -> {
             // Make sure there are no ongoing calls on the device.
             if (mTelephonyManager.getCallState() != TelephonyManager.CALL_STATE_IDLE) {
@@ -13523,18 +13526,18 @@
         if (!mHasFeature) {
             return;
         }
-        Objects.requireNonNull(admin);
+        Objects.requireNonNull(admin, "ComponentName is null");
+        final CallerIdentity identity = getCallerIdentity(admin);
+        Preconditions.checkCallAuthorization(isDeviceOwner(identity));
 
         synchronized (getLockObject()) {
-            ActiveAdmin deviceOwner =
-                    getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
-
+            ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
             if (deviceOwner.isLogoutEnabled == enabled) {
                 // already in the requested state
                 return;
             }
             deviceOwner.isLogoutEnabled = enabled;
-            saveSettingsLocked(mInjector.userHandleGetCallingUserId());
+            saveSettingsLocked(identity.getUserId());
         }
     }
 
@@ -13700,20 +13703,20 @@
         if (!mHasFeature) {
             return;
         }
-        Objects.requireNonNull(admin);
+        Objects.requireNonNull(admin, "ComponentName is null");
+        final CallerIdentity identity = getCallerIdentity(admin);
+        Preconditions.checkCallAuthorization(isDeviceOwner(identity));
 
         final String startUserSessionMessageString =
                 startUserSessionMessage != null ? startUserSessionMessage.toString() : null;
 
         synchronized (getLockObject()) {
-            final ActiveAdmin deviceOwner =
-                    getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
-
+            final ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
             if (TextUtils.equals(deviceOwner.startUserSessionMessage, startUserSessionMessage)) {
                 return;
             }
             deviceOwner.startUserSessionMessage = startUserSessionMessageString;
-            saveSettingsLocked(mInjector.userHandleGetCallingUserId());
+            saveSettingsLocked(identity.getUserId());
         }
 
         mInjector.getActivityManagerInternal()
@@ -13725,20 +13728,20 @@
         if (!mHasFeature) {
             return;
         }
-        Objects.requireNonNull(admin);
+        Objects.requireNonNull(admin, "ComponentName is null");
+        final CallerIdentity identity = getCallerIdentity(admin);
+        Preconditions.checkCallAuthorization(isDeviceOwner(identity));
 
         final String endUserSessionMessageString =
                 endUserSessionMessage != null ? endUserSessionMessage.toString() : null;
 
         synchronized (getLockObject()) {
-            final ActiveAdmin deviceOwner =
-                    getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
-
+            final ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
             if (TextUtils.equals(deviceOwner.endUserSessionMessage, endUserSessionMessage)) {
                 return;
             }
             deviceOwner.endUserSessionMessage = endUserSessionMessageString;
-            saveSettingsLocked(mInjector.userHandleGetCallingUserId());
+            saveSettingsLocked(identity.getUserId());
         }
 
         mInjector.getActivityManagerInternal()
@@ -13750,11 +13753,12 @@
         if (!mHasFeature) {
             return null;
         }
-        Objects.requireNonNull(admin);
+        Objects.requireNonNull(admin, "ComponentName is null");
+        final CallerIdentity identity = getCallerIdentity(admin);
+        Preconditions.checkCallAuthorization(isDeviceOwner(identity));
 
         synchronized (getLockObject()) {
-            final ActiveAdmin deviceOwner =
-                    getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
+            final ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
             return deviceOwner.startUserSessionMessage;
         }
     }
@@ -13764,11 +13768,12 @@
         if (!mHasFeature) {
             return null;
         }
-        Objects.requireNonNull(admin);
+        Objects.requireNonNull(admin, "ComponentName is null");
+        final CallerIdentity identity = getCallerIdentity(admin);
+        Preconditions.checkCallAuthorization(isDeviceOwner(identity));
 
         synchronized (getLockObject()) {
-            final ActiveAdmin deviceOwner =
-                    getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
+            final ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
             return deviceOwner.endUserSessionMessage;
         }
     }
@@ -13807,9 +13812,10 @@
         if (!mHasFeature || !mHasTelephonyFeature) {
             return -1;
         }
-        Objects.requireNonNull(who, "ComponentName is null in addOverrideApn");
+        Objects.requireNonNull(who, "ComponentName is null");
         Objects.requireNonNull(apnSetting, "ApnSetting is null in addOverrideApn");
-        enforceDeviceOwner(who);
+        final CallerIdentity identity = getCallerIdentity(who);
+        Preconditions.checkCallAuthorization(isDeviceOwner(identity));
 
         TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
         if (tm != null) {
@@ -13827,9 +13833,10 @@
         if (!mHasFeature || !mHasTelephonyFeature) {
             return false;
         }
-        Objects.requireNonNull(who, "ComponentName is null in updateOverrideApn");
+        Objects.requireNonNull(who, "ComponentName is null");
         Objects.requireNonNull(apnSetting, "ApnSetting is null in updateOverrideApn");
-        enforceDeviceOwner(who);
+        final CallerIdentity identity = getCallerIdentity(who);
+        Preconditions.checkCallAuthorization(isDeviceOwner(identity));
 
         if (apnId < 0) {
             return false;
@@ -13849,9 +13856,9 @@
         if (!mHasFeature || !mHasTelephonyFeature) {
             return false;
         }
-        Objects.requireNonNull(who, "ComponentName is null in removeOverrideApn");
-        enforceDeviceOwner(who);
-
+        Objects.requireNonNull(who, "ComponentName is null");
+        final CallerIdentity identity = getCallerIdentity(who);
+        Preconditions.checkCallAuthorization(isDeviceOwner(identity));
         return removeOverrideApnUnchecked(apnId);
     }
 
@@ -13870,9 +13877,9 @@
         if (!mHasFeature || !mHasTelephonyFeature) {
             return Collections.emptyList();
         }
-        Objects.requireNonNull(who, "ComponentName is null in getOverrideApns");
-        enforceDeviceOwner(who);
-
+        Objects.requireNonNull(who, "ComponentName is null");
+        final CallerIdentity identity = getCallerIdentity(who);
+        Preconditions.checkCallAuthorization(isDeviceOwner(identity));
         return getOverrideApnsUnchecked();
     }
 
@@ -13891,9 +13898,9 @@
         if (!mHasFeature || !mHasTelephonyFeature) {
             return;
         }
-        Objects.requireNonNull(who, "ComponentName is null in setOverrideApnEnabled");
-        enforceDeviceOwner(who);
-
+        Objects.requireNonNull(who, "ComponentName is null");
+        final CallerIdentity identity = getCallerIdentity(who);
+        Preconditions.checkCallAuthorization(isDeviceOwner(identity));
         setOverrideApnsEnabledUnchecked(enabled);
     }
 
@@ -13909,8 +13916,9 @@
         if (!mHasFeature || !mHasTelephonyFeature) {
             return false;
         }
-        Objects.requireNonNull(who, "ComponentName is null in isOverrideApnEnabled");
-        enforceDeviceOwner(who);
+        Objects.requireNonNull(who, "ComponentName is null");
+        final CallerIdentity identity = getCallerIdentity(who);
+        Preconditions.checkCallAuthorization(isDeviceOwner(identity));
 
         Cursor enforceCursor = mInjector.binderWithCleanCallingIdentity(
                 () -> mContext.getContentResolver().query(
@@ -13992,11 +14000,9 @@
         if (!mHasFeature) {
             return PRIVATE_DNS_SET_ERROR_FAILURE_SETTING;
         }
-
         Objects.requireNonNull(who, "ComponentName is null");
-        enforceDeviceOwner(who);
-
-        final int returnCode;
+        final CallerIdentity identity = getCallerIdentity(who);
+        Preconditions.checkCallAuthorization(isDeviceOwner(identity));
 
         switch (mode) {
             case PRIVATE_DNS_MODE_OPPORTUNISTIC:
@@ -14030,9 +14036,10 @@
         if (!mHasFeature) {
             return PRIVATE_DNS_MODE_UNKNOWN;
         }
-
         Objects.requireNonNull(who, "ComponentName is null");
-        enforceDeviceOwner(who);
+        final CallerIdentity identity = getCallerIdentity(who);
+        Preconditions.checkCallAuthorization(isDeviceOwner(identity));
+
         String currentMode = mInjector.settingsGlobalGetString(PRIVATE_DNS_MODE);
         if (currentMode == null) {
             currentMode = ConnectivityManager.PRIVATE_DNS_DEFAULT_MODE_FALLBACK;
@@ -14054,10 +14061,9 @@
         if (!mHasFeature) {
             return null;
         }
-
         Objects.requireNonNull(who, "ComponentName is null");
-        enforceDeviceOwner(who);
-
+        final CallerIdentity identity = getCallerIdentity(who);
+        Preconditions.checkCallAuthorization(isDeviceOwner(identity));
         return mInjector.settingsGlobalGetString(PRIVATE_DNS_SPECIFIER);
     }
 
@@ -14402,13 +14408,13 @@
 
     @Override
     public void setUserControlDisabledPackages(ComponentName who, List<String> packages) {
-        Preconditions.checkNotNull(who, "ComponentName is null");
+        Objects.requireNonNull(who, "ComponentName is null");
         Preconditions.checkNotNull(packages, "packages is null");
+        final CallerIdentity identity = getCallerIdentity(who);
+        Preconditions.checkCallAuthorization(isDeviceOwner(identity));
 
-        enforceDeviceOwner(who);
         synchronized (getLockObject()) {
-            final int userHandle = mInjector.userHandleGetCallingUserId();
-            setUserControlDisabledPackagesLocked(userHandle, packages);
+            setUserControlDisabledPackagesLocked(identity.getUserId(), packages);
             DevicePolicyEventLogger
                     .createEvent(DevicePolicyEnums.SET_USER_CONTROL_DISABLED_PACKAGES)
                     .setAdmin(who)
@@ -14428,12 +14434,12 @@
 
     @Override
     public List<String> getUserControlDisabledPackages(ComponentName who) {
-        Preconditions.checkNotNull(who, "ComponentName is null");
+        final CallerIdentity identity = getCallerIdentity(who);
+        Preconditions.checkCallAuthorization(isDeviceOwner(identity));
 
-        enforceDeviceOwner(who);
-        final int userHandle = mInjector.binderGetCallingUserHandle().getIdentifier();
         synchronized (getLockObject()) {
-            final List<String> packages = getUserData(userHandle).mUserControlDisabledPackages;
+            final List<String> packages =
+                    getUserData(identity.getUserId()).mUserControlDisabledPackages;
             return packages == null ? Collections.EMPTY_LIST : packages;
         }
     }
diff --git a/services/tests/PackageManagerServiceTests/host/Android.bp b/services/tests/PackageManagerServiceTests/host/Android.bp
index e4e7e22..4f636ef 100644
--- a/services/tests/PackageManagerServiceTests/host/Android.bp
+++ b/services/tests/PackageManagerServiceTests/host/Android.bp
@@ -22,6 +22,7 @@
     ],
     static_libs: [
         "frameworks-base-hostutils",
+        "PackageManagerServiceHostTestsIntentVerifyUtils",
     ],
     test_suites: ["general-tests"],
     java_resources: [
@@ -33,7 +34,15 @@
         ":PackageManagerTestAppVersion4",
         ":PackageManagerTestAppOriginalOverride",
         ":PackageManagerServiceDeviceSideTests",
-    ],
+        ":PackageManagerTestIntentVerifier",
+        ":PackageManagerTestIntentVerifierTarget1",
+        ":PackageManagerTestIntentVerifierTarget2",
+        ":PackageManagerTestIntentVerifierTarget3",
+        ":PackageManagerTestIntentVerifierTarget4Base",
+        ":PackageManagerTestIntentVerifierTarget4NoAutoVerify",
+        ":PackageManagerTestIntentVerifierTarget4Wildcard",
+        ":PackageManagerTestIntentVerifierTarget4WildcardNoAutoVerify",
+    ]
 }
 
 genrule {
diff --git a/services/tests/PackageManagerServiceTests/host/libs/IntentVerifyUtils/Android.bp b/services/tests/PackageManagerServiceTests/host/libs/IntentVerifyUtils/Android.bp
new file mode 100644
index 0000000..b7a0624
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/host/libs/IntentVerifyUtils/Android.bp
@@ -0,0 +1,19 @@
+// 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.
+
+java_library {
+    name: "PackageManagerServiceHostTestsIntentVerifyUtils",
+    srcs: ["src/**/*.kt"],
+    host_supported: true,
+}
diff --git a/tests/AutoVerify/app1/src/com/android/test/autoverify/MainActivity.java b/services/tests/PackageManagerServiceTests/host/libs/IntentVerifyUtils/src/com/android/server/pm/test/intent/verify/IntentVerifyTestParams.kt
similarity index 78%
rename from tests/AutoVerify/app1/src/com/android/test/autoverify/MainActivity.java
rename to services/tests/PackageManagerServiceTests/host/libs/IntentVerifyUtils/src/com/android/server/pm/test/intent/verify/IntentVerifyTestParams.kt
index 09ef472..48119e0 100644
--- a/tests/AutoVerify/app1/src/com/android/test/autoverify/MainActivity.java
+++ b/services/tests/PackageManagerServiceTests/host/libs/IntentVerifyUtils/src/com/android/server/pm/test/intent/verify/IntentVerifyTestParams.kt
@@ -13,3 +13,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+package com.android.server.pm.test.intent.verify
+
+interface IntentVerifyTestParams {
+
+    val methodName: String
+
+    fun toArgsMap(): Map<String, String>
+}
diff --git a/services/tests/PackageManagerServiceTests/host/libs/IntentVerifyUtils/src/com/android/server/pm/test/intent/verify/SetActivityAsAlwaysParams.kt b/services/tests/PackageManagerServiceTests/host/libs/IntentVerifyUtils/src/com/android/server/pm/test/intent/verify/SetActivityAsAlwaysParams.kt
new file mode 100644
index 0000000..26c3903
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/host/libs/IntentVerifyUtils/src/com/android/server/pm/test/intent/verify/SetActivityAsAlwaysParams.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.server.pm.test.intent.verify
+
+data class SetActivityAsAlwaysParams(
+    val uri: String,
+    val packageName: String,
+    val activityName: String,
+    override val methodName: String = "setActivityAsAlways"
+) : IntentVerifyTestParams {
+
+    companion object {
+        private const val KEY_URI = "uri"
+        private const val KEY_PACKAGE_NAME = "packageName"
+        private const val KEY_ACTIVITY_NAME = "activityName"
+
+        fun fromArgs(args: Map<String, String>) = SetActivityAsAlwaysParams(
+                args.getValue(KEY_URI),
+                args.getValue(KEY_PACKAGE_NAME),
+                args.getValue(KEY_ACTIVITY_NAME)
+        )
+    }
+
+    override fun toArgsMap() = mapOf(
+            KEY_URI to uri,
+            KEY_PACKAGE_NAME to packageName,
+            KEY_ACTIVITY_NAME to activityName
+    )
+}
diff --git a/services/tests/PackageManagerServiceTests/host/libs/IntentVerifyUtils/src/com/android/server/pm/test/intent/verify/StartActivityParams.kt b/services/tests/PackageManagerServiceTests/host/libs/IntentVerifyUtils/src/com/android/server/pm/test/intent/verify/StartActivityParams.kt
new file mode 100644
index 0000000..7eddcfb
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/host/libs/IntentVerifyUtils/src/com/android/server/pm/test/intent/verify/StartActivityParams.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.server.pm.test.intent.verify
+
+data class StartActivityParams(
+    val uri: String,
+    val expected: List<String>,
+    val withBrowsers: Boolean = false,
+    override val methodName: String = "verifyActivityStart"
+) : IntentVerifyTestParams {
+    companion object {
+        private const val KEY_URI = "uri"
+        private const val KEY_EXPECTED = "expected"
+        private const val KEY_BROWSER = "browser"
+
+        fun fromArgs(args: Map<String, String>) = StartActivityParams(
+                args.getValue(KEY_URI),
+                args.getValue(KEY_EXPECTED).split(","),
+                args.getValue(KEY_BROWSER).toBoolean()
+        )
+    }
+
+    constructor(
+        uri: String,
+        expected: String,
+        withBrowsers: Boolean = false
+    ) : this(uri, listOf(expected), withBrowsers)
+
+    override fun toArgsMap() = mapOf(
+            KEY_URI to uri,
+            KEY_EXPECTED to expected.joinToString(separator = ","),
+            KEY_BROWSER to withBrowsers.toString()
+    )
+}
diff --git a/services/tests/PackageManagerServiceTests/host/libs/IntentVerifyUtils/src/com/android/server/pm/test/intent/verify/VerifyRequest.kt b/services/tests/PackageManagerServiceTests/host/libs/IntentVerifyUtils/src/com/android/server/pm/test/intent/verify/VerifyRequest.kt
new file mode 100644
index 0000000..f93b1e0
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/host/libs/IntentVerifyUtils/src/com/android/server/pm/test/intent/verify/VerifyRequest.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.server.pm.test.intent.verify
+
+data class VerifyRequest(
+    val id: Int = -1,
+    val scheme: String,
+    val hosts: List<String>,
+    val packageName: String
+) {
+
+    companion object {
+        fun deserialize(value: String?): VerifyRequest {
+            val lines = value?.trim()?.lines()
+                    ?: return VerifyRequest(scheme = "", hosts = emptyList(), packageName = "")
+            return VerifyRequest(
+                    lines[0].removePrefix("id=").toInt(),
+                    lines[1].removePrefix("scheme="),
+                    lines[2].removePrefix("hosts=").split(","),
+                    lines[3].removePrefix("packageName=")
+            )
+        }
+    }
+
+    constructor(id: Int = -1, scheme: String, host: String, packageName: String) :
+            this(id, scheme, listOf(host), packageName)
+
+    fun serializeToString() = """
+        id=$id
+        scheme=$scheme
+        hosts=${hosts.joinToString(separator = ",")}
+        packageName=$packageName
+    """.trimIndent() + "\n"
+}
diff --git a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/FactoryPackageTest.kt b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/FactoryPackageTest.kt
index 3847658..e17358d 100644
--- a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/FactoryPackageTest.kt
+++ b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/FactoryPackageTest.kt
@@ -31,7 +31,8 @@
     private val preparer: SystemPreparer = SystemPreparer(tempFolder,
             SystemPreparer.RebootStrategy.FULL, deviceRebootRule) { this.device }
 
-    @get:Rule
+    @Rule
+    @JvmField
     val rules = RuleChain.outerRule(tempFolder).around(preparer)!!
     private val filePath =
             HostUtils.makePathForApk("PackageManagerTestApp.apk", Partition.SYSTEM)
diff --git a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/HostUtils.kt b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/HostUtils.kt
index 8dfefaf..24c714c 100644
--- a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/HostUtils.kt
+++ b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/HostUtils.kt
@@ -18,6 +18,7 @@
 
 import com.android.internal.util.test.SystemPreparer
 import com.android.tradefed.device.ITestDevice
+import org.junit.rules.TemporaryFolder
 import java.io.File
 import java.io.FileOutputStream
 
@@ -34,6 +35,19 @@
     }
 }
 
+internal fun ITestDevice.installJavaResourceApk(
+    tempFolder: TemporaryFolder,
+    javaResource: String,
+    reinstall: Boolean = true,
+    extraArgs: Array<String> = emptyArray()
+): String? {
+    val file = HostUtils.copyResourceToHostFile(javaResource, tempFolder.newFile())
+    return installPackage(file, reinstall, *extraArgs)
+}
+
+internal fun ITestDevice.uninstallPackages(vararg pkgNames: String) =
+        pkgNames.forEach { uninstallPackage(it) }
+
 internal object HostUtils {
 
     fun getDataDir(device: ITestDevice, pkgName: String) =
diff --git a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/InvalidNewSystemAppTest.kt b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/InvalidNewSystemAppTest.kt
index b7d1359..37c999c 100644
--- a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/InvalidNewSystemAppTest.kt
+++ b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/InvalidNewSystemAppTest.kt
@@ -47,7 +47,8 @@
     private val preparer: SystemPreparer = SystemPreparer(tempFolder,
             SystemPreparer.RebootStrategy.FULL, deviceRebootRule) { this.device }
 
-    @get:Rule
+    @Rule
+    @JvmField
     val rules = RuleChain.outerRule(tempFolder).around(preparer)!!
     private val filePath = HostUtils.makePathForApk("PackageManagerTestApp.apk", Partition.PRODUCT)
 
diff --git a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/OriginalPackageMigrationTest.kt b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/OriginalPackageMigrationTest.kt
index 4ae3ca5..4becae6 100644
--- a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/OriginalPackageMigrationTest.kt
+++ b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/OriginalPackageMigrationTest.kt
@@ -47,7 +47,8 @@
     private val preparer: SystemPreparer = SystemPreparer(tempFolder,
             SystemPreparer.RebootStrategy.FULL, deviceRebootRule) { this.device }
 
-    @get:Rule
+    @Rule
+    @JvmField
     val rules = RuleChain.outerRule(tempFolder).around(preparer)!!
 
     @Before
diff --git a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/Partition.kt b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/Partition.kt
index 654c11c..6479f58 100644
--- a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/Partition.kt
+++ b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/Partition.kt
@@ -22,6 +22,7 @@
 // Unfortunately no easy way to access PMS SystemPartitions, so mock them here
 internal enum class Partition(val baseAppFolder: Path) {
     SYSTEM("/system/app"),
+    SYSTEM_PRIVILEGED("/system/priv-app"),
     VENDOR("/vendor/app"),
     PRODUCT("/product/app"),
     SYSTEM_EXT("/system_ext/app")
diff --git a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/SystemStubMultiUserDisableUninstallTest.kt b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/SystemStubMultiUserDisableUninstallTest.kt
index 207f10a..46120af 100644
--- a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/SystemStubMultiUserDisableUninstallTest.kt
+++ b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/SystemStubMultiUserDisableUninstallTest.kt
@@ -110,7 +110,8 @@
     private val preparer: SystemPreparer = SystemPreparer(tempFolder,
             SystemPreparer.RebootStrategy.FULL, deviceRebootRule) { this.device }
 
-    @get:Rule
+    @Rule
+    @JvmField
     val rules = RuleChain.outerRule(tempFolder).let {
         if (DEBUG_NO_REBOOT) {
             it!!
diff --git a/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/intent/verify/IntentFilterVerificationTest.kt b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/intent/verify/IntentFilterVerificationTest.kt
new file mode 100644
index 0000000..fffda8e
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/host/src/com/android/server/pm/test/intent/verify/IntentFilterVerificationTest.kt
@@ -0,0 +1,413 @@
+/*
+ * 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.server.pm.test.intent.verify
+
+import com.android.internal.util.test.SystemPreparer
+import com.android.server.pm.test.Partition
+import com.android.server.pm.test.deleteApkFolders
+import com.android.server.pm.test.installJavaResourceApk
+import com.android.server.pm.test.pushApk
+import com.android.server.pm.test.uninstallPackages
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.ClassRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.RuleChain
+import org.junit.rules.TemporaryFolder
+import org.junit.runner.RunWith
+import java.io.File
+import java.util.concurrent.TimeUnit
+
+@RunWith(DeviceJUnit4ClassRunner::class)
+class IntentFilterVerificationTest : BaseHostJUnit4Test() {
+
+    companion object {
+        private const val VERIFIER = "PackageManagerTestIntentVerifier.apk"
+        private const val VERIFIER_PKG_NAME = "com.android.server.pm.test.intent.verifier"
+        private const val TARGET_PKG_PREFIX = "$VERIFIER_PKG_NAME.target"
+        private const val TARGET_APK_PREFIX = "PackageManagerTestIntentVerifierTarget"
+        private const val TARGET_ONE = "${TARGET_APK_PREFIX}1.apk"
+        private const val TARGET_ONE_PKG_NAME = "$TARGET_PKG_PREFIX.one"
+        private const val TARGET_TWO = "${TARGET_APK_PREFIX}2.apk"
+        private const val TARGET_TWO_PKG_NAME = "$TARGET_PKG_PREFIX.two"
+        private const val TARGET_THREE = "${TARGET_APK_PREFIX}3.apk"
+        private const val TARGET_THREE_PKG_NAME = "$TARGET_PKG_PREFIX.three"
+        private const val TARGET_FOUR_BASE = "${TARGET_APK_PREFIX}4Base.apk"
+        private const val TARGET_FOUR_PKG_NAME = "$TARGET_PKG_PREFIX.four"
+        private const val TARGET_FOUR_NO_AUTO_VERIFY = "${TARGET_APK_PREFIX}4NoAutoVerify.apk"
+        private const val TARGET_FOUR_WILDCARD = "${TARGET_APK_PREFIX}4Wildcard.apk"
+        private const val TARGET_FOUR_WILDCARD_NO_AUTO_VERIFY =
+                "${TARGET_APK_PREFIX}4WildcardNoAutoVerify.apk"
+
+        @get:ClassRule
+        val deviceRebootRule = SystemPreparer.TestRuleDelegate(true)
+    }
+
+    private val tempFolder = TemporaryFolder()
+    private val preparer: SystemPreparer = SystemPreparer(tempFolder,
+            SystemPreparer.RebootStrategy.FULL, deviceRebootRule) { this.device }
+
+    @Rule
+    @JvmField
+    val rules = RuleChain.outerRule(tempFolder).around(preparer)!!
+
+    private val permissionsFile = File("/system/etc/permissions" +
+            "/privapp-PackageManagerIntentFilterVerificationTest-permissions.xml")
+
+    @Before
+    fun cleanupAndPushPermissionsFile() {
+        // In order for the test app to be the verification agent, it needs a permission file
+        // which can be pushed onto the system and removed afterwards.
+        val file = tempFolder.newFile().apply {
+            """
+                <permissions>
+                    <privapp-permissions package="$VERIFIER_PKG_NAME">
+                        <permission name="android.permission.INTENT_FILTER_VERIFICATION_AGENT"/>
+                    </privapp-permissions>
+                </permissions>
+            """
+                    .trimIndent()
+                    .let { writeText(it) }
+        }
+        device.uninstallPackages(TARGET_ONE_PKG_NAME, TARGET_TWO_PKG_NAME, TARGET_THREE_PKG_NAME,
+                TARGET_FOUR_PKG_NAME)
+        preparer.pushApk(VERIFIER, Partition.SYSTEM_PRIVILEGED)
+                .pushFile(file, permissionsFile.toString())
+                .reboot()
+        runTest("clearResponse")
+    }
+
+    @After
+    fun cleanupAndDeletePermissionsFile() {
+        device.uninstallPackages(TARGET_ONE_PKG_NAME, TARGET_TWO_PKG_NAME, TARGET_THREE_PKG_NAME,
+                TARGET_FOUR_PKG_NAME)
+        preparer.deleteApkFolders(Partition.SYSTEM_PRIVILEGED, VERIFIER)
+                .deleteFile(permissionsFile.toString())
+        device.reboot()
+    }
+
+    @Test
+    fun verifyOne() {
+        installPackage(TARGET_ONE)
+
+        assertReceivedRequests(true, VerifyRequest(
+                scheme = "https",
+                hosts = listOf(
+                        "https_only.pm.server.android.com",
+                        "other_activity.pm.server.android.com",
+                        "http_only.pm.server.android.com",
+                        "verify.pm.server.android.com",
+                        "https_plus_non_web_scheme.pm.server.android.com",
+                        "multiple.pm.server.android.com",
+                        // TODO(b/159952358): the following domain should not be
+                        //  verified, this is because the verifier tries to verify all web domains,
+                        //  even in intent filters not marked for auto verify
+                        "no_verify.pm.server.android.com"
+                ),
+                packageName = TARGET_ONE_PKG_NAME
+        ))
+
+        runTest(StartActivityParams(
+                uri = "https://https_only.pm.server.android.com",
+                expected = "$TARGET_ONE_PKG_NAME.TargetActivity"
+        ))
+    }
+
+    @Test
+    fun nonWebScheme() {
+        installPackage(TARGET_TWO)
+        assertReceivedRequests(null)
+    }
+
+    @Test
+    fun verifyHttpNonSecureOnly() {
+        installPackage(TARGET_THREE)
+        assertReceivedRequests(true, VerifyRequest(
+                scheme = "https",
+                hosts = listOf(
+                        "multiple.pm.server.android.com"
+                ),
+                packageName = TARGET_THREE_PKG_NAME
+        ))
+
+        runTest(StartActivityParams(
+                uri = "http://multiple.pm.server.android.com",
+                expected = "$TARGET_THREE_PKG_NAME.TargetActivity"
+        ))
+    }
+
+    @Test
+    fun multipleResults() {
+        installPackage(TARGET_ONE)
+        installPackage(TARGET_THREE)
+        assertReceivedRequests(true, VerifyRequest(
+                scheme = "https",
+                hosts = listOf(
+                        "https_only.pm.server.android.com",
+                        "other_activity.pm.server.android.com",
+                        "http_only.pm.server.android.com",
+                        "verify.pm.server.android.com",
+                        "https_plus_non_web_scheme.pm.server.android.com",
+                        "multiple.pm.server.android.com",
+                        // TODO(b/159952358): the following domain should not be
+                        //  verified, this is because the verifier tries to verify all web domains,
+                        //  even in intent filters not marked for auto verify
+                        "no_verify.pm.server.android.com"
+                ),
+                packageName = TARGET_ONE_PKG_NAME
+        ), VerifyRequest(
+                scheme = "https",
+                hosts = listOf(
+                        "multiple.pm.server.android.com"
+                ),
+                packageName = TARGET_THREE_PKG_NAME
+        ))
+
+        // Target3 declares http non-s, so it should be included in the set here
+        runTest(StartActivityParams(
+                uri = "http://multiple.pm.server.android.com",
+                expected = listOf(
+                        "$TARGET_ONE_PKG_NAME.TargetActivity2",
+                        "$TARGET_THREE_PKG_NAME.TargetActivity"
+                )
+        ))
+
+        // But it excludes https, so it shouldn't resolve here
+        runTest(StartActivityParams(
+                uri = "https://multiple.pm.server.android.com",
+                expected = "$TARGET_ONE_PKG_NAME.TargetActivity2"
+        ))
+
+        // Remove Target3 and return to single verified Target1 app for http non-s
+        device.uninstallPackage(TARGET_THREE_PKG_NAME)
+        runTest(StartActivityParams(
+                uri = "http://multiple.pm.server.android.com",
+                expected = "$TARGET_ONE_PKG_NAME.TargetActivity2"
+        ))
+    }
+
+    @Test
+    fun demoteAlways() {
+        installPackage(TARGET_FOUR_BASE)
+        assertReceivedRequests(false, VerifyRequest(
+                scheme = "https",
+                host = "failing.pm.server.android.com",
+                packageName = TARGET_FOUR_PKG_NAME
+        ))
+
+        runTest(StartActivityParams(
+                uri = "https://failing.pm.server.android.com",
+                expected = "$TARGET_FOUR_PKG_NAME.TargetActivity",
+                withBrowsers = true
+        ))
+        runTest(SetActivityAsAlwaysParams(
+                uri = "https://failing.pm.server.android.com",
+                packageName = TARGET_FOUR_PKG_NAME,
+                activityName = "$TARGET_FOUR_PKG_NAME.TargetActivity"
+        ))
+        runTest(StartActivityParams(
+                uri = "https://failing.pm.server.android.com",
+                expected = "$TARGET_FOUR_PKG_NAME.TargetActivity"
+        ))
+
+        // Re-installing with same host/verify set will maintain always setting
+        installPackage(TARGET_FOUR_BASE)
+        assertReceivedRequests(null)
+        runTest(StartActivityParams(
+                uri = "https://failing.pm.server.android.com",
+                expected = "$TARGET_FOUR_PKG_NAME.TargetActivity"
+        ))
+
+        // Installing with new wildcard host will downgrade out of always, re-including browsers
+        installPackage(TARGET_FOUR_WILDCARD)
+
+        // TODO(b/159952358): The first request without the wildcard should not be sent. This is
+        //  caused by the request being queued even if it should be dropped from the previous
+        //  install case since the host set didn't change.
+        assertReceivedRequests(false, VerifyRequest(
+                scheme = "https",
+                hosts = listOf("failing.pm.server.android.com"),
+                packageName = TARGET_FOUR_PKG_NAME
+        ), VerifyRequest(
+                scheme = "https",
+                hosts = listOf("failing.pm.server.android.com", "wildcard.tld"),
+                packageName = TARGET_FOUR_PKG_NAME
+        ))
+        runTest(StartActivityParams(
+                uri = "https://failing.pm.server.android.com",
+                expected = "$TARGET_FOUR_PKG_NAME.TargetActivity",
+                withBrowsers = true
+        ))
+    }
+
+    @Test
+    fun unverifiedReinstallResendRequest() {
+        installPackage(TARGET_FOUR_BASE)
+        assertReceivedRequests(false, VerifyRequest(
+                scheme = "https",
+                host = "failing.pm.server.android.com",
+                packageName = TARGET_FOUR_PKG_NAME
+        ))
+
+        installPackage(TARGET_FOUR_BASE)
+
+        assertReceivedRequests(false, VerifyRequest(
+                scheme = "https",
+                host = "failing.pm.server.android.com",
+                packageName = TARGET_FOUR_PKG_NAME
+        ))
+    }
+
+    @Test
+    fun unverifiedUpdateRemovingDomainNoRequestDemoteAlways() {
+        installPackage(TARGET_FOUR_WILDCARD)
+        assertReceivedRequests(false, VerifyRequest(
+                scheme = "https",
+                hosts = listOf("failing.pm.server.android.com", "wildcard.tld"),
+                packageName = TARGET_FOUR_PKG_NAME
+        ))
+
+        runTest(SetActivityAsAlwaysParams(
+                uri = "https://failing.pm.server.android.com",
+                packageName = TARGET_FOUR_PKG_NAME,
+                activityName = "$TARGET_FOUR_PKG_NAME.TargetActivity"
+        ))
+
+        // Re-installing with a smaller host/verify set will not request re-verification
+        installPackage(TARGET_FOUR_BASE)
+        assertReceivedRequests(null)
+        runTest(StartActivityParams(
+                uri = "https://failing.pm.server.android.com",
+                expected = "$TARGET_FOUR_PKG_NAME.TargetActivity"
+        ))
+
+        // Re-installing with a (now) larger host/verify set will re-request and demote
+        installPackage(TARGET_FOUR_WILDCARD)
+        // TODO(b/159952358): The first request should not be sent. This is caused by the request
+        //  being queued even if it should be dropped from the previous install case.
+        assertReceivedRequests(false, VerifyRequest(
+                scheme = "https",
+                host = "failing.pm.server.android.com",
+                packageName = TARGET_FOUR_PKG_NAME
+        ), VerifyRequest(
+                scheme = "https",
+                hosts = listOf("failing.pm.server.android.com", "wildcard.tld"),
+                packageName = TARGET_FOUR_PKG_NAME
+        ))
+
+        runTest(StartActivityParams(
+                uri = "https://failing.pm.server.android.com",
+                expected = "$TARGET_FOUR_PKG_NAME.TargetActivity",
+                withBrowsers = true
+        ))
+    }
+
+    // TODO(b/159952358): I would expect this to demote
+    // TODO(b/32810168)
+    @Test
+    fun verifiedUpdateRemovingAutoVerifyMaintainsAlways() {
+        installPackage(TARGET_FOUR_BASE)
+        assertReceivedRequests(true, VerifyRequest(
+                scheme = "https",
+                host = "failing.pm.server.android.com",
+                packageName = TARGET_FOUR_PKG_NAME
+        ))
+
+        runTest(StartActivityParams(
+                uri = "https://failing.pm.server.android.com",
+                expected = "$TARGET_FOUR_PKG_NAME.TargetActivity"
+        ))
+
+        installPackage(TARGET_FOUR_NO_AUTO_VERIFY)
+        assertReceivedRequests(null)
+
+        runTest(StartActivityParams(
+                uri = "https://failing.pm.server.android.com",
+                expected = "$TARGET_FOUR_PKG_NAME.TargetActivity"
+        ))
+    }
+
+    @Test
+    fun verifiedUpdateRemovingAutoVerifyAddingDomainDemotesAlways() {
+        installPackage(TARGET_FOUR_BASE)
+
+        assertReceivedRequests(true, VerifyRequest(
+                scheme = "https",
+                host = "failing.pm.server.android.com",
+                packageName = TARGET_FOUR_PKG_NAME
+        ))
+
+        runTest(StartActivityParams(
+                uri = "https://failing.pm.server.android.com",
+                expected = "$TARGET_FOUR_PKG_NAME.TargetActivity"
+        ))
+
+        installPackage(TARGET_FOUR_WILDCARD_NO_AUTO_VERIFY)
+        assertReceivedRequests(null)
+
+        runTest(StartActivityParams(
+                uri = "https://failing.pm.server.android.com",
+                expected = "$TARGET_FOUR_PKG_NAME.TargetActivity",
+                withBrowsers = true
+        ))
+    }
+
+    private fun installPackage(javaResourceName: String) {
+        // Need to pass --user as verification is not currently run for all user installs
+        assertThat(device.installJavaResourceApk(tempFolder, javaResourceName,
+                extraArgs = arrayOf("--user", device.currentUser.toString()))).isNull()
+    }
+
+    private fun assertReceivedRequests(success: Boolean?, vararg expected: VerifyRequest?) {
+        // TODO(b/159952358): This can probably be less than 10
+        // Because tests have to assert that multiple broadcasts aren't received, there's no real
+        // better way to await for a value than sleeping for a long enough time.
+        TimeUnit.SECONDS.sleep(10)
+
+        val params = mutableMapOf<String, String>()
+        if (expected.any { it != null }) {
+            params["expected"] = expected.filterNotNull()
+                    .joinToString(separator = "") { it.serializeToString() }
+        }
+        runTest("compareLastReceived", params)
+
+        if (success != null) {
+            if (success) {
+                runTest("verifyPreviousReceivedSuccess")
+            } else {
+                runTest("verifyPreviousReceivedFailure")
+            }
+            runTest("clearResponse")
+        }
+    }
+
+    private fun runTest(params: IntentVerifyTestParams) =
+            runTest(params.methodName, params.toArgsMap())
+
+    private fun runTest(testName: String, args: Map<String, String> = emptyMap()) {
+        val escapedArgs = args.mapValues {
+            // Need to escape strings so that args are passed properly through the shell command
+            "\"${it.value.trim('"')}\""
+        }
+        runDeviceTests(device, null, VERIFIER_PKG_NAME, "$VERIFIER_PKG_NAME.VerifyReceiverTest",
+                testName, null, 10 * 60 * 1000L, 10 * 60 * 1000L, 0L, true, false, escapedArgs)
+    }
+}
diff --git a/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifier/Android.bp b/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifier/Android.bp
new file mode 100644
index 0000000..e82f57d
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifier/Android.bp
@@ -0,0 +1,28 @@
+// 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.
+
+android_test_helper_app {
+    name: "PackageManagerTestIntentVerifier",
+    srcs: [ "src/**/*.kt" ],
+    static_libs: [
+        "androidx.test.core",
+        "androidx.test.espresso.core",
+        "androidx.test.runner",
+        "compatibility-device-util-axt",
+        "junit",
+        "truth-prebuilt",
+        "PackageManagerServiceHostTestsIntentVerifyUtils",
+    ],
+    platform_apis: true,
+}
diff --git a/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifier/AndroidManifest.xml b/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifier/AndroidManifest.xml
new file mode 100644
index 0000000..17b50b0
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifier/AndroidManifest.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.server.pm.test.intent.verifier"
+    >
+
+    <uses-permission android:name="android.permission.INTENT_FILTER_VERIFICATION_AGENT" />
+    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+    <uses-permission android:name="android.permission.SET_PREFERRED_APPLICATIONS" />
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.server.pm.test.intent.verifier"
+        />
+
+    <application>
+        <receiver android:name=".VerifyReceiver" android:exported="true">
+            <intent-filter android:priority="999">
+                <action android:name="android.intent.action.INTENT_FILTER_NEEDS_VERIFICATION"/>
+                <data android:mimeType="application/vnd.android.package-archive"/>
+            </intent-filter>
+        </receiver>
+    </application>
+
+</manifest>
diff --git a/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifier/src/com/android/server/pm/test/intent/verifier/VerifyReceiver.kt b/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifier/src/com/android/server/pm/test/intent/verifier/VerifyReceiver.kt
new file mode 100644
index 0000000..073c2be
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifier/src/com/android/server/pm/test/intent/verifier/VerifyReceiver.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.server.pm.test.intent.verifier
+
+import android.content.BroadcastReceiver
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import com.android.server.pm.test.intent.verify.VerifyRequest
+
+class VerifyReceiver : BroadcastReceiver() {
+
+    override fun onReceive(context: Context, intent: Intent) {
+        if (intent.action != Intent.ACTION_INTENT_FILTER_NEEDS_VERIFICATION) return
+        val params = intent.toVerifyParams()
+
+        // If the receiver is called for a normal request, proxy it to the real verifier on device
+        if (params.hosts.none { it.contains("pm.server.android.com") }) {
+            sendToRealVerifier(context, Intent(intent))
+            return
+        }
+
+        // When the receiver is invoked for a test install, there is no direct connection to host,
+        // so store the result in a file to read and assert on later. Append is intentional so that
+        // amount of invocations and clean up can be verified.
+        context.filesDir.resolve("test.txt")
+                .appendText(params.serializeToString())
+    }
+
+    private fun sendToRealVerifier(context: Context, intent: Intent) {
+        context.packageManager.queryBroadcastReceivers(intent, 0)
+                .first { it.activityInfo?.packageName != context.packageName }
+                .let { it.activityInfo!! }
+                .let { intent.setComponent(ComponentName(it.packageName, it.name)) }
+                .run { context.sendBroadcast(intent) }
+    }
+
+    private fun Intent.toVerifyParams() = VerifyRequest(
+            id = getIntExtra(PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_ID, -1),
+            scheme = getStringExtra(PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_URI_SCHEME)!!,
+            hosts = getStringExtra(PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_HOSTS)!!
+                    .split(' '),
+            packageName = getStringExtra(
+                    PackageManager.EXTRA_INTENT_FILTER_VERIFICATION_PACKAGE_NAME)!!
+
+    )
+}
diff --git a/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifier/src/com/android/server/pm/test/intent/verifier/VerifyReceiverTest.kt b/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifier/src/com/android/server/pm/test/intent/verifier/VerifyReceiverTest.kt
new file mode 100644
index 0000000..6de3d4e
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifier/src/com/android/server/pm/test/intent/verifier/VerifyReceiverTest.kt
@@ -0,0 +1,160 @@
+/*
+ * 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.server.pm.test.intent.verifier
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Bundle
+import android.os.UserHandle
+import androidx.test.InstrumentationRegistry
+import androidx.test.runner.AndroidJUnit4
+import com.android.compatibility.common.util.ShellIdentityUtils
+import com.android.server.pm.test.intent.verify.SetActivityAsAlwaysParams
+import com.android.server.pm.test.intent.verify.StartActivityParams
+import com.android.server.pm.test.intent.verify.VerifyRequest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.io.File
+
+@RunWith(AndroidJUnit4::class)
+class VerifyReceiverTest {
+
+    val args: Bundle = InstrumentationRegistry.getArguments()
+    val context: Context = InstrumentationRegistry.getContext()
+
+    private val file = context.filesDir.resolve("test.txt")
+
+    @Test
+    fun clearResponse() {
+        file.delete()
+    }
+
+    @Test
+    fun compareLastReceived() {
+        val lastReceivedText = file.readTextIfExists()
+        val expectedText = args.getString("expected")
+        if (expectedText.isNullOrEmpty()) {
+            assertThat(lastReceivedText).isEmpty()
+            return
+        }
+
+        val expectedParams = expectedText.parseParams()
+        val lastReceivedParams = lastReceivedText.parseParams()
+
+        assertThat(lastReceivedParams).hasSize(expectedParams.size)
+
+        lastReceivedParams.zip(expectedParams).forEach { (actual, expected) ->
+            assertThat(actual.hosts).containsExactlyElementsIn(expected.hosts)
+            assertThat(actual.packageName).isEqualTo(expected.packageName)
+            assertThat(actual.scheme).isEqualTo(expected.scheme)
+        }
+    }
+
+    @Test
+    fun setActivityAsAlways() {
+        val params = SetActivityAsAlwaysParams.fromArgs(
+                args.keySet().associateWith { args.getString(it)!! })
+        val uri = Uri.parse(params.uri)
+        val filter = IntentFilter().apply {
+            addAction(Intent.ACTION_VIEW)
+            addCategory(Intent.CATEGORY_DEFAULT)
+            addDataScheme(uri.scheme)
+            addDataAuthority(uri.authority, null)
+        }
+
+        val intent = Intent(Intent.ACTION_VIEW, uri).apply {
+            addCategory(Intent.CATEGORY_DEFAULT)
+        }
+        val allResults = context.packageManager.queryIntentActivities(intent, 0)
+        val allComponents = allResults
+                .map { ComponentName(it.activityInfo.packageName, it.activityInfo.name) }
+                .toTypedArray()
+        val matchingInfo = allResults.first {
+            it.activityInfo.packageName == params.packageName &&
+                    it.activityInfo.name == params.activityName
+        }
+
+        ShellIdentityUtils.invokeMethodWithShellPermissions(context.packageManager,
+                ShellIdentityUtils.ShellPermissionMethodHelper<Unit, PackageManager> {
+                    it.addUniquePreferredActivity(filter, matchingInfo.match, allComponents,
+                            ComponentName(matchingInfo.activityInfo.packageName,
+                                    matchingInfo.activityInfo.name))
+                    it.updateIntentVerificationStatusAsUser(params.packageName,
+                            PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_ALWAYS,
+                            UserHandle.myUserId())
+                }, "android.permission.SET_PREFERRED_APPLICATIONS")
+    }
+
+    @Test
+    fun verifyPreviousReceivedSuccess() {
+        file.readTextIfExists()
+                .parseParams()
+                .forEach {
+                    context.packageManager.verifyIntentFilter(it.id,
+                            PackageManager.INTENT_FILTER_VERIFICATION_SUCCESS, emptyList())
+                }
+    }
+
+    @Test
+    fun verifyPreviousReceivedFailure() {
+        file.readTextIfExists()
+                .parseParams()
+                .forEach {
+                    context.packageManager.verifyIntentFilter(it.id,
+                            PackageManager.INTENT_FILTER_VERIFICATION_FAILURE, it.hosts)
+                }
+    }
+
+    @Test
+    fun verifyActivityStart() {
+        val params = StartActivityParams
+                .fromArgs(args.keySet().associateWith { args.getString(it)!! })
+        val uri = Uri.parse(params.uri)
+        val intent = Intent(Intent.ACTION_VIEW).apply {
+            data = uri
+            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+        }
+
+        val expectedActivities = params.expected.toMutableList()
+
+        if (params.withBrowsers) {
+            // Since the host doesn't know what browsers the device has, query here and add it to
+            // set if it's expected that browser are returned
+            val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://example.com"))
+            expectedActivities += context.packageManager.queryIntentActivities(browserIntent, 0)
+                    .map { it.activityInfo.name }
+        }
+
+        val infos = context.packageManager.queryIntentActivities(intent, 0)
+                .map { it.activityInfo.name }
+        assertThat(infos).containsExactlyElementsIn(expectedActivities)
+    }
+
+    private fun File.readTextIfExists() = if (exists()) readText() else ""
+
+    // Rudimentary list deserialization by splitting text block into 4 line sections
+    private fun String.parseParams() = trim()
+            .lines()
+            .windowed(4, 4)
+            .map { it.joinToString(separator = "\n") }
+            .map { VerifyRequest.deserialize(it) }
+}
diff --git a/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifierTarget/Android.bp b/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifierTarget/Android.bp
new file mode 100644
index 0000000..7161fdd
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifierTarget/Android.bp
@@ -0,0 +1,48 @@
+// 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.
+
+android_test_helper_app {
+    name: "PackageManagerTestIntentVerifierTarget1",
+    manifest: "AndroidManifest1.xml",
+}
+
+android_test_helper_app {
+    name: "PackageManagerTestIntentVerifierTarget2",
+    manifest: "AndroidManifest2.xml",
+}
+
+android_test_helper_app {
+    name: "PackageManagerTestIntentVerifierTarget3",
+    manifest: "AndroidManifest3.xml",
+}
+
+android_test_helper_app {
+    name: "PackageManagerTestIntentVerifierTarget4Base",
+    manifest: "AndroidManifest4Base.xml",
+}
+
+android_test_helper_app {
+    name: "PackageManagerTestIntentVerifierTarget4NoAutoVerify",
+    manifest: "AndroidManifest4NoAutoVerify.xml",
+}
+
+android_test_helper_app {
+    name: "PackageManagerTestIntentVerifierTarget4Wildcard",
+    manifest: "AndroidManifest4Wildcard.xml",
+}
+
+android_test_helper_app {
+    name: "PackageManagerTestIntentVerifierTarget4WildcardNoAutoVerify",
+    manifest: "AndroidManifest4WildcardNoAutoVerify.xml",
+}
diff --git a/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifierTarget/AndroidManifest1.xml b/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifierTarget/AndroidManifest1.xml
new file mode 100644
index 0000000..6cf5c76
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifierTarget/AndroidManifest1.xml
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.server.pm.test.intent.verifier.target.one" android:versionCode="1">
+
+    <application>
+        <activity android:name=".TargetActivity" android:exported="true">
+            <intent-filter android:autoVerify="true">
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:scheme="http" />
+                <data android:scheme="https" />
+                <data android:host="verify.pm.server.android.com" />
+            </intent-filter>
+
+            <intent-filter android:autoVerify="false">
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:scheme="http" />
+                <data android:scheme="https" />
+                <data android:host="no_verify.pm.server.android.com" />
+            </intent-filter>
+
+            <intent-filter android:autoVerify="true">
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:scheme="http" />
+                <data android:host="http_only.pm.server.android.com" />
+            </intent-filter>
+
+            <intent-filter android:autoVerify="true">
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:scheme="https" />
+                <data android:host="https_only.pm.server.android.com" />
+            </intent-filter>
+
+            <intent-filter android:autoVerify="true">
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:scheme="htttps" />
+                <data android:host="non_http.pm.server.android.com" />
+            </intent-filter>
+
+            <intent-filter android:autoVerify="true">
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:scheme="https" />
+                <data android:scheme="non_web_scheme" />
+                <data android:host="https_plus_non_web_scheme.pm.server.android.com" />
+            </intent-filter>
+        </activity>
+
+        <activity android:name=".TargetActivity2" android:exported="true">
+            <intent-filter android:autoVerify="true">
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:scheme="http" />
+                <data android:scheme="https" />
+                <data android:host="other_activity.pm.server.android.com" />
+            </intent-filter>
+
+            <intent-filter android:autoVerify="true">
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:scheme="http" />
+                <data android:scheme="https" />
+                <data android:host="multiple.pm.server.android.com" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
diff --git a/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifierTarget/AndroidManifest2.xml b/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifierTarget/AndroidManifest2.xml
new file mode 100644
index 0000000..087ef705
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifierTarget/AndroidManifest2.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.server.pm.test.intent.verifier.target.two"
+    android:versionCode="1">
+
+    <application>
+        <activity android:name=".TargetActivity" android:exported="true">
+            <intent-filter android:autoVerify="true">
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:scheme="http" />
+                <data android:scheme="https" />
+                <data android:scheme="non_web_scheme" />
+                <data android:host="only_https_plus_non_web_scheme.pm.server.android.com" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
diff --git a/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifierTarget/AndroidManifest3.xml b/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifierTarget/AndroidManifest3.xml
new file mode 100644
index 0000000..eb75b5e
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifierTarget/AndroidManifest3.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.server.pm.test.intent.verifier.target.three"
+    android:versionCode="1">
+
+    <application>
+        <activity android:name=".TargetActivity" android:exported="true">
+            <intent-filter android:autoVerify="true">
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:scheme="http" />
+                <data android:host="multiple.pm.server.android.com" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
diff --git a/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifierTarget/AndroidManifest4Base.xml b/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifierTarget/AndroidManifest4Base.xml
new file mode 100644
index 0000000..7eacb8b
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifierTarget/AndroidManifest4Base.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.server.pm.test.intent.verifier.target.four"
+    android:versionCode="1">
+
+    <application>
+        <activity android:name=".TargetActivity" android:exported="true">
+            <intent-filter android:autoVerify="true">
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:scheme="http" />
+                <data android:scheme="https" />
+                <data android:host="failing.pm.server.android.com" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
diff --git a/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifierTarget/AndroidManifest4NoAutoVerify.xml b/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifierTarget/AndroidManifest4NoAutoVerify.xml
new file mode 100644
index 0000000..ecfee55
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifierTarget/AndroidManifest4NoAutoVerify.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.server.pm.test.intent.verifier.target.four"
+    android:versionCode="1">
+
+    <application>
+        <activity android:name=".TargetActivity" android:exported="true">
+            <intent-filter android:autoVerify="false">
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:scheme="http" />
+                <data android:scheme="https" />
+                <data android:host="failing.pm.server.android.com" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
diff --git a/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifierTarget/AndroidManifest4Wildcard.xml b/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifierTarget/AndroidManifest4Wildcard.xml
new file mode 100644
index 0000000..0f0f53b
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifierTarget/AndroidManifest4Wildcard.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.server.pm.test.intent.verifier.target.four"
+    android:versionCode="1">
+
+    <application>
+        <activity android:name=".TargetActivity" android:exported="true">
+            <intent-filter android:autoVerify="true">
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:scheme="http" />
+                <data android:scheme="https" />
+                <data android:host="failing.pm.server.android.com" />
+                <data android:host="*.wildcard.tld" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
diff --git a/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifierTarget/AndroidManifest4WildcardNoAutoVerify.xml b/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifierTarget/AndroidManifest4WildcardNoAutoVerify.xml
new file mode 100644
index 0000000..d5652e1
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/host/test-apps/IntentVerifierTarget/AndroidManifest4WildcardNoAutoVerify.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.server.pm.test.intent.verifier.target.four"
+    android:versionCode="1">
+
+    <application>
+        <activity android:name=".TargetActivity" android:exported="true">
+            <intent-filter android:autoVerify="false">
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <data android:scheme="http" />
+                <data android:scheme="https" />
+                <data android:host="failing.pm.server.android.com" />
+                <data android:host="*.wildcard.tld" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
diff --git a/services/tests/servicestests/src/com/android/server/VibratorServiceTest.java b/services/tests/servicestests/src/com/android/server/VibratorServiceTest.java
index 02d10e3..b7a36f2 100644
--- a/services/tests/servicestests/src/com/android/server/VibratorServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/VibratorServiceTest.java
@@ -223,9 +223,24 @@
     }
 
     @Test
-    public void arePrimitivesSupported_withComposeCapability_returnsAlwaysTrue() {
+    public void arePrimitivesSupported_withNullResultFromNative_returnsAlwaysFalse() {
         mockVibratorCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
-        assertArrayEquals(new boolean[]{true, true},
+        when(mNativeWrapperMock.vibratorGetSupportedPrimitives()).thenReturn(null);
+
+        assertArrayEquals(new boolean[]{false, false},
+                createService().arePrimitivesSupported(new int[]{
+                        VibrationEffect.Composition.PRIMITIVE_CLICK,
+                        VibrationEffect.Composition.PRIMITIVE_QUICK_RISE
+                }));
+    }
+
+    @Test
+    public void arePrimitivesSupported_withSomeSupportedPrimitives_returnsBasedOnNativeResult() {
+        mockVibratorCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
+        when(mNativeWrapperMock.vibratorGetSupportedPrimitives())
+                .thenReturn(new int[]{VibrationEffect.Composition.PRIMITIVE_CLICK});
+
+        assertArrayEquals(new boolean[]{true, false},
                 createService().arePrimitivesSupported(new int[]{
                         VibrationEffect.Composition.PRIMITIVE_CLICK,
                         VibrationEffect.Composition.PRIMITIVE_QUICK_RISE
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
index 8fc2287..7f6723e 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java
@@ -149,6 +149,7 @@
     public static final String NOT_PROFILE_OWNER_MSG = "does not own the profile";
     public static final String NOT_ORG_OWNED_PROFILE_OWNER_MSG =
             "not the profile owner on organization-owned device";
+    public static final String INVALID_CALLING_IDENTITY_MSG = "Calling identity is not authorized";
     public static final String ONGOING_CALL_MSG = "ongoing call on the device";
 
     // TODO replace all instances of this with explicit {@link #mServiceContext}.
@@ -2404,13 +2405,13 @@
         // Set admin1 as DA.
         dpm.setActiveAdmin(admin1, false);
         assertTrue(dpm.isAdminActive(admin1));
-        assertExpectException(SecurityException.class, /* messageRegex= */ NOT_DEVICE_OWNER_MSG,
-                () -> dpm.reboot(admin1));
+        assertExpectException(SecurityException.class, /* messageRegex= */
+                INVALID_CALLING_IDENTITY_MSG, () -> dpm.reboot(admin1));
 
         // Set admin1 as PO.
         assertTrue(dpm.setProfileOwner(admin1, null, UserHandle.USER_SYSTEM));
-        assertExpectException(SecurityException.class, /* messageRegex= */ NOT_DEVICE_OWNER_MSG,
-                () -> dpm.reboot(admin1));
+        assertExpectException(SecurityException.class, /* messageRegex= */
+                INVALID_CALLING_IDENTITY_MSG, () -> dpm.reboot(admin1));
 
         // Remove PO and add DO.
         dpm.clearProfileOwner(admin1);
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
index 0ea84da..26d46db 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
@@ -1276,7 +1276,8 @@
          * @return The initialized AudioRecord
          */
         private @NonNull AudioRecord createAudioRecordForEvent(
-                @NonNull SoundTrigger.GenericRecognitionEvent event) {
+                @NonNull SoundTrigger.GenericRecognitionEvent event)
+                throws IllegalArgumentException, UnsupportedOperationException {
             AudioAttributes.Builder attributesBuilder = new AudioAttributes.Builder();
             attributesBuilder.setInternalCapturePreset(MediaRecorder.AudioSource.HOTWORD);
             AudioAttributes attributes = attributesBuilder.build();
@@ -1285,21 +1286,15 @@
 
             sEventLogger.log(new SoundTriggerLogger.StringEvent("createAudioRecordForEvent"));
 
-            try {
-                return (new AudioRecord.Builder())
-                            .setAudioAttributes(attributes)
-                            .setAudioFormat((new AudioFormat.Builder())
-                                .setChannelMask(originalFormat.getChannelMask())
-                                .setEncoding(originalFormat.getEncoding())
-                                .setSampleRate(originalFormat.getSampleRate())
-                                .build())
-                            .setSessionId(event.getCaptureSession())
-                            .build();
-            } catch (IllegalArgumentException | UnsupportedOperationException e) {
-                Slog.w(TAG, mPuuid + ": createAudioRecordForEvent(" + event
-                        + "), failed to create AudioRecord");
-                return null;
-            }
+            return (new AudioRecord.Builder())
+                        .setAudioAttributes(attributes)
+                        .setAudioFormat((new AudioFormat.Builder())
+                            .setChannelMask(originalFormat.getChannelMask())
+                            .setEncoding(originalFormat.getEncoding())
+                            .setSampleRate(originalFormat.getSampleRate())
+                            .build())
+                        .setSessionId(event.getCaptureSession())
+                        .build();
         }
 
         @Override
@@ -1325,13 +1320,13 @@
                     // execute if throttled:
                     () -> {
                         if (event.isCaptureAvailable()) {
-                            AudioRecord capturedData = createAudioRecordForEvent(event);
-
-                            // Currently we need to start and release the audio record to reset
-                            // the DSP even if we don't want to process the event
-                            if (capturedData != null) {
+                            try {
+                                AudioRecord capturedData = createAudioRecordForEvent(event);
                                 capturedData.startRecording();
                                 capturedData.release();
+                            } catch (IllegalArgumentException | UnsupportedOperationException e) {
+                                Slog.w(TAG, mPuuid + ": createAudioRecordForEvent(" + event
+                                        + "), failed to create AudioRecord");
                             }
                         }
                     }));
diff --git a/tests/AutoVerify/app1/Android.bp b/tests/AutoVerify/app1/Android.bp
deleted file mode 100644
index 548519f..0000000
--- a/tests/AutoVerify/app1/Android.bp
+++ /dev/null
@@ -1,11 +0,0 @@
-android_app {
-    name: "AutoVerifyTest",
-    srcs: ["src/**/*.java"],
-    resource_dirs: ["res"],
-    platform_apis: true,
-    min_sdk_version: "26",
-    target_sdk_version: "26",
-    optimize: {
-        enabled: false,
-    },
-}
diff --git a/tests/AutoVerify/app1/AndroidManifest.xml b/tests/AutoVerify/app1/AndroidManifest.xml
deleted file mode 100644
index d9caad4..0000000
--- a/tests/AutoVerify/app1/AndroidManifest.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     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.
--->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.test.autoverify" >
-
-    <uses-sdk android:targetSdkVersion="26" />
-
-    <application
-        android:label="@string/app_name" >
-        <activity
-            android:name=".MainActivity"
-            android:label="@string/app_name" >
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
-            </intent-filter>
-
-            <intent-filter android:autoVerify="true">
-                <action android:name="android.intent.action.VIEW" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="http" />
-                <data android:scheme="https" />
-                <data android:host="explicit.example.com" />
-            </intent-filter>
-        </activity>
-    </application>
-</manifest>
diff --git a/tests/AutoVerify/app1/res/values/strings.xml b/tests/AutoVerify/app1/res/values/strings.xml
deleted file mode 100644
index e234355..0000000
--- a/tests/AutoVerify/app1/res/values/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-Copyright 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.
--->
-
-<resources>
-    <!-- app icon label, do not translate -->
-    <string name="app_name" translatable="false">AutoVerify Test</string>
-</resources>
diff --git a/tests/AutoVerify/app2/Android.bp b/tests/AutoVerify/app2/Android.bp
deleted file mode 100644
index 1c6c97b..0000000
--- a/tests/AutoVerify/app2/Android.bp
+++ /dev/null
@@ -1,11 +0,0 @@
-android_app {
-    name: "AutoVerifyTest2",
-    srcs: ["src/**/*.java"],
-    resource_dirs: ["res"],
-    platform_apis: true,
-    min_sdk_version: "26",
-    target_sdk_version: "26",
-    optimize: {
-        enabled: false,
-    },
-}
diff --git a/tests/AutoVerify/app2/AndroidManifest.xml b/tests/AutoVerify/app2/AndroidManifest.xml
deleted file mode 100644
index a008078..0000000
--- a/tests/AutoVerify/app2/AndroidManifest.xml
+++ /dev/null
@@ -1,44 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     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.
--->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.test.autoverify" >
-
-    <uses-sdk android:targetSdkVersion="26" />
-
-    <application
-        android:label="@string/app_name" >
-        <activity
-            android:name=".MainActivity"
-            android:label="@string/app_name" >
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
-            </intent-filter>
-
-            <intent-filter android:autoVerify="true">
-                <action android:name="android.intent.action.VIEW" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="http" />
-                <data android:scheme="https" />
-                <data android:host="explicit.example.com" />
-                <data android:host="*.wildcard.tld" />
-            </intent-filter>
-        </activity>
-    </application>
-</manifest>
diff --git a/tests/AutoVerify/app2/res/values/strings.xml b/tests/AutoVerify/app2/res/values/strings.xml
deleted file mode 100644
index e234355..0000000
--- a/tests/AutoVerify/app2/res/values/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-Copyright 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.
--->
-
-<resources>
-    <!-- app icon label, do not translate -->
-    <string name="app_name" translatable="false">AutoVerify Test</string>
-</resources>
diff --git a/tests/AutoVerify/app2/src/com/android/test/autoverify/MainActivity.java b/tests/AutoVerify/app2/src/com/android/test/autoverify/MainActivity.java
deleted file mode 100644
index 09ef472..0000000
--- a/tests/AutoVerify/app2/src/com/android/test/autoverify/MainActivity.java
+++ /dev/null
@@ -1,15 +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.
- */
diff --git a/tests/AutoVerify/app3/Android.bp b/tests/AutoVerify/app3/Android.bp
deleted file mode 100644
index 70a2b77..0000000
--- a/tests/AutoVerify/app3/Android.bp
+++ /dev/null
@@ -1,11 +0,0 @@
-android_app {
-    name: "AutoVerifyTest3",
-    srcs: ["src/**/*.java"],
-    resource_dirs: ["res"],
-    platform_apis: true,
-    min_sdk_version: "26",
-    target_sdk_version: "26",
-    optimize: {
-        enabled: false,
-    },
-}
diff --git a/tests/AutoVerify/app3/AndroidManifest.xml b/tests/AutoVerify/app3/AndroidManifest.xml
deleted file mode 100644
index efaabc9..0000000
--- a/tests/AutoVerify/app3/AndroidManifest.xml
+++ /dev/null
@@ -1,44 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     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.
--->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.test.autoverify" >
-
-    <uses-sdk android:targetSdkVersion="26" />
-
-    <application
-        android:label="@string/app_name" >
-        <activity
-            android:name=".MainActivity"
-            android:label="@string/app_name" >
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
-            </intent-filter>
-
-            <!-- does not request autoVerify -->
-            <intent-filter>
-                <action android:name="android.intent.action.VIEW" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="http" />
-                <data android:scheme="https" />
-                <data android:host="explicit.example.com" />
-            </intent-filter>
-        </activity>
-    </application>
-</manifest>
diff --git a/tests/AutoVerify/app3/res/values/strings.xml b/tests/AutoVerify/app3/res/values/strings.xml
deleted file mode 100644
index e234355..0000000
--- a/tests/AutoVerify/app3/res/values/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-Copyright 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.
--->
-
-<resources>
-    <!-- app icon label, do not translate -->
-    <string name="app_name" translatable="false">AutoVerify Test</string>
-</resources>
diff --git a/tests/AutoVerify/app3/src/com/android/test/autoverify/MainActivity.java b/tests/AutoVerify/app3/src/com/android/test/autoverify/MainActivity.java
deleted file mode 100644
index 09ef472..0000000
--- a/tests/AutoVerify/app3/src/com/android/test/autoverify/MainActivity.java
+++ /dev/null
@@ -1,15 +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.
- */
diff --git a/tests/AutoVerify/app4/Android.bp b/tests/AutoVerify/app4/Android.bp
deleted file mode 100644
index fbdae11..0000000
--- a/tests/AutoVerify/app4/Android.bp
+++ /dev/null
@@ -1,11 +0,0 @@
-android_app {
-    name: "AutoVerifyTest4",
-    srcs: ["src/**/*.java"],
-    resource_dirs: ["res"],
-    platform_apis: true,
-    min_sdk_version: "26",
-    target_sdk_version: "26",
-    optimize: {
-        enabled: false,
-    },
-}
diff --git a/tests/AutoVerify/app4/AndroidManifest.xml b/tests/AutoVerify/app4/AndroidManifest.xml
deleted file mode 100644
index 1c975f8..0000000
--- a/tests/AutoVerify/app4/AndroidManifest.xml
+++ /dev/null
@@ -1,45 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     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.
--->
-
-<manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.android.test.autoverify" >
-
-    <uses-sdk android:targetSdkVersion="26" />
-
-    <application
-        android:label="@string/app_name" >
-        <activity
-            android:name=".MainActivity"
-            android:label="@string/app_name" >
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
-            </intent-filter>
-
-            <!-- intentionally does not autoVerify -->
-            <intent-filter>
-                <action android:name="android.intent.action.VIEW" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="http" />
-                <data android:scheme="https" />
-                <data android:host="explicit.example.com" />
-                <data android:host="*.wildcard.tld" />
-            </intent-filter>
-        </activity>
-    </application>
-</manifest>
diff --git a/tests/AutoVerify/app4/res/values/strings.xml b/tests/AutoVerify/app4/res/values/strings.xml
deleted file mode 100644
index e234355..0000000
--- a/tests/AutoVerify/app4/res/values/strings.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-Copyright 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.
--->
-
-<resources>
-    <!-- app icon label, do not translate -->
-    <string name="app_name" translatable="false">AutoVerify Test</string>
-</resources>
diff --git a/tests/AutoVerify/app4/src/com/android/test/autoverify/MainActivity.java b/tests/AutoVerify/app4/src/com/android/test/autoverify/MainActivity.java
deleted file mode 100644
index 09ef472..0000000
--- a/tests/AutoVerify/app4/src/com/android/test/autoverify/MainActivity.java
+++ /dev/null
@@ -1,15 +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.
- */
diff --git a/tests/utils/hostutils/src/com/android/internal/util/test/SystemPreparer.java b/tests/utils/hostutils/src/com/android/internal/util/test/SystemPreparer.java
index c2a5459..f80af03 100644
--- a/tests/utils/hostutils/src/com/android/internal/util/test/SystemPreparer.java
+++ b/tests/utils/hostutils/src/com/android/internal/util/test/SystemPreparer.java
@@ -62,12 +62,22 @@
     private final RebootStrategy mRebootStrategy;
     private final TearDownRule mTearDownRule;
 
+    // When debugging, it may be useful to run a test case without rebooting the device afterwards,
+    // to manually verify the device state.
+    private boolean mDebugSkipAfterReboot;
+
     public SystemPreparer(TemporaryFolder hostTempFolder, DeviceProvider deviceProvider) {
         this(hostTempFolder, RebootStrategy.FULL, null, deviceProvider);
     }
 
     public SystemPreparer(TemporaryFolder hostTempFolder, RebootStrategy rebootStrategy,
             @Nullable TestRuleDelegate testRuleDelegate, DeviceProvider deviceProvider) {
+        this(hostTempFolder, rebootStrategy, testRuleDelegate, false, deviceProvider);
+    }
+
+    public SystemPreparer(TemporaryFolder hostTempFolder, RebootStrategy rebootStrategy,
+            @Nullable TestRuleDelegate testRuleDelegate, boolean debugSkipAfterReboot,
+            DeviceProvider deviceProvider) {
         mHostTempFolder = hostTempFolder;
         mDeviceProvider = deviceProvider;
         mRebootStrategy = rebootStrategy;
@@ -75,6 +85,7 @@
         if (testRuleDelegate != null) {
             testRuleDelegate.setDelegate(mTearDownRule);
         }
+        mDebugSkipAfterReboot = debugSkipAfterReboot;
     }
 
     /** Copies a file within the host test jar to a path on device. */
@@ -172,7 +183,9 @@
             case USERSPACE_UNTIL_ONLINE:
                 device.rebootUserspaceUntilOnline();
                 break;
-            case START_STOP:
+            // TODO(b/159540015): Make this START_STOP instead of default once it's fixed. Can't
+            //  currently be done because START_STOP is commented out.
+            default:
                 device.executeShellCommand("stop");
                 device.executeShellCommand("start");
                 ITestDevice.RecoveryMode cachedRecoveryMode = device.getRecoveryMode();
@@ -228,7 +241,9 @@
             for (final String packageName : mInstalledPackages) {
                 device.uninstallPackage(packageName);
             }
-            reboot();
+            if (!mDebugSkipAfterReboot) {
+                reboot();
+            }
         } catch (DeviceNotAvailableException e) {
             Assert.fail(e.toString());
         }
@@ -355,6 +370,7 @@
     /**
      * How to reboot the device. Ordered from slowest to fastest.
      */
+    @SuppressWarnings("DanglingJavadoc")
     public enum RebootStrategy {
         /** @see ITestDevice#reboot() */
         FULL,
@@ -374,7 +390,15 @@
          *
          * TODO(b/159540015): There's a bug with this causing unnecessary disk space usage, which
          *  can eventually lead to an insufficient storage space error.
+         *
+         * This can be uncommented for local development, but should be left out when merging.
+         * It is done this way to hopefully be caught by code review, since merging this will
+         * break all of postsubmit. But the nearly 50% reduction in test runtime is worth having
+         * this option exist.
+         *
+         * @deprecated do not use this in merged code until bug is resolved
          */
-        START_STOP
+//        @Deprecated
+//        START_STOP
     }
 }