Merge "Expand TestableDeviceConfigTest#getProperties()." into rvc-dev
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 2399e37..f613df2 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -385,6 +385,15 @@
      */
     public static final int WATCH_FOREGROUND_CHANGES = 1 << 0;
 
+
+    /**
+     * Flag to determine whether we should log noteOp/startOp calls to make sure they
+     * are correctly used
+     *
+     * @hide
+     */
+    public static final boolean NOTE_OP_COLLECTION_ENABLED = false;
+
     /**
      * @hide
      */
@@ -7103,6 +7112,7 @@
     public int noteOpNoThrow(int op, int uid, @Nullable String packageName,
             @Nullable String featureId, @Nullable String message) {
         try {
+            collectNoteOpCallsForValidation(op);
             int collectionMode = getNotedOpCollectionMode(uid, packageName, op);
             if (collectionMode == COLLECT_ASYNC) {
                 if (message == null) {
@@ -7263,6 +7273,7 @@
         int myUid = Process.myUid();
 
         try {
+            collectNoteOpCallsForValidation(op);
             int collectionMode = getNotedOpCollectionMode(proxiedUid, proxiedPackageName, op);
             if (collectionMode == COLLECT_ASYNC) {
                 if (message == null) {
@@ -7583,6 +7594,7 @@
     public int startOpNoThrow(int op, int uid, @NonNull String packageName,
             boolean startIfModeDefault, @Nullable String featureId, @Nullable String message) {
         try {
+            collectNoteOpCallsForValidation(op);
             int collectionMode = getNotedOpCollectionMode(uid, packageName, op);
             if (collectionMode == COLLECT_ASYNC) {
                 if (message == null) {
@@ -8492,4 +8504,24 @@
     public static int leftCircularDistance(int from, int to, int size) {
         return (to + size - from) % size;
     }
+
+    /**
+     * Helper method for noteOp, startOp and noteProxyOp to call AppOpsService to collect/log
+     * stack traces
+     *
+     * <p> For each call, the stacktrace op code, package name and long version code will be
+     * passed along where it will be logged/collected
+     *
+     * @param op The operation to note
+     */
+    private void collectNoteOpCallsForValidation(int op) {
+        if (NOTE_OP_COLLECTION_ENABLED) {
+            try {
+                mService.collectNoteOpCallsForValidation(getFormattedStackTrace(),
+                        op, mContext.getOpPackageName(), mContext.getApplicationInfo().longVersionCode);
+            } catch (RemoteException e) {
+                // Swallow error, only meant for logging ops, should not affect flow of the code
+            }
+        }
+    }
 }
diff --git a/core/java/android/os/IPowerManager.aidl b/core/java/android/os/IPowerManager.aidl
index 1cefbd9..c44a0bd 100644
--- a/core/java/android/os/IPowerManager.aidl
+++ b/core/java/android/os/IPowerManager.aidl
@@ -38,6 +38,8 @@
     void releaseWakeLock(IBinder lock, int flags);
     void updateWakeLockUids(IBinder lock, in int[] uids);
     oneway void powerHint(int hintId, int data);
+    oneway void setPowerBoost(int boost, int durationMs);
+    oneway void setPowerMode(int mode, boolean enabled);
 
     void updateWakeLockWorkSource(IBinder lock, in WorkSource ws, String historyTag);
     boolean isWakeLockLevelSupported(int level);
diff --git a/core/java/android/os/PowerManagerInternal.java b/core/java/android/os/PowerManagerInternal.java
index 9661a08..51f01ca 100644
--- a/core/java/android/os/PowerManagerInternal.java
+++ b/core/java/android/os/PowerManagerInternal.java
@@ -201,6 +201,119 @@
      */
     public abstract void powerHint(int hintId, int data);
 
+    /**
+     * Boost: It is sent when user interacting with the device, for example,
+     * touchscreen events are incoming.
+     * Defined in hardware/interfaces/power/aidl/android/hardware/power/Boost.aidl
+     */
+    public static final int BOOST_INTERACTION = 0;
+
+    /**
+     * Boost: It indicates that the framework is likely to provide a new display
+     * frame soon. This implies that the device should ensure that the display
+     * processing path is powered up and ready to receive that update.
+     * Defined in hardware/interfaces/power/aidl/android/hardware/power/Boost.aidl
+     */
+    public static final int BOOST_DISPLAY_UPDATE_IMMINENT = 1;
+
+    /**
+     * SetPowerBoost() indicates the device may need to boost some resources, as
+     * the load is likely to increase before the kernel governors can react.
+     * Depending on the boost, it may be appropriate to raise the frequencies of
+     * CPU, GPU, memory subsystem, or stop CPU from going into deep sleep state.
+     *
+     * @param boost Boost which is to be set with a timeout.
+     * @param durationMs The expected duration of the user's interaction, if
+     *        known, or 0 if the expected duration is unknown.
+     *        a negative value indicates canceling previous boost.
+     *        A given platform can choose to boost some time based on durationMs,
+     *        and may also pick an appropriate timeout for 0 case.
+     */
+    public abstract void setPowerBoost(int boost, int durationMs);
+
+    /**
+     * Mode: It indicates that the device is to allow wake up when the screen
+     * is tapped twice.
+     * Defined in hardware/interfaces/power/aidl/android/hardware/power/Mode.aidl
+     */
+    public static final int MODE_DOUBLE_TAP_TO_WAKE = 0;
+
+    /**
+     * Mode: It indicates Low power mode is activated or not. Low power mode
+     * is intended to save battery at the cost of performance.
+     * Defined in hardware/interfaces/power/aidl/android/hardware/power/Mode.aidl
+     */
+    public static final int MODE_LOW_POWER = 1;
+
+    /**
+     * Mode: It indicates Sustained Performance mode is activated or not.
+     * Sustained performance mode is intended to provide a consistent level of
+     * performance for a prolonged amount of time.
+     * Defined in hardware/interfaces/power/aidl/android/hardware/power/Mode.aidl
+     */
+    public static final int MODE_SUSTAINED_PERFORMANCE = 2;
+
+    /**
+     * Mode: It sets the device to a fixed performance level which can be sustained
+     * under normal indoor conditions for at least 10 minutes.
+     * Fixed performance mode puts both upper and lower bounds on performance such
+     * that any workload run while in a fixed performance mode should complete in
+     * a repeatable amount of time.
+     * Defined in hardware/interfaces/power/aidl/android/hardware/power/Mode.aidl
+     */
+    public static final int MODE_FIXED_PERFORMANCE = 3;
+
+    /**
+     * Mode: It indicates VR Mode is activated or not. VR mode is intended to
+     * provide minimum guarantee for performance for the amount of time the device
+     * can sustain it.
+     * Defined in hardware/interfaces/power/aidl/android/hardware/power/Mode.aidl
+     */
+    public static final int MODE_VR = 4;
+
+    /**
+     * Mode: It indicates that an application has been launched.
+     * Defined in hardware/interfaces/power/aidl/android/hardware/power/Mode.aidl
+     */
+    public static final int MODE_LAUNCH = 5;
+
+    /**
+     * Mode: It indicates that the device is about to enter a period of expensive
+     * rendering.
+     * Defined in hardware/interfaces/power/aidl/android/hardware/power/Mode.aidl
+     */
+    public static final int MODE_EXPENSIVE_RENDERING = 6;
+
+    /**
+     * Mode: It indicates that the device is about entering/leaving interactive
+     * state or on-interactive state.
+     * Defined in hardware/interfaces/power/aidl/android/hardware/power/Mode.aidl
+     */
+    public static final int MODE_INTERACTIVE = 7;
+
+    /**
+     * Mode: It indicates the device is in device idle, externally known as doze.
+     * Defined in hardware/interfaces/power/aidl/android/hardware/power/Mode.aidl
+     */
+    public static final int MODE_DEVICE_IDLE = 8;
+
+    /**
+     * Mode: It indicates that display is either off or still on but is optimized
+     * for low power.
+     * Defined in hardware/interfaces/power/aidl/android/hardware/power/Mode.aidl
+     */
+    public static final int MODE_DISPLAY_INACTIVE = 9;
+
+    /**
+     * SetPowerMode() is called to enable/disable specific hint mode, which
+     * may result in adjustment of power/performance parameters of the
+     * cpufreq governor and other controls on device side.
+     *
+     * @param mode Mode which is to be enable/disable.
+     * @param enabled true to enable, false to disable the mode.
+     */
+    public abstract void setPowerMode(int mode, boolean enabled);
+
     /** Returns whether there hasn't been a user activity event for the given number of ms. */
     public abstract boolean wasDeviceIdleFor(long ms);
 
diff --git a/core/java/com/android/internal/app/IAppOpsService.aidl b/core/java/com/android/internal/app/IAppOpsService.aidl
index 1c1c254..907ea55 100644
--- a/core/java/com/android/internal/app/IAppOpsService.aidl
+++ b/core/java/com/android/internal/app/IAppOpsService.aidl
@@ -103,4 +103,6 @@
     int checkOperationRaw(int code, int uid, String packageName);
 
     void reloadNonHistoricalState();
+
+    void collectNoteOpCallsForValidation(String stackTrace, int op, String packageName, long version);
 }
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index afa58d5..49edcf7 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -195,6 +195,7 @@
         <permission name="android.permission.GET_ACCOUNTS_PRIVILEGED"/>
         <permission name="android.permission.INTERACT_ACROSS_USERS"/>
         <permission name="android.permission.MANAGE_USERS"/>
+        <permission name="android.permission.READ_PRIVILEGED_PHONE_STATE"/>
         <permission name="android.permission.UPDATE_APP_OPS_STATS"/>
         <permission name="android.permission.USE_RESERVED_DISK"/>
     </privapp-permissions>
diff --git a/packages/CarSystemUI/res/layout/car_fullscreen_user_switcher.xml b/packages/CarSystemUI/res/layout/car_fullscreen_user_switcher.xml
index 55207b3..6ecab51 100644
--- a/packages/CarSystemUI/res/layout/car_fullscreen_user_switcher.xml
+++ b/packages/CarSystemUI/res/layout/car_fullscreen_user_switcher.xml
@@ -18,8 +18,7 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:id="@+id/fullscreen_user_switcher"
     android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:fitsSystemWindows="true">
+    android:layout_height="match_parent">
 
     <LinearLayout
         android:id="@+id/container"
@@ -28,11 +27,10 @@
         android:layout_alignParentTop="true"
         android:orientation="vertical">
 
-        <!-- TODO(b/150302361): Status bar is commented out since a top inset is being added which causes it to be displayed below the top of the screen. -->
-        <!--        <include
-                    layout="@layout/car_status_bar_header"
-                    android:layout_alignParentTop="true"
-                    android:theme="@android:style/Theme"/>-->
+        <include
+            layout="@layout/car_status_bar_header"
+            android:layout_alignParentTop="true"
+            android:theme="@android:style/Theme"/>
 
 
         <FrameLayout
@@ -42,9 +40,8 @@
                 android:id="@+id/user_grid"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
-                android:layout_gravity="center_vertical"/>
-            <!-- TODO(b/150302361): Re-add marginTop once status bar has been added back. -->
-            <!--                android:layout_marginTop="@dimen/car_user_switcher_margin_top"/>-->
+                android:layout_gravity="center_vertical"
+                android:layout_marginTop="@dimen/car_user_switcher_margin_top"/>
         </FrameLayout>
 
     </LinearLayout>
diff --git a/packages/CarSystemUI/res/layout/super_notification_shade.xml b/packages/CarSystemUI/res/layout/super_notification_shade.xml
index cb65045..e36d8ca 100644
--- a/packages/CarSystemUI/res/layout/super_notification_shade.xml
+++ b/packages/CarSystemUI/res/layout/super_notification_shade.xml
@@ -72,11 +72,6 @@
         android:layout_marginBottom="@dimen/navigation_bar_height"
         android:visibility="invisible"/>
 
-    <include layout="@layout/headsup_container"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:visibility="invisible"/>
-
     <include layout="@layout/status_bar_expanded"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
diff --git a/packages/CarSystemUI/src/com/android/systemui/CarSystemUIBinder.java b/packages/CarSystemUI/src/com/android/systemui/CarSystemUIBinder.java
index c010881..59fa9d0 100644
--- a/packages/CarSystemUI/src/com/android/systemui/CarSystemUIBinder.java
+++ b/packages/CarSystemUI/src/com/android/systemui/CarSystemUIBinder.java
@@ -18,6 +18,7 @@
 
 import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.bubbles.dagger.BubbleModule;
+import com.android.systemui.car.notification.CarNotificationModule;
 import com.android.systemui.globalactions.GlobalActionsComponent;
 import com.android.systemui.keyguard.KeyguardViewMediator;
 import com.android.systemui.keyguard.dagger.KeyguardModule;
@@ -49,7 +50,8 @@
 
 /** Binder for car specific {@link SystemUI} modules. */
 @Module(includes = {RecentsModule.class, CarStatusBarModule.class, NotificationsModule.class,
-        BubbleModule.class, KeyguardModule.class, OverlayWindowModule.class})
+        BubbleModule.class, KeyguardModule.class, OverlayWindowModule.class,
+        CarNotificationModule.class})
 public abstract class CarSystemUIBinder {
     /** Inject into AuthController. */
     @Binds
diff --git a/packages/CarSystemUI/src/com/android/systemui/CarSystemUIModule.java b/packages/CarSystemUI/src/com/android/systemui/CarSystemUIModule.java
index bae42b5..22c3acf 100644
--- a/packages/CarSystemUI/src/com/android/systemui/CarSystemUIModule.java
+++ b/packages/CarSystemUI/src/com/android/systemui/CarSystemUIModule.java
@@ -22,6 +22,7 @@
 import android.content.Context;
 
 import com.android.keyguard.KeyguardViewController;
+import com.android.systemui.car.CarDeviceProvisionedController;
 import com.android.systemui.car.CarDeviceProvisionedControllerImpl;
 import com.android.systemui.dagger.SystemUIRootComponent;
 import com.android.systemui.dock.DockManager;
@@ -137,4 +138,8 @@
     @Binds
     abstract DeviceProvisionedController bindDeviceProvisionedController(
             CarDeviceProvisionedControllerImpl deviceProvisionedController);
+
+    @Binds
+    abstract CarDeviceProvisionedController bindCarDeviceProvisionedController(
+            CarDeviceProvisionedControllerImpl deviceProvisionedController);
 }
diff --git a/packages/CarSystemUI/src/com/android/systemui/car/CarDeviceProvisionedControllerImpl.java b/packages/CarSystemUI/src/com/android/systemui/car/CarDeviceProvisionedControllerImpl.java
index 38d5211b..09e62d2 100644
--- a/packages/CarSystemUI/src/com/android/systemui/car/CarDeviceProvisionedControllerImpl.java
+++ b/packages/CarSystemUI/src/com/android/systemui/car/CarDeviceProvisionedControllerImpl.java
@@ -44,8 +44,9 @@
             CarSettings.Secure.KEY_SETUP_WIZARD_IN_PROGRESS);
     private final ContentObserver mCarSettingsObserver = new ContentObserver(
             Dependency.get(Dependency.MAIN_HANDLER)) {
+
         @Override
-        public void onChange(boolean selfChange, Uri uri, int userId) {
+        public void onChange(boolean selfChange, Uri uri, int flags) {
             if (USER_SETUP_IN_PROGRESS_URI.equals(uri)) {
                 notifyUserSetupInProgressChanged();
             }
diff --git a/packages/CarSystemUI/src/com/android/systemui/car/notification/CarHeadsUpNotificationSystemContainer.java b/packages/CarSystemUI/src/com/android/systemui/car/notification/CarHeadsUpNotificationSystemContainer.java
new file mode 100644
index 0000000..689d2d5
--- /dev/null
+++ b/packages/CarSystemUI/src/com/android/systemui/car/notification/CarHeadsUpNotificationSystemContainer.java
@@ -0,0 +1,118 @@
+/*
+ * 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 android.content.Context;
+import android.content.res.Resources;
+import android.graphics.PixelFormat;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+
+import com.android.car.notification.R;
+import com.android.car.notification.headsup.CarHeadsUpNotificationContainer;
+import com.android.systemui.car.CarDeviceProvisionedController;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.statusbar.car.CarStatusBar;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import dagger.Lazy;
+
+/**
+ * A controller for SysUI's HUN display.
+ */
+@Singleton
+public class CarHeadsUpNotificationSystemContainer implements CarHeadsUpNotificationContainer {
+    private final CarDeviceProvisionedController mCarDeviceProvisionedController;
+    private final Lazy<CarStatusBar> mCarStatusBarLazy;
+
+    private final ViewGroup mWindow;
+    private final FrameLayout mHeadsUpContentFrame;
+
+    private final boolean mEnableHeadsUpNotificationWhenNotificationShadeOpen;
+
+    @Inject
+    CarHeadsUpNotificationSystemContainer(Context context,
+            @Main Resources resources,
+            CarDeviceProvisionedController deviceProvisionedController,
+            WindowManager windowManager,
+            // TODO: Remove dependency on CarStatusBar
+            Lazy<CarStatusBar> carStatusBarLazy) {
+        mCarDeviceProvisionedController = deviceProvisionedController;
+        mCarStatusBarLazy = carStatusBarLazy;
+
+        WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                WindowManager.LayoutParams.WRAP_CONTENT,
+                WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG,
+                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                        | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
+                PixelFormat.TRANSLUCENT);
+
+        lp.gravity = Gravity.TOP;
+        lp.setTitle("HeadsUpNotification");
+
+        mWindow = (ViewGroup) LayoutInflater.from(context)
+                .inflate(R.layout.headsup_container, null, false);
+        windowManager.addView(mWindow, lp);
+        mWindow.setVisibility(View.INVISIBLE);
+        mHeadsUpContentFrame = mWindow.findViewById(R.id.headsup_content);
+
+        mEnableHeadsUpNotificationWhenNotificationShadeOpen = resources.getBoolean(
+                R.bool.config_enableHeadsUpNotificationWhenNotificationShadeOpen);
+    }
+
+    private void animateShow() {
+        if ((mEnableHeadsUpNotificationWhenNotificationShadeOpen
+                || !mCarStatusBarLazy.get().isPanelExpanded()) && isCurrentUserSetup()) {
+            mWindow.setVisibility(View.VISIBLE);
+        }
+    }
+
+    private void animateHide() {
+        mWindow.setVisibility(View.INVISIBLE);
+    }
+
+    @Override
+    public void displayNotification(View notificationView) {
+        mHeadsUpContentFrame.addView(notificationView);
+        animateShow();
+    }
+
+    @Override
+    public void removeNotification(View notificationView) {
+        mHeadsUpContentFrame.removeView(notificationView);
+        if (mHeadsUpContentFrame.getChildCount() == 0) {
+            animateHide();
+        }
+    }
+
+    @Override
+    public boolean isVisible() {
+        return mWindow.getVisibility() == View.VISIBLE;
+    }
+
+    private boolean isCurrentUserSetup() {
+        return mCarDeviceProvisionedController.isCurrentUserSetup()
+                && !mCarDeviceProvisionedController.isCurrentUserSetupInProgress();
+    }
+}
diff --git a/packages/CarSystemUI/src/com/android/systemui/car/notification/CarNotificationModule.java b/packages/CarSystemUI/src/com/android/systemui/car/notification/CarNotificationModule.java
new file mode 100644
index 0000000..b7bc631
--- /dev/null
+++ b/packages/CarSystemUI/src/com/android/systemui/car/notification/CarNotificationModule.java
@@ -0,0 +1,84 @@
+/*
+ * 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 android.content.Context;
+
+import com.android.car.notification.CarHeadsUpNotificationManager;
+import com.android.car.notification.CarNotificationListener;
+import com.android.car.notification.CarUxRestrictionManagerWrapper;
+import com.android.car.notification.NotificationClickHandlerFactory;
+import com.android.car.notification.NotificationDataManager;
+import com.android.car.notification.headsup.CarHeadsUpNotificationContainer;
+import com.android.internal.statusbar.IStatusBarService;
+
+import javax.inject.Singleton;
+
+import dagger.Binds;
+import dagger.Module;
+import dagger.Provides;
+
+/**
+ * Module for Car SysUI Notifications
+ */
+@Module
+public abstract class CarNotificationModule {
+    @Provides
+    @Singleton
+    static NotificationClickHandlerFactory provideNotificationClickHandlerFactory(
+            IStatusBarService barService) {
+        return new NotificationClickHandlerFactory(barService);
+    }
+
+    @Provides
+    @Singleton
+    static NotificationDataManager provideNotificationDataManager() {
+        return new NotificationDataManager();
+    }
+
+    @Provides
+    @Singleton
+    static CarUxRestrictionManagerWrapper provideCarUxRestrictionManagerWrapper() {
+        return new CarUxRestrictionManagerWrapper();
+    }
+
+    @Provides
+    @Singleton
+    static CarNotificationListener provideCarNotificationListener(Context context,
+            CarUxRestrictionManagerWrapper carUxRestrictionManagerWrapper,
+            CarHeadsUpNotificationManager carHeadsUpNotificationManager,
+            NotificationDataManager notificationDataManager) {
+        CarNotificationListener listener = new CarNotificationListener();
+        listener.registerAsSystemService(context, carUxRestrictionManagerWrapper,
+                carHeadsUpNotificationManager, notificationDataManager);
+        return listener;
+    }
+
+    @Provides
+    @Singleton
+    static CarHeadsUpNotificationManager provideCarHeadsUpNotificationManager(Context context,
+            NotificationClickHandlerFactory notificationClickHandlerFactory,
+            NotificationDataManager notificationDataManager,
+            CarHeadsUpNotificationContainer headsUpNotificationDisplay) {
+        return new CarHeadsUpNotificationManager(context, notificationClickHandlerFactory,
+                notificationDataManager, headsUpNotificationDisplay);
+    }
+
+    @Binds
+    abstract CarHeadsUpNotificationContainer bindsCarHeadsUpNotificationContainer(
+            CarHeadsUpNotificationSystemContainer carHeadsUpNotificationSystemContainer);
+}
diff --git a/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBar.java b/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBar.java
index 2045527..b63162b 100644
--- a/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBar.java
+++ b/packages/CarSystemUI/src/com/android/systemui/navigationbar/car/CarNavigationBar.java
@@ -49,7 +49,6 @@
 import com.android.systemui.statusbar.SuperStatusBarViewFactory;
 import com.android.systemui.statusbar.phone.AutoHideController;
 import com.android.systemui.statusbar.phone.BarTransitions;
-import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
 import java.io.FileDescriptor;
@@ -105,7 +104,7 @@
     public CarNavigationBar(Context context,
             CarNavigationBarController carNavigationBarController,
             WindowManager windowManager,
-            DeviceProvisionedController deviceProvisionedController,
+            CarDeviceProvisionedController deviceProvisionedController,
             CommandQueue commandQueue,
             AutoHideController autoHideController,
             ButtonSelectionStateListener buttonSelectionStateListener,
@@ -117,8 +116,7 @@
         super(context);
         mCarNavigationBarController = carNavigationBarController;
         mWindowManager = windowManager;
-        mCarDeviceProvisionedController = (CarDeviceProvisionedController)
-                deviceProvisionedController;
+        mCarDeviceProvisionedController = deviceProvisionedController;
         mCommandQueue = commandQueue;
         mAutoHideController = autoHideController;
         mButtonSelectionStateListener = buttonSelectionStateListener;
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
index 7ad3d45..de768cb 100644
--- a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
+++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBar.java
@@ -43,11 +43,9 @@
 import androidx.annotation.NonNull;
 import androidx.recyclerview.widget.RecyclerView;
 
-import com.android.car.notification.CarHeadsUpNotificationManager;
 import com.android.car.notification.CarNotificationListener;
 import com.android.car.notification.CarNotificationView;
 import com.android.car.notification.CarUxRestrictionManagerWrapper;
-import com.android.car.notification.HeadsUpEntry;
 import com.android.car.notification.NotificationClickHandlerFactory;
 import com.android.car.notification.NotificationDataManager;
 import com.android.car.notification.NotificationViewController;
@@ -133,7 +131,6 @@
 import com.android.systemui.statusbar.phone.dagger.StatusBarComponent;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
-import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.ExtensionController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.NetworkController;
@@ -185,14 +182,15 @@
     private final Lazy<PowerManagerHelper> mPowerManagerHelperLazy;
     private final ShadeController mShadeController;
     private final CarServiceProvider mCarServiceProvider;
+    private final NotificationDataManager mNotificationDataManager;
     private final CarDeviceProvisionedController mCarDeviceProvisionedController;
     private final ScreenLifecycle mScreenLifecycle;
+    private final CarNotificationListener mCarNotificationListener;
 
     private boolean mDeviceIsSetUpForUser = true;
     private boolean mIsUserSetupInProgress = false;
     private PowerManagerHelper mPowerManagerHelper;
     private FlingAnimationUtils mFlingAnimationUtils;
-    private NotificationDataManager mNotificationDataManager;
     private NotificationClickHandlerFactory mNotificationClickHandlerFactory;
 
     // The container for the notifications.
@@ -230,24 +228,8 @@
     private boolean mIsNotificationCardSwiping;
     // If notification shade is being swiped vertically to close.
     private boolean mIsSwipingVerticallyToClose;
-    // Whether heads-up notifications should be shown when shade is open.
-    private boolean mEnableHeadsUpNotificationWhenNotificationShadeOpen;
 
-    private CarUxRestrictionManagerWrapper mCarUxRestrictionManagerWrapper;
-
-    private final CarPowerStateListener mCarPowerStateListener =
-            (int state) -> {
-                // When the car powers on, clear all notifications and mute/unread states.
-                Log.d(TAG, "New car power state: " + state);
-                if (state == CarPowerStateListener.ON) {
-                    if (mNotificationClickHandlerFactory != null) {
-                        mNotificationClickHandlerFactory.clearAllNotifications();
-                    }
-                    if (mNotificationDataManager != null) {
-                        mNotificationDataManager.clearAll();
-                    }
-                }
-            };
+    private final CarUxRestrictionManagerWrapper mCarUxRestrictionManagerWrapper;
 
     public CarStatusBar(
             Context context,
@@ -288,7 +270,7 @@
             BubbleController bubbleController,
             NotificationGroupManager groupManager,
             VisualStabilityManager visualStabilityManager,
-            DeviceProvisionedController deviceProvisionedController,
+            CarDeviceProvisionedController carDeviceProvisionedController,
             NavigationBarController navigationBarController,
             Lazy<AssistManager> assistManagerLazy,
             ConfigurationController configurationController,
@@ -330,7 +312,10 @@
             CarServiceProvider carServiceProvider,
             Lazy<PowerManagerHelper> powerManagerHelperLazy,
             CarNavigationBarController carNavigationBarController,
-            FlingAnimationUtils.Builder flingAnimationUtilsBuilder) {
+            FlingAnimationUtils.Builder flingAnimationUtilsBuilder,
+            NotificationDataManager notificationDataManager,
+            CarUxRestrictionManagerWrapper carUxRestrictionManagerWrapper,
+            CarNotificationListener carNotificationListener) {
         super(
                 context,
                 notificationsController,
@@ -370,7 +355,7 @@
                 bubbleController,
                 groupManager,
                 visualStabilityManager,
-                deviceProvisionedController,
+                carDeviceProvisionedController,
                 navigationBarController,
                 assistManagerLazy,
                 configurationController,
@@ -411,14 +396,16 @@
         mUserSwitcherController = userSwitcherController;
         mScrimController = scrimController;
         mLockscreenLockIconController = lockscreenLockIconController;
-        mCarDeviceProvisionedController =
-                (CarDeviceProvisionedController) deviceProvisionedController;
+        mCarDeviceProvisionedController = carDeviceProvisionedController;
         mShadeController = shadeController;
         mCarServiceProvider = carServiceProvider;
         mPowerManagerHelperLazy = powerManagerHelperLazy;
         mCarNavigationBarController = carNavigationBarController;
         mFlingAnimationUtilsBuilder = flingAnimationUtilsBuilder;
         mScreenLifecycle = screenLifecycle;
+        mNotificationDataManager = notificationDataManager;
+        mCarUxRestrictionManagerWrapper = carUxRestrictionManagerWrapper;
+        mCarNotificationListener = carNotificationListener;
     }
 
     @Override
@@ -461,7 +448,17 @@
         mCarBatteryController.startListening();
 
         mPowerManagerHelper = mPowerManagerHelperLazy.get();
-        mPowerManagerHelper.setCarPowerStateListener(mCarPowerStateListener);
+        mPowerManagerHelper.setCarPowerStateListener(
+                state -> {
+                    // When the car powers on, clear all notifications and mute/unread states.
+                    Log.d(TAG, "New car power state: " + state);
+                    if (state == CarPowerStateListener.ON) {
+                        if (mNotificationClickHandlerFactory != null) {
+                            mNotificationClickHandlerFactory.clearAllNotifications();
+                        }
+                        mNotificationDataManager.clearAll();
+                    }
+                });
         mPowerManagerHelper.connectToCarService();
 
         mCarDeviceProvisionedController.addCallback(
@@ -612,27 +609,13 @@
                 mShadeController.animateCollapsePanels();
             }
         });
-        CarNotificationListener carNotificationListener = new CarNotificationListener();
-        mCarUxRestrictionManagerWrapper = new CarUxRestrictionManagerWrapper();
-
-        mNotificationDataManager = new NotificationDataManager();
 
         mNotificationDataManager.setOnUnseenCountUpdateListener(() -> {
-            if (mNotificationDataManager != null) {
-                onUseenCountUpdate(mNotificationDataManager.getUnseenNotificationCount());
-            }
+            onUseenCountUpdate(mNotificationDataManager.getUnseenNotificationCount());
         });
 
-        mEnableHeadsUpNotificationWhenNotificationShadeOpen = mContext.getResources().getBoolean(
-                R.bool.config_enableHeadsUpNotificationWhenNotificationShadeOpen);
-        CarHeadsUpNotificationManager carHeadsUpNotificationManager =
-                new CarSystemUIHeadsUpNotificationManager(mContext,
-                        mNotificationClickHandlerFactory, mNotificationDataManager);
         mNotificationClickHandlerFactory.setNotificationDataManager(mNotificationDataManager);
 
-        carNotificationListener.registerAsSystemService(mContext, mCarUxRestrictionManagerWrapper,
-                carHeadsUpNotificationManager, mNotificationDataManager);
-
         final View glassPane = mNotificationShadeWindowView.findViewById(R.id.glass_pane);
         mNotificationView = mNotificationShadeWindowView.findViewById(R.id.notification_view);
         mHandleBar = mNotificationShadeWindowView.findViewById(R.id.handle_bar);
@@ -735,7 +718,7 @@
                     mNotificationViewController = new NotificationViewController(
                             mNotificationView,
                             PreprocessingManager.getInstance(mContext),
-                            carNotificationListener,
+                            mCarNotificationListener,
                             mCarUxRestrictionManagerWrapper,
                             mNotificationDataManager);
                     mNotificationViewController.enable();
@@ -1221,61 +1204,4 @@
             return true;
         }
     }
-
-    /**
-     * SystemUi version of the notification manager that overrides methods such that the
-     * notifications end up in the status bar layouts instead of a standalone window.
-     */
-    private class CarSystemUIHeadsUpNotificationManager extends CarHeadsUpNotificationManager {
-
-        CarSystemUIHeadsUpNotificationManager(Context context,
-                NotificationClickHandlerFactory clickHandlerFactory,
-                NotificationDataManager notificationDataManager) {
-            super(context, clickHandlerFactory, notificationDataManager);
-        }
-
-        @Override
-        protected View createHeadsUpPanel() {
-            // In SystemUi the view is already in the window so just return a reference.
-            return mNotificationShadeWindowView.findViewById(R.id.notification_headsup);
-        }
-
-        @Override
-        protected void addHeadsUpPanelToDisplay() {
-            // Set the panel initial state to invisible
-            mHeadsUpPanel.setVisibility(View.INVISIBLE);
-        }
-
-        @Override
-        protected void setInternalInsetsInfo(ViewTreeObserver.InternalInsetsInfo info,
-                HeadsUpEntry currentNotification, boolean panelExpanded) {
-            super.setInternalInsetsInfo(info, currentNotification, mPanelExpanded);
-        }
-
-        @Override
-        protected void setHeadsUpVisible() {
-            // if the Notifications panel is showing or SUW for user is in progress then don't show
-            // heads up notifications
-            if ((!mEnableHeadsUpNotificationWhenNotificationShadeOpen && mPanelExpanded)
-                    || !isDeviceSetupForUser()) {
-                return;
-            }
-
-            super.setHeadsUpVisible();
-            if (mHeadsUpPanel.getVisibility() == View.VISIBLE) {
-                mNotificationShadeWindowController.setHeadsUpShowing(true);
-                mStatusBarWindowController.setForceStatusBarVisible(true);
-            }
-        }
-
-        @Override
-        protected void removeNotificationFromPanel(HeadsUpEntry currentHeadsUpNotification) {
-            super.removeNotificationFromPanel(currentHeadsUpNotification);
-            // If the panel ended up empty and hidden we can remove it from SystemUi
-            if (mHeadsUpPanel.getVisibility() != View.VISIBLE) {
-                mNotificationShadeWindowController.setHeadsUpShowing(false);
-                mStatusBarWindowController.setForceStatusBarVisible(false);
-            }
-        }
-    }
 }
diff --git a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBarModule.java b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBarModule.java
index 9798ee7..9a53584 100644
--- a/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBarModule.java
+++ b/packages/CarSystemUI/src/com/android/systemui/statusbar/car/CarStatusBarModule.java
@@ -23,6 +23,9 @@
 import android.os.PowerManager;
 import android.util.DisplayMetrics;
 
+import com.android.car.notification.CarNotificationListener;
+import com.android.car.notification.CarUxRestrictionManagerWrapper;
+import com.android.car.notification.NotificationDataManager;
 import com.android.internal.logging.MetricsLogger;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.ViewMediatorCallback;
@@ -30,6 +33,7 @@
 import com.android.systemui.assist.AssistManager;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.bubbles.BubbleController;
+import com.android.systemui.car.CarDeviceProvisionedController;
 import com.android.systemui.car.CarServiceProvider;
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dagger.qualifiers.UiBackground;
@@ -92,7 +96,6 @@
 import com.android.systemui.statusbar.phone.dagger.StatusBarPhoneDependenciesModule;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
-import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.ExtensionController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.NetworkController;
@@ -162,7 +165,7 @@
             BubbleController bubbleController,
             NotificationGroupManager groupManager,
             VisualStabilityManager visualStabilityManager,
-            DeviceProvisionedController deviceProvisionedController,
+            CarDeviceProvisionedController carDeviceProvisionedController,
             NavigationBarController navigationBarController,
             Lazy<AssistManager> assistManagerLazy,
             ConfigurationController configurationController,
@@ -203,7 +206,10 @@
             CarServiceProvider carServiceProvider,
             Lazy<PowerManagerHelper> powerManagerHelperLazy,
             CarNavigationBarController carNavigationBarController,
-            FlingAnimationUtils.Builder flingAnimationUtilsBuilder) {
+            FlingAnimationUtils.Builder flingAnimationUtilsBuilder,
+            NotificationDataManager notificationDataManager,
+            CarUxRestrictionManagerWrapper carUxRestrictionManagerWrapper,
+            CarNotificationListener carNotificationListener) {
         return new CarStatusBar(
                 context,
                 notificationsController,
@@ -243,7 +249,7 @@
                 bubbleController,
                 groupManager,
                 visualStabilityManager,
-                deviceProvisionedController,
+                carDeviceProvisionedController,
                 navigationBarController,
                 assistManagerLazy,
                 configurationController,
@@ -283,6 +289,9 @@
                 carServiceProvider,
                 powerManagerHelperLazy,
                 carNavigationBarController,
-                flingAnimationUtilsBuilder);
+                flingAnimationUtilsBuilder,
+                notificationDataManager,
+                carUxRestrictionManagerWrapper,
+                carNotificationListener);
     }
 }
diff --git a/packages/CarSystemUI/tests/src/com/android/systemui/car/notification/CarHeadsUpNotificationSystemContainerTest.java b/packages/CarSystemUI/tests/src/com/android/systemui/car/notification/CarHeadsUpNotificationSystemContainerTest.java
new file mode 100644
index 0000000..05b8e6a
--- /dev/null
+++ b/packages/CarSystemUI/tests/src/com/android/systemui/car/notification/CarHeadsUpNotificationSystemContainerTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.testing.TestableResources;
+import android.view.View;
+import android.view.WindowManager;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.car.CarDeviceProvisionedController;
+import com.android.systemui.statusbar.car.CarStatusBar;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+@SmallTest
+public class CarHeadsUpNotificationSystemContainerTest extends SysuiTestCase {
+    private CarHeadsUpNotificationSystemContainer mDefaultController;
+    private CarHeadsUpNotificationSystemContainer mOverrideEnabledController;
+    @Mock
+    private CarDeviceProvisionedController mCarDeviceProvisionedController;
+    @Mock
+    private CarStatusBar mCarStatusBar;
+    @Mock
+    private WindowManager mWindowManager;
+
+    @Mock
+    private View mNotificationView;
+    @Mock
+    private View mNotificationView2;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        when(mCarStatusBar.isPanelExpanded()).thenReturn(false);
+        when(mCarDeviceProvisionedController.isCurrentUserSetup()).thenReturn(true);
+        when(mCarDeviceProvisionedController.isCurrentUserSetupInProgress()).thenReturn(false);
+
+        TestableResources testableResources = mContext.getOrCreateTestableResources();
+
+        testableResources.addOverride(
+                R.bool.config_enableHeadsUpNotificationWhenNotificationShadeOpen, false);
+
+        mDefaultController = new CarHeadsUpNotificationSystemContainer(mContext,
+                testableResources.getResources(), mCarDeviceProvisionedController, mWindowManager,
+                () -> mCarStatusBar);
+
+        testableResources.addOverride(
+                R.bool.config_enableHeadsUpNotificationWhenNotificationShadeOpen, true);
+
+        mOverrideEnabledController = new CarHeadsUpNotificationSystemContainer(mContext,
+                testableResources.getResources(), mCarDeviceProvisionedController, mWindowManager,
+                () -> mCarStatusBar);
+    }
+
+    @Test
+    public void testDisplayNotification_firstNotification_isVisible() {
+        mDefaultController.displayNotification(mNotificationView);
+        assertThat(mDefaultController.isVisible()).isTrue();
+    }
+
+    @Test
+    public void testRemoveNotification_lastNotification_isInvisible() {
+        mDefaultController.displayNotification(mNotificationView);
+        mDefaultController.removeNotification(mNotificationView);
+        assertThat(mDefaultController.isVisible()).isFalse();
+    }
+
+    @Test
+    public void testRemoveNotification_nonLastNotification_isVisible() {
+        mDefaultController.displayNotification(mNotificationView);
+        mDefaultController.displayNotification(mNotificationView2);
+        mDefaultController.removeNotification(mNotificationView);
+        assertThat(mDefaultController.isVisible()).isTrue();
+    }
+
+    @Test
+    public void testDisplayNotification_userSetupInProgress_isInvisible() {
+        when(mCarDeviceProvisionedController.isCurrentUserSetupInProgress()).thenReturn(true);
+        mDefaultController.displayNotification(mNotificationView);
+        assertThat(mDefaultController.isVisible()).isFalse();
+
+    }
+
+    @Test
+    public void testDisplayNotification_userSetupIncomplete_isInvisible() {
+        when(mCarDeviceProvisionedController.isCurrentUserSetup()).thenReturn(false);
+        mDefaultController.displayNotification(mNotificationView);
+        assertThat(mDefaultController.isVisible()).isFalse();
+    }
+
+    @Test
+    public void testDisplayNotification_notificationPanelExpanded_isInvisible() {
+        when(mCarStatusBar.isPanelExpanded()).thenReturn(true);
+        mDefaultController.displayNotification(mNotificationView);
+        assertThat(mDefaultController.isVisible()).isFalse();
+    }
+
+    @Test
+    public void testDisplayNotification_notificationPanelExpandedEnabledHUNWhenOpen_isVisible() {
+        when(mCarStatusBar.isPanelExpanded()).thenReturn(true);
+        mOverrideEnabledController.displayNotification(mNotificationView);
+        assertThat(mOverrideEnabledController.isVisible()).isTrue();
+    }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiEntryPreference.java b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiEntryPreference.java
index 4ebb102..6a7000e 100644
--- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiEntryPreference.java
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiEntryPreference.java
@@ -43,10 +43,6 @@
             R.attr.state_encrypted
     };
 
-    private static final int[] STATE_METERED = {
-            R.attr.state_metered
-    };
-
     private static final int[] FRICTION_ATTRS = {
             R.attr.wifi_friction
     };
@@ -201,8 +197,6 @@
         if ((mWifiEntry.getSecurity() != WifiEntry.SECURITY_NONE)
                 && (mWifiEntry.getSecurity() != WifiEntry.SECURITY_OWE)) {
             mFrictionSld.setState(STATE_SECURED);
-        } else if (mWifiEntry.isMetered()) {
-            mFrictionSld.setState(STATE_METERED);
         }
         frictionImageView.setImageDrawable(mFrictionSld.getCurrent());
     }
diff --git a/packages/SystemUI/res/drawable/dismiss_circle_background.xml b/packages/SystemUI/res/drawable/dismiss_circle_background.xml
new file mode 100644
index 0000000..e311c52
--- /dev/null
+++ b/packages/SystemUI/res/drawable/dismiss_circle_background.xml
@@ -0,0 +1,28 @@
+<?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.
+  -->
+
+<shape
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval">
+
+    <stroke
+        android:width="1dp"
+        android:color="#66FFFFFF" />
+
+    <solid android:color="#B3000000" />
+
+</shape>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/dismiss_target_x.xml b/packages/SystemUI/res/drawable/dismiss_target_x.xml
new file mode 100644
index 0000000..3672eff
--- /dev/null
+++ b/packages/SystemUI/res/drawable/dismiss_target_x.xml
@@ -0,0 +1,28 @@
+<?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.
+  -->
+
+<!-- 'X' icon. -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24.0dp"
+        android:height="24.0dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:pathData="M19.000000,6.400000l-1.400000,-1.400000 -5.600000,5.600000 -5.600000,-5.600000 -1.400000,1.400000 5.600000,5.600000 -5.600000,5.600000 1.400000,1.400000 5.600000,-5.600000 5.600000,5.600000 1.400000,-1.400000 -5.600000,-5.600000z"
+        android:fillColor="#FFFFFFFF"
+        android:strokeColor="#FF000000"/>
+</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout-land-television/volume_dialog_row.xml b/packages/SystemUI/res/layout-land-television/volume_dialog_row.xml
new file mode 100644
index 0000000..08209ab
--- /dev/null
+++ b/packages/SystemUI/res/layout-land-television/volume_dialog_row.xml
@@ -0,0 +1,69 @@
+<!--
+     Copyright (C) 2015 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:tag="row"
+    android:layout_width="@dimen/volume_dialog_row_width"
+    android:layout_height="wrap_content"
+    android:background="@android:color/transparent"
+    android:clipChildren="false"
+    android:clipToPadding="false"
+    android:theme="@style/qs_theme">
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:background="@android:color/transparent"
+        android:gravity="center"
+        android:layout_gravity="center"
+        android:orientation="horizontal" >
+        <com.android.keyguard.AlphaOptimizedImageButton
+            android:id="@+id/volume_row_icon"
+            style="@style/VolumeButtons"
+            android:layout_width="@dimen/volume_dialog_tap_target_size"
+            android:layout_height="@dimen/volume_dialog_tap_target_size"
+            android:background="@drawable/ripple_drawable_20dp"
+            android:tint="@color/accent_tint_color_selector"
+            android:soundEffectsEnabled="false" />
+        <TextView
+            android:id="@+id/volume_row_header"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:ellipsize="end"
+            android:maxLength="10"
+            android:maxLines="1"
+            android:visibility="gone"
+            android:textColor="?android:attr/colorControlNormal"
+            android:textAppearance="@style/TextAppearance.Volume.Header" />
+        <FrameLayout
+            android:id="@+id/volume_row_slider_frame"
+            android:layout_height="match_parent"
+            android:layoutDirection="ltr"
+            android:layout_width="@dimen/volume_dialog_row_width">
+            <SeekBar
+                android:id="@+id/volume_row_slider"
+                android:clickable="false"
+                android:layout_width="@dimen/volume_dialog_row_width"
+                android:layout_height="match_parent"
+                android:layoutDirection="ltr"
+                android:layout_gravity="center"
+                android:rotation="0" />
+        </FrameLayout>
+    </LinearLayout>
+
+    <include layout="@layout/volume_dnd_icon"/>
+
+</FrameLayout>
diff --git a/packages/SystemUI/res/layout/qs_media_panel.xml b/packages/SystemUI/res/layout/qs_media_panel.xml
index 22303dc..34bd703 100644
--- a/packages/SystemUI/res/layout/qs_media_panel.xml
+++ b/packages/SystemUI/res/layout/qs_media_panel.xml
@@ -32,7 +32,6 @@
         android:orientation="horizontal"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:id="@+id/header"
         android:layout_marginBottom="16dp"
     >
 
@@ -73,7 +72,7 @@
 
             <!-- Song name -->
             <TextView
-                android:id="@+id/header_text"
+                android:id="@+id/header_title"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:singleLine="true"
diff --git a/packages/SystemUI/res/values-land-television/dimens.xml b/packages/SystemUI/res/values-land-television/dimens.xml
new file mode 100644
index 0000000..499341c
--- /dev/null
+++ b/packages/SystemUI/res/values-land-television/dimens.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<resources>
+  <!-- Width of volume bar -->
+  <dimen name="volume_dialog_row_width">252dp</dimen>
+  <dimen name="volume_dialog_tap_target_size">36dp</dimen>
+</resources>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 7aaf6f9..9437485 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -983,6 +983,9 @@
     <!-- The touchable/draggable edge size for PIP resize. -->
     <dimen name="pip_resize_edge_size">30dp</dimen>
 
+    <!-- The corner radius for PiP window. -->
+    <dimen name="pip_corner_radius">8dp</dimen>
+
     <dimen name="default_gear_space">18dp</dimen>
     <dimen name="cell_overlay_padding">18dp</dimen>
 
@@ -1183,6 +1186,9 @@
     <dimen name="bubble_dismiss_target_padding_x">40dp</dimen>
     <dimen name="bubble_dismiss_target_padding_y">20dp</dimen>
 
+    <dimen name="dismiss_circle_size">52dp</dimen>
+    <dimen name="dismiss_target_x_size">24dp</dimen>
+
     <!-- Bubbles user education views -->
     <dimen name="bubbles_manage_education_width">160dp</dimen>
     <!-- The inset from the top bound of the manage button to place the user education. -->
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/SurfaceViewRequestReceiver.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/SurfaceViewRequestReceiver.java
index 8809d83..7cfa289 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/SurfaceViewRequestReceiver.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/SurfaceViewRequestReceiver.java
@@ -62,7 +62,7 @@
                             surfaceControl.getWidth(),
                             surfaceControl.getHeight(),
                             WindowManager.LayoutParams.TYPE_APPLICATION,
-                            0,
+                            WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
                             mOpacity);
 
             mSurfaceControlViewHost.addView(view, layoutParams);
diff --git a/packages/SystemUI/src/com/android/systemui/CameraAvailabilityListener.kt b/packages/SystemUI/src/com/android/systemui/CameraAvailabilityListener.kt
index 24fa91b..284074e 100644
--- a/packages/SystemUI/src/com/android/systemui/CameraAvailabilityListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/CameraAvailabilityListener.kt
@@ -26,7 +26,7 @@
 
 import kotlin.math.roundToInt
 
-const val TAG = "CameraOpTransitionController"
+const val TAG = "CameraAvailabilityListener"
 
 /**
  * Listens for usage of the Camera and controls the ScreenDecorations transition to show extra
diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
index 86aa640..cab9f18 100644
--- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
+++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java
@@ -724,9 +724,10 @@
         private final List<Rect> mBounds = new ArrayList();
         private final Rect mBoundingRect = new Rect();
         private final Path mBoundingPath = new Path();
-        // Don't initialize these because they are cached elsewhere and may not exist
+        // Don't initialize these yet because they may never exist
         private Rect mProtectionRect;
         private Path mProtectionPath;
+        private Path mProtectionPathOrig;
         private Rect mTotalBounds = new Rect();
         // Whether or not to show the cutout protection path
         private boolean mShowProtection = false;
@@ -812,7 +813,11 @@
         }
 
         void setProtection(Path protectionPath, Rect pathBounds) {
-            mProtectionPath = protectionPath;
+            if (mProtectionPathOrig == null) {
+                mProtectionPathOrig = new Path();
+                mProtectionPath = new Path();
+            }
+            mProtectionPathOrig.set(protectionPath);
             mProtectionRect = pathBounds;
         }
 
@@ -889,7 +894,9 @@
             Matrix m = new Matrix();
             transformPhysicalToLogicalCoordinates(mInfo.rotation, dw, dh, m);
             mBoundingPath.transform(m);
-            if (mProtectionPath != null) {
+            if (mProtectionPathOrig != null) {
+                // Reset the protection path so we don't aggregate rotations
+                mProtectionPath.set(mProtectionPathOrig);
                 mProtectionPath.transform(m);
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index 0cf6d89..8cc10d9 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -32,7 +32,6 @@
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
-import android.annotation.NonNull;
 import android.app.Notification;
 import android.content.Context;
 import android.content.res.Configuration;
@@ -47,7 +46,6 @@
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.os.Bundle;
-import android.os.VibrationEffect;
 import android.os.Vibrator;
 import android.util.Log;
 import android.view.Choreographer;
@@ -56,6 +54,7 @@
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
 import android.view.View;
+import android.view.ViewGroup;
 import android.view.ViewTreeObserver;
 import android.view.WindowInsets;
 import android.view.WindowManager;
@@ -66,6 +65,7 @@
 import android.widget.TextView;
 
 import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.dynamicanimation.animation.DynamicAnimation;
 import androidx.dynamicanimation.animation.FloatPropertyCompat;
@@ -81,7 +81,10 @@
 import com.android.systemui.bubbles.animation.PhysicsAnimationLayout;
 import com.android.systemui.bubbles.animation.StackAnimationController;
 import com.android.systemui.shared.system.SysUiStatsLog;
+import com.android.systemui.util.DismissCircleView;
 import com.android.systemui.util.FloatingContentCoordinator;
+import com.android.systemui.util.animation.PhysicsAnimator;
+import com.android.systemui.util.magnetictarget.MagnetizedObject;
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
@@ -227,8 +230,6 @@
         pw.print("  gestureInProgress:    "); pw.println(mIsGestureInProgress);
         pw.print("  showingDismiss:       "); pw.println(mShowingDismiss);
         pw.print("  isExpansionAnimating: "); pw.println(mIsExpansionAnimating);
-        pw.print("  draggingInDismiss:    "); pw.println(mDraggingInDismissTarget);
-        pw.print("  animatingMagnet:      "); pw.println(mAnimatingMagnet);
         mStackAnimationController.dump(fd, pw, args);
         mExpandedAnimationController.dump(fd, pw, args);
     }
@@ -240,16 +241,6 @@
     private boolean mIsExpansionAnimating = false;
     private boolean mShowingDismiss = false;
 
-    /**
-     * Whether the user is currently dragging their finger within the dismiss target. In this state
-     * the stack will be magnetized to the center of the target, so we shouldn't move it until the
-     * touch exits the dismiss target area.
-     */
-    private boolean mDraggingInDismissTarget = false;
-
-    /** Whether the stack is magneting towards the dismiss target. */
-    private boolean mAnimatingMagnet = false;
-
     /** The view to desaturate/darken when magneted to the dismiss target. */
     private View mDesaturateAndDarkenTargetView;
 
@@ -331,8 +322,100 @@
     @NonNull
     private final SurfaceSynchronizer mSurfaceSynchronizer;
 
-    private BubbleDismissView mDismissContainer;
-    private Runnable mAfterMagnet;
+    /**
+     * The currently magnetized object, which is being dragged and will be attracted to the magnetic
+     * dismiss target.
+     *
+     * This is either the stack itself, or an individual bubble.
+     */
+    private MagnetizedObject<?> mMagnetizedObject;
+
+    /**
+     * The action to run when the magnetized object is released in the dismiss target.
+     *
+     * This will actually perform the dismissal of either the stack or an individual bubble.
+     */
+    private Runnable mReleasedInDismissTargetAction;
+
+    /**
+     * The MagneticTarget instance for our circular dismiss view. This is added to the
+     * MagnetizedObject instances for the stack and any dragged-out bubbles.
+     */
+    private MagnetizedObject.MagneticTarget mMagneticTarget;
+
+    /** Magnet listener that handles animating and dismissing individual dragged-out bubbles. */
+    private final MagnetizedObject.MagnetListener mIndividualBubbleMagnetListener =
+            new MagnetizedObject.MagnetListener() {
+                @Override
+                public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) {
+                    animateDesaturateAndDarken(
+                            mExpandedAnimationController.getDraggedOutBubble(), true);
+                }
+
+                @Override
+                public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
+                        float velX, float velY, boolean wasFlungOut) {
+                    animateDesaturateAndDarken(
+                            mExpandedAnimationController.getDraggedOutBubble(), false);
+
+                    if (wasFlungOut) {
+                        mExpandedAnimationController.snapBubbleBack(
+                                mExpandedAnimationController.getDraggedOutBubble(), velX, velY);
+                        hideDismissTarget();
+                    } else {
+                        mExpandedAnimationController.onUnstuckFromTarget();
+                    }
+                }
+
+                @Override
+                public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
+                    mExpandedAnimationController.dismissDraggedOutBubble(
+                            mExpandedAnimationController.getDraggedOutBubble(),
+                            mReleasedInDismissTargetAction);
+                    hideDismissTarget();
+                }
+            };
+
+    /** Magnet listener that handles animating and dismissing the entire stack. */
+    private final MagnetizedObject.MagnetListener mStackMagnetListener =
+            new MagnetizedObject.MagnetListener() {
+                @Override
+                public void onStuckToTarget(
+                        @NonNull MagnetizedObject.MagneticTarget target) {
+                    animateDesaturateAndDarken(mBubbleContainer, true);
+                }
+
+                @Override
+                public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target,
+                        float velX, float velY, boolean wasFlungOut) {
+                    animateDesaturateAndDarken(mBubbleContainer, false);
+
+                    if (wasFlungOut) {
+                        mStackAnimationController.flingStackThenSpringToEdge(
+                                mStackAnimationController.getStackPosition().x, velX, velY);
+                        hideDismissTarget();
+                    } else {
+                        mStackAnimationController.onUnstuckFromTarget();
+                    }
+                }
+
+                @Override
+                public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) {
+                    mStackAnimationController.implodeStack(
+                            () -> {
+                                resetDesaturationAndDarken();
+                                mReleasedInDismissTargetAction.run();
+                            }
+                    );
+
+                    hideDismissTarget();
+                }
+            };
+
+    private ViewGroup mDismissTargetContainer;
+    private PhysicsAnimator<View> mDismissTargetAnimator;
+    private PhysicsAnimator.SpringConfig mDismissTargetSpring = new PhysicsAnimator.SpringConfig(
+            SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_LOW_BOUNCY);
 
     private int mOrientation = Configuration.ORIENTATION_UNDEFINED;
 
@@ -409,12 +492,31 @@
                 .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY));
         mFlyoutTransitionSpring.addEndListener(mAfterFlyoutTransitionSpring);
 
-        mDismissContainer = new BubbleDismissView(mContext);
-        mDismissContainer.setLayoutParams(new FrameLayout.LayoutParams(
+        final int targetSize = res.getDimensionPixelSize(R.dimen.dismiss_circle_size);
+        final View targetView = new DismissCircleView(context);
+        final FrameLayout.LayoutParams newParams =
+                new FrameLayout.LayoutParams(targetSize, targetSize);
+        newParams.gravity = Gravity.CENTER;
+        targetView.setLayoutParams(newParams);
+        mDismissTargetAnimator = PhysicsAnimator.getInstance(targetView);
+
+        mDismissTargetContainer = new FrameLayout(context);
+        mDismissTargetContainer.setLayoutParams(new FrameLayout.LayoutParams(
                 MATCH_PARENT,
                 getResources().getDimensionPixelSize(R.dimen.pip_dismiss_gradient_height),
                 Gravity.BOTTOM));
-        addView(mDismissContainer);
+        mDismissTargetContainer.setClipChildren(false);
+        mDismissTargetContainer.addView(targetView);
+        mDismissTargetContainer.setVisibility(View.INVISIBLE);
+        addView(mDismissTargetContainer);
+
+        // Start translated down so the target springs up.
+        targetView.setTranslationY(
+                getResources().getDimensionPixelSize(R.dimen.pip_dismiss_gradient_height));
+
+        // Save the MagneticTarget instance for the newly set up view - we'll add this to the
+        // MagnetizedObjects.
+        mMagneticTarget = new MagnetizedObject.MagneticTarget(targetView, mBubbleSize * 2);
 
         mExpandedViewXAnim =
                 new SpringAnimation(mExpandedViewContainer, DynamicAnimation.TRANSLATION_X);
@@ -1066,6 +1168,14 @@
         }
     }
 
+    /*
+     * Sets the action to run to dismiss the currently dragging object (either the stack or an
+     * individual bubble).
+     */
+    public void setReleasedInDismissTargetAction(Runnable action) {
+        mReleasedInDismissTargetAction = action;
+    }
+
     /**
      * Dismiss the stack of bubbles.
      *
@@ -1262,7 +1372,12 @@
             Log.d(TAG, "onBubbleDragStart: bubble=" + bubble);
         }
         maybeShowManageEducation(false);
-        mExpandedAnimationController.prepareForBubbleDrag(bubble);
+        mExpandedAnimationController.prepareForBubbleDrag(bubble, mMagneticTarget);
+
+        // We're dragging an individual bubble, so set the magnetized object to the magnetized
+        // bubble.
+        mMagnetizedObject = mExpandedAnimationController.getMagnetizedBubbleDraggingOut();
+        mMagnetizedObject.setMagnetListener(mIndividualBubbleMagnetListener);
     }
 
     /** Called with the coordinates to which an individual bubble has been dragged. */
@@ -1304,7 +1419,9 @@
         mBubbleContainer.setActiveController(mStackAnimationController);
         hideFlyoutImmediate();
 
-        mDraggingInDismissTarget = false;
+        // Since we're dragging the stack, set the magnetized object to the magnetized stack.
+        mMagnetizedObject = mStackAnimationController.getMagnetizedStack(mMagneticTarget);
+        mMagnetizedObject.setMagnetListener(mStackMagnetListener);
     }
 
     void onDragged(float x, float y) {
@@ -1425,6 +1542,11 @@
         }
     }
 
+    /** Passes the MotionEvent to the magnetized object and returns true if it was consumed. */
+    boolean passEventToMagnetizedObject(MotionEvent event) {
+        return mMagnetizedObject != null && mMagnetizedObject.maybeConsumeMotionEvent(event);
+    }
+
     /** Prepares and starts the desaturate/darken animation on the bubble stack. */
     private void animateDesaturateAndDarken(View targetView, boolean desaturateAndDarken) {
         mDesaturateAndDarkenTargetView = targetView;
@@ -1455,102 +1577,6 @@
         mDesaturateAndDarkenTargetView.setLayerType(View.LAYER_TYPE_NONE, null);
     }
 
-    /**
-     * Magnets the stack to the target, while also transforming the target to encircle the stack and
-     * desaturating/darkening the bubbles.
-     */
-    void animateMagnetToDismissTarget(
-            View magnetView, boolean toTarget, float x, float y, float velX, float velY) {
-        mDraggingInDismissTarget = toTarget;
-
-        if (toTarget) {
-            // The Y-value for the bubble stack to be positioned in the center of the dismiss target
-            final float destY = mDismissContainer.getDismissTargetCenterY() - mBubbleSize / 2f;
-
-            mAnimatingMagnet = true;
-
-            final Runnable afterMagnet = () -> {
-                mAnimatingMagnet = false;
-                if (mAfterMagnet != null) {
-                    mAfterMagnet.run();
-                }
-            };
-
-            if (magnetView == this) {
-                mStackAnimationController.magnetToDismiss(velX, velY, destY, afterMagnet);
-                animateDesaturateAndDarken(mBubbleContainer, true);
-            } else {
-                mExpandedAnimationController.magnetBubbleToDismiss(
-                        magnetView, velX, velY, destY, afterMagnet);
-
-                animateDesaturateAndDarken(magnetView, true);
-            }
-        } else {
-            mAnimatingMagnet = false;
-
-            if (magnetView == this) {
-                mStackAnimationController.demagnetizeFromDismissToPoint(x, y, velX, velY);
-                animateDesaturateAndDarken(mBubbleContainer, false);
-            } else {
-                mExpandedAnimationController.demagnetizeBubbleTo(x, y, velX, velY);
-                animateDesaturateAndDarken(magnetView, false);
-            }
-        }
-
-        mVibrator.vibrate(VibrationEffect.get(toTarget
-                ? VibrationEffect.EFFECT_CLICK
-                : VibrationEffect.EFFECT_TICK));
-    }
-
-    /**
-     * Magnets the stack to the dismiss target if it's not already there. Then, dismiss the stack
-     * using the 'implode' animation and animate out the target.
-     */
-    void magnetToStackIfNeededThenAnimateDismissal(
-            View touchedView, float velX, float velY, Runnable after) {
-        final View draggedOutBubble = mExpandedAnimationController.getDraggedOutBubble();
-        final Runnable animateDismissal = () -> {
-            mAfterMagnet = null;
-
-            mVibrator.vibrate(VibrationEffect.get(VibrationEffect.EFFECT_CLICK));
-            mDismissContainer.springOut();
-
-            // 'Implode' the stack and then hide the dismiss target.
-            if (touchedView == this) {
-                mStackAnimationController.implodeStack(
-                        () -> {
-                            mAnimatingMagnet = false;
-                            mShowingDismiss = false;
-                            mDraggingInDismissTarget = false;
-                            after.run();
-                            resetDesaturationAndDarken();
-                        });
-            } else {
-                mExpandedAnimationController.dismissDraggedOutBubble(draggedOutBubble, () -> {
-                    mAnimatingMagnet = false;
-                    mShowingDismiss = false;
-                    mDraggingInDismissTarget = false;
-                    resetDesaturationAndDarken();
-                    after.run();
-                });
-            }
-        };
-
-        if (mAnimatingMagnet) {
-            // If the magnet animation is currently playing, dismiss the stack after it's done. This
-            // happens if the stack is flung towards the target.
-            mAfterMagnet = animateDismissal;
-        } else if (mDraggingInDismissTarget) {
-            // If we're in the dismiss target, but not animating, we already magneted - dismiss
-            // immediately.
-            animateDismissal.run();
-        } else {
-            // Otherwise, we need to start the magnet animation and then dismiss afterward.
-            animateMagnetToDismissTarget(touchedView, true, -1 /* x */, -1 /* y */, velX, velY);
-            mAfterMagnet = animateDismissal;
-        }
-    }
-
     /** Animates in the dismiss target. */
     private void springInDismissTarget() {
         if (mShowingDismiss) {
@@ -1559,10 +1585,14 @@
 
         mShowingDismiss = true;
 
-        // Show the dismiss container and bring it to the front so the bubbles will go behind it.
-        mDismissContainer.springIn();
-        mDismissContainer.bringToFront();
-        mDismissContainer.setZ(Short.MAX_VALUE - 1);
+        mDismissTargetContainer.bringToFront();
+        mDismissTargetContainer.setZ(Short.MAX_VALUE - 1);
+        mDismissTargetContainer.setVisibility(VISIBLE);
+
+        mDismissTargetAnimator.cancel();
+        mDismissTargetAnimator
+                .spring(DynamicAnimation.TRANSLATION_Y, 0f, mDismissTargetSpring)
+                .start();
     }
 
     /**
@@ -1574,13 +1604,13 @@
             return;
         }
 
-        mDismissContainer.springOut();
         mShowingDismiss = false;
-    }
 
-    /** Whether the location of the given MotionEvent is within the dismiss target area. */
-    boolean isInDismissTarget(MotionEvent ev) {
-        return isIntersecting(mDismissContainer.getDismissTarget(), ev.getRawX(), ev.getRawY());
+        mDismissTargetAnimator
+                .spring(DynamicAnimation.TRANSLATION_Y, mDismissTargetContainer.getHeight(),
+                        mDismissTargetSpring)
+                .withEndActions(() -> mDismissTargetContainer.setVisibility(View.INVISIBLE))
+                .start();
     }
 
     /** Animates the flyout collapsed (to dot), or the reverse, starting with the given velocity. */
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
index 46d1e0d..0c5bef4 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
@@ -30,28 +30,6 @@
  * dismissing, and flings.
  */
 class BubbleTouchHandler implements View.OnTouchListener {
-    /** Velocity required to dismiss the stack without dragging it into the dismiss target. */
-    private static final float STACK_DISMISS_MIN_VELOCITY = 4000f;
-
-    /**
-     * Velocity required to dismiss an individual bubble without dragging it into the dismiss
-     * target.
-     *
-     * This is higher than the stack dismiss velocity since unlike the stack, a downward fling could
-     * also be an attempted gesture to return the bubble to the row of expanded bubbles, which would
-     * usually be below the dragged bubble. By increasing the required velocity, it's less likely
-     * that the user is trying to drop it back into the row vs. fling it away.
-     */
-    private static final float INDIVIDUAL_BUBBLE_DISMISS_MIN_VELOCITY = 6000f;
-
-    /**
-     * When the stack is flung towards the bottom of the screen, it'll be dismissed if it's flung
-     * towards the center of the screen (where the dismiss target is). This value is the width of
-     * the target area to be considered 'towards the target'. For example 50% means that the stack
-     * needs to be flung towards the middle 50%, and the 25% on the left and right sides won't
-     * count.
-     */
-    private static final float DISMISS_FLING_TARGET_WIDTH_PERCENT = 0.5f;
 
     private final PointF mTouchDown = new PointF();
     private final PointF mViewPositionOnTouchDown = new PointF();
@@ -66,8 +44,6 @@
 
     /** View that was initially touched, when we received the first ACTION_DOWN event. */
     private View mTouchedView;
-    /** Whether the current touched view is in the dismiss target. */
-    private boolean mInDismissTarget;
 
     BubbleTouchHandler(BubbleStackView stackView,
             BubbleData bubbleData, Context context) {
@@ -124,13 +100,33 @@
 
                 if (isStack) {
                     mViewPositionOnTouchDown.set(mStack.getStackPosition());
+
+                    // Dismiss the entire stack if it's released in the dismiss target.
+                    mStack.setReleasedInDismissTargetAction(
+                            () -> mController.dismissStack(BubbleController.DISMISS_USER_GESTURE));
                     mStack.onDragStart();
+                    mStack.passEventToMagnetizedObject(event);
                 } else if (isFlyout) {
                     mStack.onFlyoutDragStart();
                 } else {
                     mViewPositionOnTouchDown.set(
                             mTouchedView.getTranslationX(), mTouchedView.getTranslationY());
+
+                    // Dismiss only the dragged-out bubble if it's released in the target.
+                    final String individualBubbleKey = ((BadgedImageView) mTouchedView).getKey();
+                    mStack.setReleasedInDismissTargetAction(() -> {
+                        final Bubble bubble =
+                                mBubbleData.getBubbleWithKey(individualBubbleKey);
+                        // bubble can be null if the user is in the middle of
+                        // dismissing the bubble, but the app also sent a cancel
+                        if (bubble != null) {
+                            mController.removeBubble(bubble.getEntry(),
+                                    BubbleController.DISMISS_USER_GESTURE);
+                        }
+                    });
+
                     mStack.onBubbleDragStart(mTouchedView);
+                    mStack.passEventToMagnetizedObject(event);
                 }
 
                 break;
@@ -144,27 +140,16 @@
                 }
 
                 if (mMovedEnough) {
-                    if (isStack) {
-                        mStack.onDragged(viewX, viewY);
-                    } else if (isFlyout) {
+                    if (isFlyout) {
                         mStack.onFlyoutDragged(deltaX);
-                    } else {
-                        mStack.onBubbleDragged(mTouchedView, viewX, viewY);
-                    }
-                }
-
-                final boolean currentlyInDismissTarget = mStack.isInDismissTarget(event);
-                if (currentlyInDismissTarget != mInDismissTarget) {
-                    mInDismissTarget = currentlyInDismissTarget;
-
-                    mVelocityTracker.computeCurrentVelocity(/* maxVelocity */ 1000);
-                    final float velX = mVelocityTracker.getXVelocity();
-                    final float velY = mVelocityTracker.getYVelocity();
-
-                    // If the touch event is within the dismiss target, magnet the stack to it.
-                    if (!isFlyout) {
-                        mStack.animateMagnetToDismissTarget(
-                                mTouchedView, mInDismissTarget, viewX, viewY, velX, velY);
+                    } else if (!mStack.passEventToMagnetizedObject(event)) {
+                        // If the magnetic target doesn't consume the event, drag the stack or
+                        // bubble.
+                        if (isStack) {
+                            mStack.onDragged(viewX, viewY);
+                        } else {
+                            mStack.onBubbleDragged(mTouchedView, viewX, viewY);
+                        }
                     }
                 }
                 break;
@@ -179,42 +164,21 @@
                 final float velX = mVelocityTracker.getXVelocity();
                 final float velY = mVelocityTracker.getYVelocity();
 
-                final boolean shouldDismiss =
-                        isStack
-                                ? mInDismissTarget
-                                    || isFastFlingTowardsDismissTarget(rawX, rawY, velX, velY)
-                                : mInDismissTarget
-                                        || velY > INDIVIDUAL_BUBBLE_DISMISS_MIN_VELOCITY;
-
                 if (isFlyout && mMovedEnough) {
                     mStack.onFlyoutDragFinished(rawX - mTouchDown.x /* deltaX */, velX);
-                } else if (shouldDismiss) {
-                    final String individualBubbleKey =
-                            isStack ? null : ((BadgedImageView) mTouchedView).getKey();
-                    mStack.magnetToStackIfNeededThenAnimateDismissal(mTouchedView, velX, velY,
-                            () -> {
-                                if (isStack) {
-                                    mController.dismissStack(BubbleController.DISMISS_USER_GESTURE);
-                                } else {
-                                    final Bubble bubble =
-                                            mBubbleData.getBubbleWithKey(individualBubbleKey);
-                                    // bubble can be null if the user is in the middle of
-                                    // dismissing the bubble, but the app also sent a cancel
-                                    if (bubble != null) {
-                                        mController.removeBubble(bubble.getEntry(),
-                                                BubbleController.DISMISS_USER_GESTURE);
-                                    }
-                                }
-                            });
                 } else if (isFlyout) {
                     if (!mBubbleData.isExpanded() && !mMovedEnough) {
                         mStack.onFlyoutTapped();
                     }
                 } else if (mMovedEnough) {
-                    if (isStack) {
-                        mStack.onDragFinish(viewX, viewY, velX, velY);
-                    } else {
-                        mStack.onBubbleDragFinish(mTouchedView, viewX, viewY, velX, velY);
+                    if (!mStack.passEventToMagnetizedObject(event)) {
+                        // If the magnetic target didn't consume the event, tell the stack to finish
+                        // the drag.
+                        if (isStack) {
+                            mStack.onDragFinish(viewX, viewY, velX, velY);
+                        } else {
+                            mStack.onBubbleDragFinish(mTouchedView, viewX, viewY, velX, velY);
+                        }
                     }
                 } else if (mTouchedView == mStack.getExpandedBubbleView()) {
                     mBubbleData.setExpanded(false);
@@ -235,45 +199,15 @@
         return true;
     }
 
-    /**
-     * Whether the given touch data represents a powerful fling towards the bottom-center of the
-     * screen (the dismiss target).
-     */
-    private boolean isFastFlingTowardsDismissTarget(
-            float rawX, float rawY, float velX, float velY) {
-        // Not a fling downward towards the target if velocity is zero or negative.
-        if (velY <= 0) {
-            return false;
-        }
-
-        float bottomOfScreenInterceptX = rawX;
-
-        // Only do math if the X velocity is non-zero, otherwise X won't change.
-        if (velX != 0) {
-            // Rise over run...
-            final float slope = velY / velX;
-            // ...y = mx + b, b = y / mx...
-            final float yIntercept = rawY - slope * rawX;
-            // ...calculate the x value when y = bottom of the screen.
-            bottomOfScreenInterceptX = (mStack.getHeight() - yIntercept) / slope;
-        }
-
-        final float dismissTargetWidth =
-                mStack.getWidth() * DISMISS_FLING_TARGET_WIDTH_PERCENT;
-        return velY > STACK_DISMISS_MIN_VELOCITY
-                && bottomOfScreenInterceptX > dismissTargetWidth / 2f
-                && bottomOfScreenInterceptX < mStack.getWidth() - dismissTargetWidth / 2f;
-    }
-
     /** Clears all touch-related state. */
     private void resetForNextGesture() {
         if (mVelocityTracker != null) {
             mVelocityTracker.recycle();
             mVelocityTracker = null;
         }
+
         mTouchedView = null;
         mMovedEnough = false;
-        mInDismissTarget = false;
 
         mStack.onGestureFinished();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java
index 607b5ef..3eaa90c 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/ExpandedAnimationController.java
@@ -25,13 +25,14 @@
 import android.view.View;
 import android.view.WindowInsets;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.dynamicanimation.animation.DynamicAnimation;
 import androidx.dynamicanimation.animation.SpringForce;
 
 import com.android.systemui.Interpolators;
 import com.android.systemui.R;
-import com.android.systemui.bubbles.BubbleExperimentConfig;
+import com.android.systemui.util.magnetictarget.MagnetizedObject;
 
 import com.google.android.collect.Sets;
 
@@ -62,6 +63,12 @@
     /** What percentage of the screen to use when centering the bubbles in landscape. */
     private static final float CENTER_BUBBLES_LANDSCAPE_PERCENT = 0.66f;
 
+    /**
+     * Velocity required to dismiss an individual bubble without dragging it into the dismiss
+     * target.
+     */
+    private static final float FLING_TO_DISMISS_MIN_VELOCITY = 6000f;
+
     /** Horizontal offset between bubbles, which we need to know to re-stack them. */
     private float mStackOffsetPx;
     /** Space between status bar and bubbles in the expanded state. */
@@ -79,9 +86,6 @@
     /** What the current screen orientation is. */
     private int mScreenOrientation;
 
-    /** Whether the dragged-out bubble is in the dismiss target. */
-    private boolean mIndividualBubbleWithinDismissTarget = false;
-
     private boolean mAnimatingExpand = false;
     private boolean mAnimatingCollapse = false;
     private @Nullable Runnable mAfterExpand;
@@ -99,6 +103,17 @@
      */
     private boolean mSpringingBubbleToTouch = false;
 
+    /**
+     * Whether to spring the bubble to the next touch event coordinates. This is used to animate the
+     * bubble out of the magnetic dismiss target to the touch location.
+     *
+     * Once it 'catches up' and the animation ends, we'll revert to moving it directly.
+     */
+    private boolean mSpringToTouchOnNextMotionEvent = false;
+
+    /** The bubble currently being dragged out of the row (to potentially be dismissed). */
+    private MagnetizedObject<View> mMagnetizedBubbleDraggingOut;
+
     private int mExpandedViewPadding;
 
     public ExpandedAnimationController(Point displaySize, int expandedViewPadding,
@@ -113,9 +128,6 @@
      */
     private boolean mBubbleDraggedOutEnough = false;
 
-    /** The bubble currently being dragged out of the row (to potentially be dismissed). */
-    private View mBubbleDraggingOut;
-
     /**
      * Animates expanding the bubbles into a row along the top of the screen.
      */
@@ -235,12 +247,46 @@
         }).startAll(after);
     }
 
+    /** Notifies the controller that the dragged-out bubble was unstuck from the magnetic target. */
+    public void onUnstuckFromTarget() {
+        mSpringToTouchOnNextMotionEvent = true;
+    }
+
     /** Prepares the given bubble to be dragged out. */
-    public void prepareForBubbleDrag(View bubble) {
+    public void prepareForBubbleDrag(View bubble, MagnetizedObject.MagneticTarget target) {
         mLayout.cancelAnimationsOnView(bubble);
 
-        mBubbleDraggingOut = bubble;
-        mBubbleDraggingOut.setTranslationZ(Short.MAX_VALUE);
+        bubble.setTranslationZ(Short.MAX_VALUE);
+        mMagnetizedBubbleDraggingOut = new MagnetizedObject<View>(
+                mLayout.getContext(), bubble,
+                DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y) {
+            @Override
+            public float getWidth(@NonNull View underlyingObject) {
+                return mBubbleSizePx;
+            }
+
+            @Override
+            public float getHeight(@NonNull View underlyingObject) {
+                return mBubbleSizePx;
+            }
+
+            @Override
+            public void getLocationOnScreen(@NonNull View underlyingObject, @NonNull int[] loc) {
+                loc[0] = (int) bubble.getTranslationX();
+                loc[1] = (int) bubble.getTranslationY();
+            }
+        };
+        mMagnetizedBubbleDraggingOut.addTarget(target);
+        mMagnetizedBubbleDraggingOut.setHapticsEnabled(true);
+        mMagnetizedBubbleDraggingOut.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY);
+    }
+
+    private void springBubbleTo(View bubble, float x, float y) {
+        animationForChild(bubble)
+                .translationX(x)
+                .translationY(y)
+                .withStiffness(SpringForce.STIFFNESS_HIGH)
+                .start();
     }
 
     /**
@@ -249,20 +295,20 @@
      * bubble is dragged back into the row.
      */
     public void dragBubbleOut(View bubbleView, float x, float y) {
-        if (mSpringingBubbleToTouch) {
+        if (mSpringToTouchOnNextMotionEvent) {
+            springBubbleTo(mMagnetizedBubbleDraggingOut.getUnderlyingObject(), x, y);
+            mSpringToTouchOnNextMotionEvent = false;
+            mSpringingBubbleToTouch = true;
+        } else if (mSpringingBubbleToTouch) {
             if (mLayout.arePropertiesAnimatingOnView(
                     bubbleView, DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y)) {
-                animationForChild(mBubbleDraggingOut)
-                        .translationX(x)
-                        .translationY(y)
-                        .withStiffness(SpringForce.STIFFNESS_HIGH)
-                        .start();
+                springBubbleTo(mMagnetizedBubbleDraggingOut.getUnderlyingObject(), x, y);
             } else {
                 mSpringingBubbleToTouch = false;
             }
         }
 
-        if (!mSpringingBubbleToTouch && !mIndividualBubbleWithinDismissTarget) {
+        if (!mSpringingBubbleToTouch && !mMagnetizedBubbleDraggingOut.getObjectStuckToTarget()) {
             bubbleView.setTranslationX(x);
             bubbleView.setTranslationY(y);
         }
@@ -277,8 +323,6 @@
 
     /** Plays a dismiss animation on the dragged out bubble. */
     public void dismissDraggedOutBubble(View bubble, Runnable after) {
-        mIndividualBubbleWithinDismissTarget = false;
-
         animationForChild(bubble)
                 .withStiffness(SpringForce.STIFFNESS_HIGH)
                 .scaleX(1.1f)
@@ -290,37 +334,14 @@
     }
 
     @Nullable public View getDraggedOutBubble() {
-        return mBubbleDraggingOut;
+        return mMagnetizedBubbleDraggingOut == null
+                ? null
+                : mMagnetizedBubbleDraggingOut.getUnderlyingObject();
     }
 
-    /** Magnets the given bubble to the dismiss target. */
-    public void magnetBubbleToDismiss(
-            View bubbleView, float velX, float velY, float destY, Runnable after) {
-        mIndividualBubbleWithinDismissTarget = true;
-        mSpringingBubbleToTouch = false;
-        animationForChild(bubbleView)
-                .withStiffness(SpringForce.STIFFNESS_MEDIUM)
-                .withDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
-                .withPositionStartVelocities(velX, velY)
-                .translationX(mLayout.getWidth() / 2f - mBubbleSizePx / 2f)
-                .translationY(destY, after)
-                .start();
-    }
-
-    /**
-     * Springs the dragged-out bubble towards the given coordinates and sets flags to have touch
-     * events update the spring's final position until it's settled.
-     */
-    public void demagnetizeBubbleTo(float x, float y, float velX, float velY) {
-        mIndividualBubbleWithinDismissTarget = false;
-        mSpringingBubbleToTouch = true;
-
-        animationForChild(mBubbleDraggingOut)
-                .translationX(x)
-                .translationY(y)
-                .withPositionStartVelocities(velX, velY)
-                .withStiffness(SpringForce.STIFFNESS_HIGH)
-                .start();
+    /** Returns the MagnetizedObject instance for the dragging-out bubble. */
+    public MagnetizedObject<View> getMagnetizedBubbleDraggingOut() {
+        return mMagnetizedBubbleDraggingOut;
     }
 
     /**
@@ -335,13 +356,14 @@
                 .withPositionStartVelocities(velX, velY)
                 .start(() -> bubbleView.setTranslationZ(0f) /* after */);
 
+        mMagnetizedBubbleDraggingOut = null;
+
         updateBubblePositions();
     }
 
     /** Resets bubble drag out gesture flags. */
     public void onGestureFinished() {
         mBubbleDraggedOutEnough = false;
-        mBubbleDraggingOut = null;
         updateBubblePositions();
     }
 
@@ -373,7 +395,6 @@
         pw.print("  isActive:          "); pw.println(isActiveController());
         pw.print("  animatingExpand:   "); pw.println(mAnimatingExpand);
         pw.print("  animatingCollapse: "); pw.println(mAnimatingCollapse);
-        pw.print("  bubbleInDismiss:   "); pw.println(mIndividualBubbleWithinDismissTarget);
         pw.print("  springingBubble:   "); pw.println(mSpringingBubbleToTouch);
     }
 
@@ -453,8 +474,8 @@
         final PhysicsAnimationLayout.PhysicsPropertyAnimator animator = animationForChild(child);
 
         // If we're removing the dragged-out bubble, that means it got dismissed.
-        if (child.equals(mBubbleDraggingOut)) {
-            mBubbleDraggingOut = null;
+        if (child.equals(getDraggedOutBubble())) {
+            mMagnetizedBubbleDraggingOut = null;
             finishRemoval.run();
         } else {
             animator.alpha(0f, finishRemoval /* endAction */)
@@ -490,7 +511,7 @@
 
             // Don't animate the dragging out bubble, or it'll jump around while being dragged. It
             // will be snapped to the correct X value after the drag (if it's not dismissed).
-            if (bubble.equals(mBubbleDraggingOut)) {
+            if (bubble.equals(getDraggedOutBubble())) {
                 return;
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
index f22c8fa..b81665c 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
@@ -16,7 +16,6 @@
 
 package com.android.systemui.bubbles.animation;
 
-import android.annotation.NonNull;
 import android.content.res.Resources;
 import android.graphics.PointF;
 import android.graphics.Rect;
@@ -25,6 +24,7 @@
 import android.view.View;
 import android.view.WindowInsets;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.dynamicanimation.animation.DynamicAnimation;
 import androidx.dynamicanimation.animation.FlingAnimation;
@@ -35,6 +35,7 @@
 import com.android.systemui.R;
 import com.android.systemui.util.FloatingContentCoordinator;
 import com.android.systemui.util.animation.PhysicsAnimator;
+import com.android.systemui.util.magnetictarget.MagnetizedObject;
 
 import com.google.android.collect.Sets;
 
@@ -92,6 +93,9 @@
      */
     private static final float ESCAPE_VELOCITY = 750f;
 
+    /** Velocity required to dismiss the stack without dragging it into the dismiss target. */
+    private static final float FLING_TO_DISMISS_MIN_VELOCITY = 4000f;
+
     /**
      * The canonical position of the stack. This is typically the position of the first bubble, but
      * we need to keep track of it separately from the first bubble's translation in case there are
@@ -100,6 +104,12 @@
     private PointF mStackPosition = new PointF(-1, -1);
 
     /**
+     * MagnetizedObject instance for the stack, which is used by the touch handler for the magnetic
+     * dismiss target.
+     */
+    private MagnetizedObject<StackAnimationController> mMagnetizedStack;
+
+    /**
      * The area that Bubbles will occupy after all animations end. This is used to move other
      * floating content out of the way proactively.
      */
@@ -136,11 +146,6 @@
     private boolean mIsMovingFromFlinging = false;
 
     /**
-     * Whether the stack is within the dismiss target (either by being dragged, magnet'd, or flung).
-     */
-    private boolean mWithinDismissTarget = false;
-
-    /**
      * Whether the first bubble is springing towards the touch point, rather than using the default
      * behavior of moving directly to the touch point with the rest of the stack following it.
      *
@@ -154,6 +159,14 @@
      */
     private boolean mFirstBubbleSpringingToTouch = false;
 
+    /**
+     * Whether to spring the stack to the next touch event coordinates. This is used to animate the
+     * stack (including the first bubble) out of the magnetic dismiss target to the touch location.
+     * Once it 'catches up' and the animation ends, we'll revert to moving the first bubble directly
+     * and only animating the following bubbles.
+     */
+    private boolean mSpringToTouchOnNextMotionEvent = false;
+
     /** Horizontal offset of bubbles in the stack. */
     private float mStackOffset;
     /** Diameter of the bubble icon. */
@@ -273,7 +286,8 @@
      * Note that we need new SpringForce instances per animation despite identical configs because
      * SpringAnimation uses SpringForce's internal (changing) velocity while the animation runs.
      */
-    public void springStack(float destinationX, float destinationY, float stiffness) {
+    public void springStack(
+            float destinationX, float destinationY, float stiffness) {
         notifyFloatingCoordinatorStackAnimatingTo(destinationX, destinationY);
 
         springFirstBubbleWithStackFollowing(DynamicAnimation.TRANSLATION_X,
@@ -404,7 +418,7 @@
         pw.println(mRestingStackPosition != null ? mRestingStackPosition.toString() : "null");
         pw.print("  currentStackPos:      "); pw.println(mStackPosition.toString());
         pw.print("  isMovingFromFlinging: "); pw.println(mIsMovingFromFlinging);
-        pw.print("  withinDismiss:        "); pw.println(mWithinDismissTarget);
+        pw.print("  withinDismiss:        "); pw.println(isStackStuckToTarget());
         pw.print("  firstBubbleSpringing: "); pw.println(mFirstBubbleSpringingToTouch);
     }
 
@@ -580,14 +594,18 @@
 
     /** Moves the stack in response to a touch event. */
     public void moveStackFromTouch(float x, float y) {
-
-        // If we're springing to the touch point to 'catch up' after dragging out of the dismiss
-        // target, then update the stack position animations instead of moving the bubble directly.
-        if (mFirstBubbleSpringingToTouch) {
+        // Begin the spring-to-touch catch up animation if needed.
+        if (mSpringToTouchOnNextMotionEvent) {
+            springStack(x, y, DEFAULT_STIFFNESS);
+            mSpringToTouchOnNextMotionEvent = false;
+            mFirstBubbleSpringingToTouch = true;
+        } else if (mFirstBubbleSpringingToTouch) {
             final SpringAnimation springToTouchX =
-                    (SpringAnimation) mStackPositionAnimations.get(DynamicAnimation.TRANSLATION_X);
+                    (SpringAnimation) mStackPositionAnimations.get(
+                            DynamicAnimation.TRANSLATION_X);
             final SpringAnimation springToTouchY =
-                    (SpringAnimation) mStackPositionAnimations.get(DynamicAnimation.TRANSLATION_Y);
+                    (SpringAnimation) mStackPositionAnimations.get(
+                            DynamicAnimation.TRANSLATION_Y);
 
             // If either animation is still running, we haven't caught up. Update the animations.
             if (springToTouchX.isRunning() || springToTouchY.isRunning()) {
@@ -600,56 +618,14 @@
             }
         }
 
-        if (!mFirstBubbleSpringingToTouch && !mWithinDismissTarget) {
+        if (!mFirstBubbleSpringingToTouch && !isStackStuckToTarget()) {
             moveFirstBubbleWithStackFollowing(x, y);
         }
     }
 
-    /**
-     * Demagnetizes the stack, springing it towards the given point. This also sets flags so that
-     * subsequent touch events will update the final position of the demagnetization spring instead
-     * of directly moving the bubbles, until demagnetization is complete.
-     */
-    public void demagnetizeFromDismissToPoint(float x, float y, float velX, float velY) {
-        mWithinDismissTarget = false;
-        mFirstBubbleSpringingToTouch = true;
-
-        springFirstBubbleWithStackFollowing(
-                DynamicAnimation.TRANSLATION_X,
-                new SpringForce()
-                        .setDampingRatio(DEFAULT_BOUNCINESS)
-                        .setStiffness(DEFAULT_STIFFNESS),
-                velX, x);
-
-        springFirstBubbleWithStackFollowing(
-                DynamicAnimation.TRANSLATION_Y,
-                new SpringForce()
-                        .setDampingRatio(DEFAULT_BOUNCINESS)
-                        .setStiffness(DEFAULT_STIFFNESS),
-                velY, y);
-    }
-
-    /**
-     * Spring the stack towards the dismiss target, respecting existing velocity. This also sets
-     * flags so that subsequent touch events will not move the stack until it's demagnetized.
-     */
-    public void magnetToDismiss(float velX, float velY, float destY, Runnable after) {
-        mWithinDismissTarget = true;
-        mFirstBubbleSpringingToTouch = false;
-
-        springFirstBubbleWithStackFollowing(
-                DynamicAnimation.TRANSLATION_X,
-                new SpringForce()
-                        .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
-                        .setStiffness(SpringForce.STIFFNESS_MEDIUM),
-                velX, mLayout.getWidth() / 2f - mBubbleBitmapSize / 2f);
-
-        springFirstBubbleWithStackFollowing(
-                DynamicAnimation.TRANSLATION_Y,
-                new SpringForce()
-                        .setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY)
-                        .setStiffness(SpringForce.STIFFNESS_MEDIUM),
-                velY, destY, after);
+    /** Notify the controller that the stack has been unstuck from the dismiss target. */
+    public void onUnstuckFromTarget() {
+        mSpringToTouchOnNextMotionEvent = true;
     }
 
     /**
@@ -663,13 +639,7 @@
                 .alpha(0f)
                 .withDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)
                 .withStiffness(SpringForce.STIFFNESS_HIGH)
-                .start(() -> {
-                    // Run the callback and reset flags. The child translation animations might
-                    // still be running, but that's fine. Once the alpha is at 0f they're no longer
-                    // visible anyway.
-                    after.run();
-                    mWithinDismissTarget = false;
-                });
+                .start(after);
     }
 
     /**
@@ -720,7 +690,7 @@
         if (property.equals(DynamicAnimation.TRANSLATION_X)
                 || property.equals(DynamicAnimation.TRANSLATION_Y)) {
             return index + 1;
-        } else if (mWithinDismissTarget) {
+        } else if (isStackStuckToTarget()) {
             return index + 1; // Chain all animations in dismiss (scale, alpha, etc. are used).
         } else {
             return NONE;
@@ -733,7 +703,7 @@
         if (property.equals(DynamicAnimation.TRANSLATION_X)) {
             // If we're in the dismiss target, have the bubbles pile on top of each other with no
             // offset.
-            if (mWithinDismissTarget) {
+            if (isStackStuckToTarget()) {
                 return 0f;
             } else {
                 // Offset to the left if we're on the left, or the right otherwise.
@@ -755,7 +725,7 @@
     @Override
     void onChildAdded(View child, int index) {
         // Don't animate additions within the dismiss target.
-        if (mWithinDismissTarget) {
+        if (isStackStuckToTarget()) {
             return;
         }
 
@@ -784,8 +754,6 @@
         if (mLayout.getChildCount() > 0) {
             animationForChildAtIndex(0).translationX(mStackPosition.x).start();
         } else {
-            // If there's no other bubbles, and we were in the dismiss target, reset the flag.
-            mWithinDismissTarget = false;
             // When all children are removed ensure stack position is sane
             setStackPosition(mRestingStackPosition == null
                     ? getDefaultStartPosition()
@@ -831,6 +799,9 @@
         }
     }
 
+    private boolean isStackStuckToTarget() {
+        return mMagnetizedStack != null && mMagnetizedStack.getObjectStuckToTarget();
+    }
 
     /** Moves the stack, without any animation, to the starting position. */
     private void moveStackToStartPosition() {
@@ -959,6 +930,44 @@
     }
 
     /**
+     * Returns the {@link MagnetizedObject} instance for the bubble stack, with the provided
+     * {@link MagnetizedObject.MagneticTarget} added as a target.
+     */
+    public MagnetizedObject<StackAnimationController> getMagnetizedStack(
+            MagnetizedObject.MagneticTarget target) {
+        if (mMagnetizedStack == null) {
+            mMagnetizedStack = new MagnetizedObject<StackAnimationController>(
+                    mLayout.getContext(),
+                    this,
+                    new StackPositionProperty(DynamicAnimation.TRANSLATION_X),
+                    new StackPositionProperty(DynamicAnimation.TRANSLATION_Y)
+            ) {
+                @Override
+                public float getWidth(@NonNull StackAnimationController underlyingObject) {
+                    return mBubbleSize;
+                }
+
+                @Override
+                public float getHeight(@NonNull StackAnimationController underlyingObject) {
+                    return mBubbleSize;
+                }
+
+                @Override
+                public void getLocationOnScreen(@NonNull StackAnimationController underlyingObject,
+                        @NonNull int[] loc) {
+                    loc[0] = (int) mStackPosition.x;
+                    loc[1] = (int) mStackPosition.y;
+                }
+            };
+            mMagnetizedStack.addTarget(target);
+            mMagnetizedStack.setHapticsEnabled(true);
+            mMagnetizedStack.setFlingToTargetMinVelocity(FLING_TO_DISMISS_MIN_VELOCITY);
+        }
+
+        return mMagnetizedStack;
+    }
+
+    /**
      * FloatProperty that uses {@link #moveFirstBubbleWithStackFollowing} to set the first bubble's
      * translation and animate the rest of the stack with it. A DynamicAnimation can animate this
      * property directly to move the first bubble and cause the stack to 'follow' to the new
diff --git a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
index f818d19..50bd1ad 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/controller/ControlsControllerImpl.kt
@@ -125,14 +125,19 @@
 
     @VisibleForTesting
     internal val settingObserver = object : ContentObserver(null) {
-        override fun onChange(selfChange: Boolean, uri: Uri, userId: Int) {
+        override fun onChange(
+            selfChange: Boolean,
+            uris: MutableIterable<Uri>,
+            flags: Int,
+            userId: Int
+        ) {
             // Do not listen to changes in the middle of user change, those will be read by the
             // user-switch receiver.
             if (userChanging || userId != currentUserId) {
                 return
             }
             available = Settings.Secure.getIntForUser(contentResolver, CONTROLS_AVAILABLE,
-                    DEFAULT_ENABLED, currentUserId) != 0
+                DEFAULT_ENABLED, currentUserId) != 0
             resetFavorites(available)
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
index 44e5d3d..c28a719 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
@@ -261,7 +261,7 @@
 
     private final ContentObserver mSettingsObserver = new ContentObserver(mHandler) {
         @Override
-        public void onChange(boolean selfChange, Uri uri, int userId) {
+        public void onChange(boolean selfChange, Iterable<Uri> uris, int flags, int userId) {
             if (userId != ActivityManager.getCurrentUser()) {
                 return;
             }
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
new file mode 100644
index 0000000..a161d03
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
@@ -0,0 +1,425 @@
+/*
+ * 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.annotation.LayoutRes;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.ColorStateList;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.Icon;
+import android.graphics.drawable.RippleDrawable;
+import android.media.MediaMetadata;
+import android.media.session.MediaController;
+import android.media.session.MediaSession;
+import android.media.session.PlaybackState;
+import android.os.Handler;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.core.graphics.drawable.RoundedBitmapDrawable;
+import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
+
+import com.android.settingslib.media.MediaDevice;
+import com.android.settingslib.media.MediaOutputSliceConstants;
+import com.android.settingslib.widget.AdaptiveIcon;
+import com.android.systemui.Dependency;
+import com.android.systemui.R;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.statusbar.NotificationMediaManager;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Base media control panel for System UI
+ */
+public class MediaControlPanel implements NotificationMediaManager.MediaListener {
+    private static final String TAG = "MediaControlPanel";
+    private final NotificationMediaManager mMediaManager;
+    private final Executor mBackgroundExecutor;
+
+    private Context mContext;
+    protected LinearLayout mMediaNotifView;
+    private View mSeamless;
+    private MediaSession.Token mToken;
+    private MediaController mController;
+    private int mForegroundColor;
+    private int mBackgroundColor;
+    protected ComponentName mRecvComponent;
+
+    private final int[] mActionIds;
+
+    // Button IDs used in notifications
+    protected static final int[] NOTIF_ACTION_IDS = {
+            com.android.internal.R.id.action0,
+            com.android.internal.R.id.action1,
+            com.android.internal.R.id.action2,
+            com.android.internal.R.id.action3,
+            com.android.internal.R.id.action4
+    };
+
+    private MediaController.Callback mSessionCallback = new MediaController.Callback() {
+        @Override
+        public void onSessionDestroyed() {
+            Log.d(TAG, "session destroyed");
+            mController.unregisterCallback(mSessionCallback);
+            clearControls();
+        }
+    };
+
+    /**
+     * Initialize a new control panel
+     * @param context
+     * @param parent
+     * @param manager
+     * @param layoutId layout resource to use for this control panel
+     * @param actionIds resource IDs for action buttons in the layout
+     * @param backgroundExecutor background executor, used for processing artwork
+     */
+    public MediaControlPanel(Context context, ViewGroup parent, NotificationMediaManager manager,
+            @LayoutRes int layoutId, int[] actionIds, Executor backgroundExecutor) {
+        mContext = context;
+        LayoutInflater inflater = LayoutInflater.from(mContext);
+        mMediaNotifView = (LinearLayout) inflater.inflate(layoutId, parent, false);
+        mMediaManager = manager;
+        mActionIds = actionIds;
+        mBackgroundExecutor = backgroundExecutor;
+    }
+
+    /**
+     * Get the view used to display media controls
+     * @return the view
+     */
+    public View getView() {
+        return mMediaNotifView;
+    }
+
+    /**
+     * Get the context
+     * @return context
+     */
+    public Context getContext() {
+        return mContext;
+    }
+
+    /**
+     * Update the media panel view for the given media session
+     * @param token
+     * @param icon
+     * @param iconColor
+     * @param bgColor
+     * @param contentIntent
+     * @param appNameString
+     * @param device
+     */
+    public void setMediaSession(MediaSession.Token token, Icon icon, int iconColor,
+            int bgColor, PendingIntent contentIntent, String appNameString, MediaDevice device) {
+        mToken = token;
+        mForegroundColor = iconColor;
+        mBackgroundColor = bgColor;
+        mController = new MediaController(mContext, mToken);
+
+        MediaMetadata mediaMetadata = mController.getMetadata();
+
+        // Try to find a receiver for the media button that matches this app
+        PackageManager pm = mContext.getPackageManager();
+        Intent it = new Intent(Intent.ACTION_MEDIA_BUTTON);
+        List<ResolveInfo> info = pm.queryBroadcastReceiversAsUser(it, 0, mContext.getUser());
+        if (info != null) {
+            for (ResolveInfo inf : info) {
+                if (inf.activityInfo.packageName.equals(mController.getPackageName())) {
+                    mRecvComponent = inf.getComponentInfo().getComponentName();
+                }
+            }
+        }
+
+        mController.registerCallback(mSessionCallback);
+
+        if (mediaMetadata == null) {
+            Log.e(TAG, "Media metadata was null");
+            return;
+        }
+
+        ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
+        if (albumView != null) {
+            // Resize art in a background thread
+            mBackgroundExecutor.execute(() -> processAlbumArt(mediaMetadata, albumView));
+        }
+        mMediaNotifView.setBackgroundTintList(ColorStateList.valueOf(mBackgroundColor));
+
+        // Click action
+        mMediaNotifView.setOnClickListener(v -> {
+            try {
+                contentIntent.send();
+                // Also close shade
+                mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
+            } catch (PendingIntent.CanceledException e) {
+                Log.e(TAG, "Pending intent was canceled", e);
+            }
+        });
+
+        // App icon
+        ImageView appIcon = mMediaNotifView.findViewById(R.id.icon);
+        Drawable iconDrawable = icon.loadDrawable(mContext);
+        iconDrawable.setTint(mForegroundColor);
+        appIcon.setImageDrawable(iconDrawable);
+
+        // Song name
+        TextView titleText = mMediaNotifView.findViewById(R.id.header_title);
+        String songName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);
+        titleText.setText(songName);
+        titleText.setTextColor(mForegroundColor);
+
+        // Not in mini player:
+        // App title
+        TextView appName = mMediaNotifView.findViewById(R.id.app_name);
+        if (appName != null) {
+            appName.setText(appNameString);
+            appName.setTextColor(mForegroundColor);
+        }
+
+        // Artist name
+        TextView artistText = mMediaNotifView.findViewById(R.id.header_artist);
+        if (artistText != null) {
+            String artistName = mediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
+            artistText.setText(artistName);
+            artistText.setTextColor(mForegroundColor);
+        }
+
+        // Transfer chip
+        mSeamless = mMediaNotifView.findViewById(R.id.media_seamless);
+        if (mSeamless != null) {
+            mSeamless.setVisibility(View.VISIBLE);
+            updateDevice(device);
+            ActivityStarter mActivityStarter = Dependency.get(ActivityStarter.class);
+            mSeamless.setOnClickListener(v -> {
+                final Intent intent = new Intent()
+                        .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT)
+                        .putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME,
+                                mController.getPackageName())
+                        .putExtra(MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN, mToken);
+                mActivityStarter.startActivity(intent, false, true /* dismissShade */,
+                        Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+            });
+        }
+
+        // Ensure is only added once
+        mMediaManager.removeCallback(this);
+        mMediaManager.addCallback(this);
+    }
+
+    /**
+     * Return the token for the current media session
+     * @return the token
+     */
+    public MediaSession.Token getMediaSessionToken() {
+        return mToken;
+    }
+
+    /**
+     * Get the current media controller
+     * @return the controller
+     */
+    public MediaController getController() {
+        return mController;
+    }
+
+    /**
+     * Get the name of the package associated with the current media controller
+     * @return the package name
+     */
+    public String getMediaPlayerPackage() {
+        return mController.getPackageName();
+    }
+
+    /**
+     * Check whether this player has an attached media session.
+     * @return whether there is a controller with a current media session.
+     */
+    public boolean hasMediaSession() {
+        return mController != null && mController.getPlaybackState() != null;
+    }
+
+    /**
+     * Check whether the media controlled by this player is currently playing
+     * @return whether it is playing, or false if no controller information
+     */
+    public boolean isPlaying() {
+        return isPlaying(mController);
+    }
+
+    /**
+     * Check whether the given controller is currently playing
+     * @param controller media controller to check
+     * @return whether it is playing, or false if no controller information
+     */
+    protected boolean isPlaying(MediaController controller) {
+        if (controller == null) {
+            return false;
+        }
+
+        PlaybackState state = controller.getPlaybackState();
+        if (state == null) {
+            return false;
+        }
+
+        return (state.getState() == PlaybackState.STATE_PLAYING);
+    }
+
+    /**
+     * Process album art for layout
+     * @param metadata media metadata
+     * @param albumView view to hold the album art
+     */
+    private void processAlbumArt(MediaMetadata metadata, ImageView albumView) {
+        Bitmap albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
+        float radius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius);
+        RoundedBitmapDrawable roundedDrawable = null;
+        if (albumArt != null) {
+            Bitmap original = albumArt.copy(Bitmap.Config.ARGB_8888, true);
+            int albumSize = (int) mContext.getResources().getDimension(
+                    R.dimen.qs_media_album_size);
+            Bitmap scaled = Bitmap.createScaledBitmap(original, albumSize, albumSize, false);
+            roundedDrawable = RoundedBitmapDrawableFactory.create(mContext.getResources(), scaled);
+            roundedDrawable.setCornerRadius(radius);
+        } else {
+            Log.e(TAG, "No album art available");
+        }
+
+        // Now that it's resized, update the UI
+        final RoundedBitmapDrawable result = roundedDrawable;
+        albumView.getHandler().post(() -> {
+            if (result != null) {
+                albumView.setImageDrawable(result);
+                albumView.setVisibility(View.VISIBLE);
+            } else {
+                albumView.setImageDrawable(null);
+                albumView.setVisibility(View.GONE);
+            }
+        });
+    }
+
+    /**
+     * Update the current device information
+     * @param device device information to display
+     */
+    public void updateDevice(MediaDevice device) {
+        if (mSeamless == null) {
+            return;
+        }
+        Handler handler = mSeamless.getHandler();
+        handler.post(() -> {
+            updateChipInternal(device);
+        });
+    }
+
+    private void updateChipInternal(MediaDevice device) {
+        ColorStateList fgTintList = ColorStateList.valueOf(mForegroundColor);
+
+        // Update the outline color
+        LinearLayout viewLayout = (LinearLayout) mSeamless;
+        RippleDrawable bkgDrawable = (RippleDrawable) viewLayout.getBackground();
+        GradientDrawable rect = (GradientDrawable) bkgDrawable.getDrawable(0);
+        rect.setStroke(2, mForegroundColor);
+        rect.setColor(mBackgroundColor);
+
+        ImageView iconView = mSeamless.findViewById(R.id.media_seamless_image);
+        TextView deviceName = mSeamless.findViewById(R.id.media_seamless_text);
+        deviceName.setTextColor(fgTintList);
+
+        if (device != null) {
+            Drawable icon = device.getIcon();
+            iconView.setVisibility(View.VISIBLE);
+            iconView.setImageTintList(fgTintList);
+
+            if (icon instanceof AdaptiveIcon) {
+                AdaptiveIcon aIcon = (AdaptiveIcon) icon;
+                aIcon.setBackgroundColor(mBackgroundColor);
+                iconView.setImageDrawable(aIcon);
+            } else {
+                iconView.setImageDrawable(icon);
+            }
+            deviceName.setText(device.getName());
+        } else {
+            // Reset to default
+            iconView.setVisibility(View.GONE);
+            deviceName.setText(com.android.internal.R.string.ext_media_seamless_action);
+        }
+    }
+
+    /**
+     * Put controls into a resumption state
+     */
+    public void clearControls() {
+        // Hide all the old buttons
+        for (int i = 0; i < mActionIds.length; i++) {
+            ImageButton thisBtn = mMediaNotifView.findViewById(mActionIds[i]);
+            if (thisBtn != null) {
+                thisBtn.setVisibility(View.GONE);
+            }
+        }
+
+        // Add a restart button
+        ImageButton btn = mMediaNotifView.findViewById(mActionIds[0]);
+        btn.setOnClickListener(v -> {
+            Log.d(TAG, "Attempting to restart session");
+            // Send a media button event to previously found receiver
+            if (mRecvComponent != null) {
+                Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
+                intent.setComponent(mRecvComponent);
+                int keyCode = KeyEvent.KEYCODE_MEDIA_PLAY;
+                intent.putExtra(
+                        Intent.EXTRA_KEY_EVENT,
+                        new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
+                mContext.sendBroadcast(intent);
+            } else {
+                Log.d(TAG, "No receiver to restart");
+                // If we don't have a receiver, try relaunching the activity instead
+                try {
+                    mController.getSessionActivity().send();
+                } catch (PendingIntent.CanceledException e) {
+                    Log.e(TAG, "Pending intent was canceled", e);
+                }
+            }
+        });
+        btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play));
+        btn.setImageTintList(ColorStateList.valueOf(mForegroundColor));
+        btn.setVisibility(View.VISIBLE);
+    }
+
+    @Override
+    public void onMetadataOrStateChanged(MediaMetadata metadata, int state) {
+        if (state == PlaybackState.STATE_NONE) {
+            clearControls();
+            mMediaManager.removeCallback(this);
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/pip/PipAnimationController.java b/packages/SystemUI/src/com/android/systemui/pip/PipAnimationController.java
index 36b5fad..67802bc 100644
--- a/packages/SystemUI/src/com/android/systemui/pip/PipAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/pip/PipAnimationController.java
@@ -37,7 +37,6 @@
     private static final float FRACTION_START = 0f;
     private static final float FRACTION_END = 1f;
 
-    public static final int DURATION_NONE = 0;
     public static final int DURATION_DEFAULT_MS = 425;
     public static final int ANIM_TYPE_BOUNDS = 0;
     public static final int ANIM_TYPE_ALPHA = 1;
@@ -49,6 +48,20 @@
     @Retention(RetentionPolicy.SOURCE)
     public @interface AnimationType {}
 
+    static final int TRANSITION_DIRECTION_NONE = 0;
+    static final int TRANSITION_DIRECTION_SAME = 1;
+    static final int TRANSITION_DIRECTION_TO_PIP = 2;
+    static final int TRANSITION_DIRECTION_TO_FULLSCREEN = 3;
+
+    @IntDef(prefix = { "TRANSITION_DIRECTION_" }, value = {
+            TRANSITION_DIRECTION_NONE,
+            TRANSITION_DIRECTION_SAME,
+            TRANSITION_DIRECTION_TO_PIP,
+            TRANSITION_DIRECTION_TO_FULLSCREEN
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface TransitionDirection {}
+
     private final Interpolator mFastOutSlowInInterpolator;
 
     private PipTransitionAnimator mCurrentAnimator;
@@ -58,30 +71,28 @@
                 com.android.internal.R.interpolator.fast_out_slow_in);
     }
 
-    PipTransitionAnimator getAnimator(SurfaceControl leash, boolean scheduleFinishPip,
+    @SuppressWarnings("unchecked")
+    PipTransitionAnimator getAnimator(SurfaceControl leash,
             Rect destinationBounds, float alphaStart, float alphaEnd) {
         if (mCurrentAnimator == null) {
             mCurrentAnimator = setupPipTransitionAnimator(
-                    PipTransitionAnimator.ofAlpha(leash, scheduleFinishPip, destinationBounds,
-                            alphaStart, alphaEnd));
+                    PipTransitionAnimator.ofAlpha(leash, destinationBounds, alphaStart, alphaEnd));
         } else if (mCurrentAnimator.getAnimationType() == ANIM_TYPE_ALPHA
                 && mCurrentAnimator.isRunning()) {
             mCurrentAnimator.updateEndValue(alphaEnd);
         } else {
             mCurrentAnimator.cancel();
             mCurrentAnimator = setupPipTransitionAnimator(
-                    PipTransitionAnimator.ofAlpha(leash, scheduleFinishPip, destinationBounds,
-                            alphaStart, alphaEnd));
+                    PipTransitionAnimator.ofAlpha(leash, destinationBounds, alphaStart, alphaEnd));
         }
         return mCurrentAnimator;
     }
 
-    PipTransitionAnimator getAnimator(SurfaceControl leash, boolean scheduleFinishPip,
-            Rect startBounds, Rect endBounds) {
+    @SuppressWarnings("unchecked")
+    PipTransitionAnimator getAnimator(SurfaceControl leash, Rect startBounds, Rect endBounds) {
         if (mCurrentAnimator == null) {
             mCurrentAnimator = setupPipTransitionAnimator(
-                    PipTransitionAnimator.ofBounds(leash, scheduleFinishPip,
-                            startBounds, endBounds));
+                    PipTransitionAnimator.ofBounds(leash, startBounds, endBounds));
         } else if (mCurrentAnimator.getAnimationType() == ANIM_TYPE_BOUNDS
                 && mCurrentAnimator.isRunning()) {
             mCurrentAnimator.setDestinationBounds(endBounds);
@@ -90,8 +101,7 @@
         } else {
             mCurrentAnimator.cancel();
             mCurrentAnimator = setupPipTransitionAnimator(
-                    PipTransitionAnimator.ofBounds(leash, scheduleFinishPip,
-                            startBounds, endBounds));
+                    PipTransitionAnimator.ofBounds(leash, startBounds, endBounds));
         }
         return mCurrentAnimator;
     }
@@ -134,7 +144,6 @@
     public abstract static class PipTransitionAnimator<T> extends ValueAnimator implements
             ValueAnimator.AnimatorUpdateListener,
             ValueAnimator.AnimatorListener {
-        private final boolean mScheduleFinishPip;
         private final SurfaceControl mLeash;
         private final @AnimationType int mAnimationType;
         private final Rect mDestinationBounds = new Rect();
@@ -144,11 +153,11 @@
         private T mCurrentValue;
         private PipAnimationCallback mPipAnimationCallback;
         private SurfaceControlTransactionFactory mSurfaceControlTransactionFactory;
+        private @TransitionDirection int mTransitionDirection;
+        private int mCornerRadius;
 
-        private PipTransitionAnimator(SurfaceControl leash, boolean scheduleFinishPip,
-                @AnimationType int animationType, Rect destinationBounds,
-                T startValue, T endValue) {
-            mScheduleFinishPip = scheduleFinishPip;
+        private PipTransitionAnimator(SurfaceControl leash, @AnimationType int animationType,
+                Rect destinationBounds, T startValue, T endValue) {
             mLeash = leash;
             mAnimationType = animationType;
             mDestinationBounds.set(destinationBounds);
@@ -157,6 +166,7 @@
             addListener(this);
             addUpdateListener(this);
             mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new;
+            mTransitionDirection = TRANSITION_DIRECTION_NONE;
         }
 
         @Override
@@ -202,8 +212,15 @@
             return this;
         }
 
-        boolean shouldScheduleFinishPip() {
-            return mScheduleFinishPip;
+        @TransitionDirection int getTransitionDirection() {
+            return mTransitionDirection;
+        }
+
+        PipTransitionAnimator<T> setTransitionDirection(@TransitionDirection int direction) {
+            if (direction != TRANSITION_DIRECTION_SAME) {
+                mTransitionDirection = direction;
+            }
+            return this;
         }
 
         T getStartValue() {
@@ -226,6 +243,19 @@
             mCurrentValue = value;
         }
 
+        int getCornerRadius() {
+            return mCornerRadius;
+        }
+
+        PipTransitionAnimator<T> setCornerRadius(int cornerRadius) {
+            mCornerRadius = cornerRadius;
+            return this;
+        }
+
+        boolean shouldApplyCornerRadius() {
+            return mTransitionDirection != TRANSITION_DIRECTION_TO_FULLSCREEN;
+        }
+
         /**
          * Updates the {@link #mEndValue}.
          *
@@ -251,9 +281,9 @@
         abstract void applySurfaceControlTransaction(SurfaceControl leash,
                 SurfaceControl.Transaction tx, float fraction);
 
-        static PipTransitionAnimator<Float> ofAlpha(SurfaceControl leash, boolean scheduleFinishPip,
+        static PipTransitionAnimator<Float> ofAlpha(SurfaceControl leash,
                 Rect destinationBounds, float startValue, float endValue) {
-            return new PipTransitionAnimator<Float>(leash, scheduleFinishPip, ANIM_TYPE_ALPHA,
+            return new PipTransitionAnimator<Float>(leash, ANIM_TYPE_ALPHA,
                     destinationBounds, startValue, endValue) {
                 @Override
                 void applySurfaceControlTransaction(SurfaceControl leash,
@@ -266,16 +296,18 @@
                         final Rect bounds = getDestinationBounds();
                         tx.setPosition(leash, bounds.left, bounds.top)
                                 .setWindowCrop(leash, bounds.width(), bounds.height());
+                        tx.setCornerRadius(leash,
+                                shouldApplyCornerRadius() ? getCornerRadius() : 0);
                     }
                     tx.apply();
                 }
             };
         }
 
-        static PipTransitionAnimator<Rect> ofBounds(SurfaceControl leash, boolean scheduleFinishPip,
+        static PipTransitionAnimator<Rect> ofBounds(SurfaceControl leash,
                 Rect startValue, Rect endValue) {
             // construct new Rect instances in case they are recycled
-            return new PipTransitionAnimator<Rect>(leash, scheduleFinishPip, ANIM_TYPE_BOUNDS,
+            return new PipTransitionAnimator<Rect>(leash, ANIM_TYPE_BOUNDS,
                     endValue, new Rect(startValue), new Rect(endValue)) {
                 private final Rect mTmpRect = new Rect();
 
@@ -299,6 +331,8 @@
                     if (Float.compare(fraction, FRACTION_START) == 0) {
                         // Ensure the start condition
                         tx.setAlpha(leash, 1f);
+                        tx.setCornerRadius(leash,
+                                shouldApplyCornerRadius() ? getCornerRadius() : 0);
                     }
                     tx.apply();
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java b/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java
index 4766ebc..3933af0 100644
--- a/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java
+++ b/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java
@@ -19,6 +19,10 @@
 import static com.android.systemui.pip.PipAnimationController.ANIM_TYPE_ALPHA;
 import static com.android.systemui.pip.PipAnimationController.ANIM_TYPE_BOUNDS;
 import static com.android.systemui.pip.PipAnimationController.DURATION_DEFAULT_MS;
+import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_NONE;
+import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_SAME;
+import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_TO_FULLSCREEN;
+import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -30,7 +34,6 @@
 import android.graphics.Rect;
 import android.os.Handler;
 import android.os.Looper;
-import android.os.Message;
 import android.os.RemoteException;
 import android.util.Log;
 import android.view.DisplayInfo;
@@ -40,6 +43,7 @@
 import android.view.WindowContainerTransaction;
 
 import com.android.internal.os.SomeArgs;
+import com.android.systemui.R;
 import com.android.systemui.pip.phone.PipUpdateThread;
 
 import java.util.ArrayList;
@@ -74,6 +78,7 @@
     private final List<PipTransitionCallback> mPipTransitionCallbacks = new ArrayList<>();
     private final Rect mDisplayBounds = new Rect();
     private final Rect mLastReportedBounds = new Rect();
+    private final int mCornerRadius;
 
     // These callbacks are called on the update thread
     private final PipAnimationController.PipAnimationCallback mPipAnimationCallback =
@@ -97,7 +102,7 @@
                     callback.onPipTransitionFinished();
                 }
             });
-            finishResize(animator.getDestinationBounds(), tx, animator.shouldScheduleFinishPip());
+            finishResize(tx, animator.getDestinationBounds(), animator.getTransitionDirection());
         }
 
         @Override
@@ -111,57 +116,53 @@
         }
     };
 
-    private Handler.Callback mUpdateCallbacks = new Handler.Callback() {
-        @Override
-        public boolean handleMessage(Message msg) {
-            SomeArgs args = (SomeArgs) msg.obj;
-            Consumer<Rect> updateBoundsCallback = (Consumer<Rect>) args.arg1;
-            switch (msg.what) {
-                case MSG_RESIZE_IMMEDIATE: {
-                    Rect toBounds = (Rect) args.arg2;
-                    resizePip(toBounds);
-                    if (updateBoundsCallback != null) {
-                        updateBoundsCallback.accept(toBounds);
-                    }
-                    break;
+    @SuppressWarnings("unchecked")
+    private Handler.Callback mUpdateCallbacks = (msg) -> {
+        SomeArgs args = (SomeArgs) msg.obj;
+        Consumer<Rect> updateBoundsCallback = (Consumer<Rect>) args.arg1;
+        switch (msg.what) {
+            case MSG_RESIZE_IMMEDIATE: {
+                Rect toBounds = (Rect) args.arg2;
+                resizePip(toBounds);
+                if (updateBoundsCallback != null) {
+                    updateBoundsCallback.accept(toBounds);
                 }
-                case MSG_RESIZE_ANIMATE: {
-                    Rect currentBounds = (Rect) args.arg2;
-                    Rect toBounds = (Rect) args.arg3;
-                    boolean scheduleFinishPip = args.argi1 != 0;
-                    int duration = args.argi2;
-                    animateResizePip(scheduleFinishPip, currentBounds, toBounds, duration);
-                    if (updateBoundsCallback != null) {
-                        updateBoundsCallback.accept(toBounds);
-                    }
-                    break;
-                }
-                case MSG_OFFSET_ANIMATE: {
-                    Rect originalBounds = (Rect) args.arg2;
-                    final int offset = args.argi1;
-                    final int duration = args.argi2;
-                    offsetPip(originalBounds, 0 /* xOffset */, offset, duration);
-                    Rect toBounds = new Rect(originalBounds);
-                    toBounds.offset(0, offset);
-                    if (updateBoundsCallback != null) {
-                        updateBoundsCallback.accept(toBounds);
-                    }
-                    break;
-                }
-                case MSG_FINISH_RESIZE: {
-                    SurfaceControl.Transaction tx = (SurfaceControl.Transaction) args.arg2;
-                    Rect toBounds = (Rect) args.arg3;
-                    boolean scheduleFinishPip = args.argi1 != 0;
-                    finishResize(toBounds, tx, scheduleFinishPip);
-                    if (updateBoundsCallback != null) {
-                        updateBoundsCallback.accept(toBounds);
-                    }
-                    break;
-                }
+                break;
             }
-            args.recycle();
-            return true;
+            case MSG_RESIZE_ANIMATE: {
+                Rect currentBounds = (Rect) args.arg2;
+                Rect toBounds = (Rect) args.arg3;
+                int duration = args.argi2;
+                animateResizePip(currentBounds, toBounds, args.argi1 /* direction */, duration);
+                if (updateBoundsCallback != null) {
+                    updateBoundsCallback.accept(toBounds);
+                }
+                break;
+            }
+            case MSG_OFFSET_ANIMATE: {
+                Rect originalBounds = (Rect) args.arg2;
+                final int offset = args.argi1;
+                final int duration = args.argi2;
+                offsetPip(originalBounds, 0 /* xOffset */, offset, duration);
+                Rect toBounds = new Rect(originalBounds);
+                toBounds.offset(0, offset);
+                if (updateBoundsCallback != null) {
+                    updateBoundsCallback.accept(toBounds);
+                }
+                break;
+            }
+            case MSG_FINISH_RESIZE: {
+                SurfaceControl.Transaction tx = (SurfaceControl.Transaction) args.arg2;
+                Rect toBounds = (Rect) args.arg3;
+                finishResize(tx, toBounds, args.argi1 /* direction */);
+                if (updateBoundsCallback != null) {
+                    updateBoundsCallback.accept(toBounds);
+                }
+                break;
+            }
         }
+        args.recycle();
+        return true;
     };
 
     private ActivityManager.RunningTaskInfo mTaskInfo;
@@ -176,6 +177,7 @@
         mTaskOrganizerController = ActivityTaskManager.getTaskOrganizerController();
         mPipBoundsHandler = boundsHandler;
         mPipAnimationController = new PipAnimationController(context);
+        mCornerRadius = context.getResources().getDimensionPixelSize(R.dimen.pip_corner_radius);
     }
 
     public Handler getUpdateHandler() {
@@ -191,7 +193,8 @@
 
     /**
      * Sets the preferred animation type for one time.
-     * This is typically used to set the animation type to {@link #ANIM_TYPE_ALPHA}.
+     * This is typically used to set the animation type to
+     * {@link PipAnimationController#ANIM_TYPE_ALPHA}.
      */
     public void setOneShotAnimationType(@PipAnimationController.AnimationType int animationType) {
         mOneShotAnimationType = animationType;
@@ -200,13 +203,14 @@
     /**
      * Updates the display dimension with given {@link DisplayInfo}
      */
+    @SuppressWarnings("unchecked")
     public void onDisplayInfoChanged(DisplayInfo displayInfo) {
         final Rect newDisplayBounds = new Rect(0, 0,
                 displayInfo.logicalWidth, displayInfo.logicalHeight);
         if (!mDisplayBounds.equals(newDisplayBounds)) {
             // Updates the exiting PiP animation in case the screen rotation changes in the middle.
             // It's a legit case that PiP window is in portrait mode on home screen and
-            // the application requests landscape onces back to fullscreen mode.
+            // the application requests landscape once back to fullscreen mode.
             final PipAnimationController.PipTransitionAnimator animator =
                     mPipAnimationController.getCurrentAnimator();
             if (animator != null
@@ -250,12 +254,13 @@
         }
         if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) {
             final Rect currentBounds = mTaskInfo.configuration.windowConfiguration.getBounds();
-            scheduleAnimateResizePip(true /* scheduleFinishPip */,
-                    currentBounds, destinationBounds, DURATION_DEFAULT_MS, null);
+            scheduleAnimateResizePip(currentBounds, destinationBounds,
+                    TRANSITION_DIRECTION_TO_PIP, DURATION_DEFAULT_MS, null);
         } else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) {
             mUpdateHandler.post(() -> mPipAnimationController
-                    .getAnimator(mLeash, true /* scheduleFinishPip */,
-                            destinationBounds, 0f, 1f)
+                    .getAnimator(mLeash, destinationBounds, 0f, 1f)
+                    .setTransitionDirection(TRANSITION_DIRECTION_TO_PIP)
+                    .setCornerRadius(mCornerRadius)
                     .setPipAnimationCallback(mPipAnimationCallback)
                     .setDuration(DURATION_DEFAULT_MS)
                     .start());
@@ -272,7 +277,8 @@
             Log.wtf(TAG, "Unrecognized token: " + token);
             return;
         }
-        scheduleAnimateResizePip(mDisplayBounds, DURATION_DEFAULT_MS, null);
+        scheduleAnimateResizePip(mLastReportedBounds, mDisplayBounds,
+                TRANSITION_DIRECTION_TO_FULLSCREEN, DURATION_DEFAULT_MS, null);
         mInPip = false;
     }
 
@@ -310,12 +316,12 @@
      */
     public void scheduleAnimateResizePip(Rect toBounds, int duration,
             Consumer<Rect> updateBoundsCallback) {
-        scheduleAnimateResizePip(false /* scheduleFinishPip */,
-                mLastReportedBounds, toBounds, duration, updateBoundsCallback);
+        scheduleAnimateResizePip(mLastReportedBounds, toBounds,
+                TRANSITION_DIRECTION_NONE, duration, updateBoundsCallback);
     }
 
-    private void scheduleAnimateResizePip(boolean scheduleFinishPip,
-            Rect currentBounds, Rect destinationBounds, int durationMs,
+    private void scheduleAnimateResizePip(Rect currentBounds, Rect destinationBounds,
+            @PipAnimationController.TransitionDirection int direction, int durationMs,
             Consumer<Rect> updateBoundsCallback) {
         Objects.requireNonNull(mToken, "Requires valid IWindowContainer");
         if (!mInPip) {
@@ -326,7 +332,7 @@
         args.arg1 = updateBoundsCallback;
         args.arg2 = currentBounds;
         args.arg3 = destinationBounds;
-        args.argi1 = scheduleFinishPip ? 1 : 0;
+        args.argi1 = direction;
         args.argi2 = durationMs;
         mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_RESIZE_ANIMATE, args));
     }
@@ -351,25 +357,25 @@
         Objects.requireNonNull(mToken, "Requires valid IWindowContainer");
         SurfaceControl.Transaction tx = new SurfaceControl.Transaction()
                 .setPosition(mLeash, destinationBounds.left, destinationBounds.top)
-                .setWindowCrop(mLeash, destinationBounds.width(), destinationBounds.height());
-        scheduleFinishResizePip(tx, destinationBounds, false /* scheduleFinishPip */,
-                null);
+                .setWindowCrop(mLeash, destinationBounds.width(), destinationBounds.height())
+                .setCornerRadius(mLeash, mInPip ? mCornerRadius : 0);
+        scheduleFinishResizePip(tx, destinationBounds, TRANSITION_DIRECTION_NONE, null);
     }
 
     private void scheduleFinishResizePip(SurfaceControl.Transaction tx,
-            Rect destinationBounds, boolean scheduleFinishPip,
+            Rect destinationBounds, @PipAnimationController.TransitionDirection int direction,
             Consumer<Rect> updateBoundsCallback) {
         Objects.requireNonNull(mToken, "Requires valid IWindowContainer");
         SomeArgs args = SomeArgs.obtain();
         args.arg1 = updateBoundsCallback;
         args.arg2 = tx;
         args.arg3 = destinationBounds;
-        args.argi1 = scheduleFinishPip ? 1 : 0;
+        args.argi1 = direction;
         mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_FINISH_RESIZE, args));
     }
 
     /**
-     * Offset the PiP window, animate if the given duration is not {@link #DURATION_NONE}
+     * Offset the PiP window by a given offset on Y-axis, triggered also from screen rotation.
      */
     public void scheduleOffsetPip(Rect originalBounds, int offset, int duration,
             Consumer<Rect> updateBoundsCallback) {
@@ -398,8 +404,7 @@
         }
         final Rect destinationBounds = new Rect(originalBounds);
         destinationBounds.offset(xOffset, yOffset);
-        animateResizePip(false /* scheduleFinishPip*/, originalBounds, destinationBounds,
-                durationMs);
+        animateResizePip(originalBounds, destinationBounds, TRANSITION_DIRECTION_SAME, durationMs);
     }
 
     private void resizePip(Rect destinationBounds) {
@@ -416,11 +421,12 @@
         new SurfaceControl.Transaction()
                 .setPosition(mLeash, destinationBounds.left, destinationBounds.top)
                 .setWindowCrop(mLeash, destinationBounds.width(), destinationBounds.height())
+                .setCornerRadius(mLeash, mInPip ? mCornerRadius : 0)
                 .apply();
     }
 
-    private void finishResize(Rect destinationBounds, SurfaceControl.Transaction tx,
-            boolean shouldScheduleFinishPip) {
+    private void finishResize(SurfaceControl.Transaction tx, Rect destinationBounds,
+            @PipAnimationController.TransitionDirection int direction) {
         if (Looper.myLooper() != mUpdateHandler.getLooper()) {
             throw new RuntimeException("Callers should call scheduleResizePip() instead of this "
                     + "directly");
@@ -428,7 +434,7 @@
         mLastReportedBounds.set(destinationBounds);
         try {
             final WindowContainerTransaction wct = new WindowContainerTransaction();
-            if (shouldScheduleFinishPip) {
+            if (direction == TRANSITION_DIRECTION_TO_PIP) {
                 wct.scheduleFinishEnterPip(mToken, destinationBounds);
             } else {
                 wct.setBounds(mToken, destinationBounds);
@@ -440,8 +446,8 @@
         }
     }
 
-    private void animateResizePip(boolean scheduleFinishPip, Rect currentBounds,
-            Rect destinationBounds, int durationMs) {
+    private void animateResizePip(Rect currentBounds, Rect destinationBounds,
+            @PipAnimationController.TransitionDirection int direction, int durationMs) {
         if (Looper.myLooper() != mUpdateHandler.getLooper()) {
             throw new RuntimeException("Callers should call scheduleAnimateResizePip() instead of "
                     + "this directly");
@@ -452,7 +458,9 @@
             return;
         }
         mUpdateHandler.post(() -> mPipAnimationController
-                .getAnimator(mLeash, scheduleFinishPip, currentBounds, destinationBounds)
+                .getAnimator(mLeash, currentBounds, destinationBounds)
+                .setTransitionDirection(direction)
+                .setCornerRadius(mCornerRadius)
                 .setPipAnimationCallback(mPipAnimationCallback)
                 .setDuration(durationMs)
                 .start());
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java b/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java
index 011893d..837256b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSMediaPlayer.java
@@ -17,158 +17,53 @@
 package com.android.systemui.qs;
 
 import android.app.Notification;
-import android.app.PendingIntent;
-import android.content.ComponentName;
 import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.content.res.ColorStateList;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Matrix;
-import android.graphics.Paint;
 import android.graphics.drawable.Drawable;
-import android.graphics.drawable.GradientDrawable;
 import android.graphics.drawable.Icon;
-import android.graphics.drawable.RippleDrawable;
-import android.media.MediaMetadata;
-import android.media.session.MediaController;
 import android.media.session.MediaSession;
-import android.media.session.PlaybackState;
-import android.os.Handler;
 import android.util.Log;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ImageButton;
-import android.widget.ImageView;
 import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import androidx.core.graphics.drawable.RoundedBitmapDrawable;
-import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
 
 import com.android.settingslib.media.MediaDevice;
-import com.android.settingslib.media.MediaOutputSliceConstants;
-import com.android.settingslib.widget.AdaptiveIcon;
-import com.android.systemui.Dependency;
 import com.android.systemui.R;
-import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.media.MediaControlPanel;
+import com.android.systemui.statusbar.NotificationMediaManager;
 
-import java.util.List;
+import java.util.concurrent.Executor;
 
 /**
  * Single media player for carousel in QSPanel
  */
-public class QSMediaPlayer {
+public class QSMediaPlayer extends MediaControlPanel {
 
     private static final String TAG = "QSMediaPlayer";
 
-    private Context mContext;
-    private LinearLayout mMediaNotifView;
-    private View mSeamless;
-    private MediaSession.Token mToken;
-    private MediaController mController;
-    private int mForegroundColor;
-    private int mBackgroundColor;
-    private ComponentName mRecvComponent;
-    private QSPanel mParent;
-
-    private MediaController.Callback mSessionCallback = new MediaController.Callback() {
-        @Override
-        public void onSessionDestroyed() {
-            Log.d(TAG, "session destroyed");
-            mController.unregisterCallback(mSessionCallback);
-
-            // Hide all the old buttons
-            final int[] actionIds = {
-                    R.id.action0,
-                    R.id.action1,
-                    R.id.action2,
-                    R.id.action3,
-                    R.id.action4
-            };
-            for (int i = 0; i < actionIds.length; i++) {
-                ImageButton thisBtn = mMediaNotifView.findViewById(actionIds[i]);
-                if (thisBtn != null) {
-                    thisBtn.setVisibility(View.GONE);
-                }
-            }
-
-            // Add a restart button
-            ImageButton btn = mMediaNotifView.findViewById(actionIds[0]);
-            btn.setOnClickListener(v -> {
-                Log.d(TAG, "Attempting to restart session");
-                // Send a media button event to previously found receiver
-                if (mRecvComponent != null) {
-                    Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
-                    intent.setComponent(mRecvComponent);
-                    int keyCode = KeyEvent.KEYCODE_MEDIA_PLAY;
-                    intent.putExtra(
-                            Intent.EXTRA_KEY_EVENT,
-                            new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
-                    mContext.sendBroadcast(intent);
-                } else {
-                    Log.d(TAG, "No receiver to restart");
-                    // If we don't have a receiver, try relaunching the activity instead
-                    try {
-                        mController.getSessionActivity().send();
-                    } catch (PendingIntent.CanceledException e) {
-                        Log.e(TAG, "Pending intent was canceled");
-                        e.printStackTrace();
-                    }
-                }
-            });
-            btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play));
-            btn.setImageTintList(ColorStateList.valueOf(mForegroundColor));
-            btn.setVisibility(View.VISIBLE);
-
-            // Add long-click option to remove the player
-            ViewGroup mMediaCarousel = (ViewGroup) mMediaNotifView.getParent();
-            mMediaNotifView.setOnLongClickListener(v -> {
-                // Replace player view with delete/cancel view
-                v.setVisibility(View.GONE);
-
-                View options = LayoutInflater.from(mContext).inflate(
-                        R.layout.qs_media_panel_options, null, false);
-                ImageButton btnDelete = options.findViewById(R.id.remove);
-                btnDelete.setOnClickListener(b -> {
-                    mMediaCarousel.removeView(options);
-                    mParent.removeMediaPlayer(QSMediaPlayer.this);
-                });
-                ImageButton btnCancel = options.findViewById(R.id.cancel);
-                btnCancel.setOnClickListener(b -> {
-                    mMediaCarousel.removeView(options);
-                    v.setVisibility(View.VISIBLE);
-                });
-
-                int pos = mMediaCarousel.indexOfChild(v);
-                mMediaCarousel.addView(options, pos, v.getLayoutParams());
-                return true; // consumed click
-            });
-        }
+    // Button IDs for QS controls
+    static final int[] QS_ACTION_IDS = {
+            R.id.action0,
+            R.id.action1,
+            R.id.action2,
+            R.id.action3,
+            R.id.action4
     };
 
     /**
-     *
+     * Initialize quick shade version of player
      * @param context
      * @param parent
+     * @param manager
+     * @param backgroundExecutor
      */
-    public QSMediaPlayer(Context context, ViewGroup parent) {
-        mContext = context;
-        LayoutInflater inflater = LayoutInflater.from(mContext);
-        mMediaNotifView = (LinearLayout) inflater.inflate(R.layout.qs_media_panel, parent, false);
-    }
-
-    public View getView() {
-        return mMediaNotifView;
+    public QSMediaPlayer(Context context, ViewGroup parent, NotificationMediaManager manager,
+            Executor backgroundExecutor) {
+        super(context, parent, manager, R.layout.qs_media_panel, QS_ACTION_IDS, backgroundExecutor);
     }
 
     /**
-     * Create or update the player view for the given media session
-     * @param parent the parent QSPanel
+     * Update media panel view for the given media session
      * @param token token for this media session
      * @param icon app notification icon
      * @param iconColor foreground color (for text, icons)
@@ -177,114 +72,20 @@
      * @param notif reference to original notification
      * @param device current playback device
      */
-    public void setMediaSession(QSPanel parent, MediaSession.Token token, Icon icon, int iconColor,
+    public void setMediaSession(MediaSession.Token token, Icon icon, int iconColor,
             int bgColor, View actionsContainer, Notification notif, MediaDevice device) {
-        mParent = parent;
-        mToken = token;
-        mForegroundColor = iconColor;
-        mBackgroundColor = bgColor;
-        mController = new MediaController(mContext, token);
 
-        // Try to find a receiver for the media button that matches this app
-        PackageManager pm = mContext.getPackageManager();
-        Intent it = new Intent(Intent.ACTION_MEDIA_BUTTON);
-        List<ResolveInfo> info = pm.queryBroadcastReceiversAsUser(it, 0, mContext.getUser());
-        if (info != null) {
-            for (ResolveInfo inf : info) {
-                if (inf.activityInfo.packageName.equals(mController.getPackageName())) {
-                    mRecvComponent = inf.getComponentInfo().getComponentName();
-                }
-            }
-        }
-
-        // reset in case we had previously restarted the stream
-        mMediaNotifView.setOnLongClickListener(null);
-        mController.registerCallback(mSessionCallback);
-        MediaMetadata mMediaMetadata = mController.getMetadata();
-        if (mMediaMetadata == null) {
-            Log.e(TAG, "Media metadata was null");
-            return;
-        }
-
-        Notification.Builder builder = Notification.Builder.recoverBuilder(mContext, notif);
-
-        // Album art
-        addAlbumArt(mMediaMetadata, bgColor);
-
-        LinearLayout headerView = mMediaNotifView.findViewById(R.id.header);
-
-        // App icon
-        ImageView appIcon = headerView.findViewById(R.id.icon);
-        Drawable iconDrawable = icon.loadDrawable(mContext);
-        iconDrawable.setTint(iconColor);
-        appIcon.setImageDrawable(iconDrawable);
-
-        // App title
-        TextView appName = headerView.findViewById(R.id.app_name);
-        String appNameString = builder.loadHeaderAppName();
-        appName.setText(appNameString);
-        appName.setTextColor(iconColor);
-
-        // Action
-        mMediaNotifView.setOnClickListener(v -> {
-            try {
-                notif.contentIntent.send();
-                // Also close shade
-                mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
-            } catch (PendingIntent.CanceledException e) {
-                Log.e(TAG, "Pending intent was canceled");
-                e.printStackTrace();
-            }
-        });
-
-        // Transfer chip
-        mSeamless = headerView.findViewById(R.id.media_seamless);
-        mSeamless.setVisibility(View.VISIBLE);
-        updateChip(device);
-        ActivityStarter mActivityStarter = Dependency.get(ActivityStarter.class);
-        mSeamless.setOnClickListener(v -> {
-            final Intent intent = new Intent()
-                    .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT)
-                    .putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME,
-                        mController.getPackageName())
-                    .putExtra(MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN, token);
-            mActivityStarter.startActivity(intent, false, true /* dismissShade */,
-                    Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
-        });
-
-        // Artist name
-        TextView artistText = headerView.findViewById(R.id.header_artist);
-        String artistName = mMediaMetadata.getString(MediaMetadata.METADATA_KEY_ARTIST);
-        artistText.setText(artistName);
-        artistText.setTextColor(iconColor);
-
-        // Song name
-        TextView titleText = headerView.findViewById(R.id.header_text);
-        String songName = mMediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);
-        titleText.setText(songName);
-        titleText.setTextColor(iconColor);
+        String appName = Notification.Builder.recoverBuilder(getContext(), notif)
+                .loadHeaderAppName();
+        super.setMediaSession(token, icon, iconColor, bgColor, notif.contentIntent,
+                appName, device);
 
         // Media controls
         LinearLayout parentActionsLayout = (LinearLayout) actionsContainer;
-        final int[] actionIds = {
-                R.id.action0,
-                R.id.action1,
-                R.id.action2,
-                R.id.action3,
-                R.id.action4
-        };
-        final int[] notifActionIds = {
-                com.android.internal.R.id.action0,
-                com.android.internal.R.id.action1,
-                com.android.internal.R.id.action2,
-                com.android.internal.R.id.action3,
-                com.android.internal.R.id.action4
-        };
-
         int i = 0;
-        for (; i < parentActionsLayout.getChildCount() && i < actionIds.length; i++) {
-            ImageButton thisBtn = mMediaNotifView.findViewById(actionIds[i]);
-            ImageButton thatBtn = parentActionsLayout.findViewById(notifActionIds[i]);
+        for (; i < parentActionsLayout.getChildCount() && i < QS_ACTION_IDS.length; i++) {
+            ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]);
+            ImageButton thatBtn = parentActionsLayout.findViewById(NOTIF_ACTION_IDS[i]);
             if (thatBtn == null || thatBtn.getDrawable() == null
                     || thatBtn.getVisibility() != View.VISIBLE) {
                 thisBtn.setVisibility(View.GONE);
@@ -301,116 +102,9 @@
         }
 
         // Hide any unused buttons
-        for (; i < actionIds.length; i++) {
-            ImageButton thisBtn = mMediaNotifView.findViewById(actionIds[i]);
+        for (; i < QS_ACTION_IDS.length; i++) {
+            ImageButton thisBtn = mMediaNotifView.findViewById(QS_ACTION_IDS[i]);
             thisBtn.setVisibility(View.GONE);
         }
     }
-
-    public MediaSession.Token getMediaSessionToken() {
-        return mToken;
-    }
-
-    public String getMediaPlayerPackage() {
-        return mController.getPackageName();
-    }
-
-    /**
-     * Check whether the media controlled by this player is currently playing
-     * @return whether it is playing, or false if no controller information
-     */
-    public boolean isPlaying() {
-        if (mController == null) {
-            return false;
-        }
-
-        PlaybackState state = mController.getPlaybackState();
-        if (state == null) {
-            return false;
-        }
-
-        return (state.getState() == PlaybackState.STATE_PLAYING);
-    }
-
-    private void addAlbumArt(MediaMetadata metadata, int bgColor) {
-        Bitmap albumArt = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART);
-        float radius = mContext.getResources().getDimension(R.dimen.qs_media_corner_radius);
-        ImageView albumView = mMediaNotifView.findViewById(R.id.album_art);
-        if (albumArt != null) {
-            Log.d(TAG, "updating album art");
-            Bitmap original = albumArt.copy(Bitmap.Config.ARGB_8888, true);
-            int albumSize = (int) mContext.getResources().getDimension(R.dimen.qs_media_album_size);
-            Bitmap scaled = scaleBitmap(original, albumSize, albumSize);
-            RoundedBitmapDrawable roundedDrawable = RoundedBitmapDrawableFactory.create(
-                    mContext.getResources(), scaled);
-            roundedDrawable.setCornerRadius(radius);
-            albumView.setImageDrawable(roundedDrawable);
-        } else {
-            Log.e(TAG, "No album art available");
-            albumView.setImageDrawable(null);
-        }
-
-        mMediaNotifView.setBackgroundTintList(ColorStateList.valueOf(bgColor));
-    }
-
-    private Bitmap scaleBitmap(Bitmap original, int width, int height) {
-        Bitmap cropped = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
-        Canvas canvas = new Canvas(cropped);
-
-        float scale = (float) cropped.getWidth() / (float) original.getWidth();
-        float dy = (cropped.getHeight() - original.getHeight() * scale) / 2.0f;
-        Matrix transformation = new Matrix();
-        transformation.postTranslate(0, dy);
-        transformation.preScale(scale, scale);
-
-        Paint paint = new Paint();
-        paint.setFilterBitmap(true);
-        canvas.drawBitmap(original, transformation, paint);
-
-        return cropped;
-    }
-
-    protected void updateChip(MediaDevice device) {
-        if (mSeamless == null) {
-            return;
-        }
-        Handler handler = mSeamless.getHandler();
-        handler.post(() -> {
-            updateChipInternal(device);
-        });
-    }
-
-    private void updateChipInternal(MediaDevice device) {
-        ColorStateList fgTintList = ColorStateList.valueOf(mForegroundColor);
-
-        // Update the outline color
-        LinearLayout viewLayout = (LinearLayout) mSeamless;
-        RippleDrawable bkgDrawable = (RippleDrawable) viewLayout.getBackground();
-        GradientDrawable rect = (GradientDrawable) bkgDrawable.getDrawable(0);
-        rect.setStroke(2, mForegroundColor);
-        rect.setColor(mBackgroundColor);
-
-        ImageView iconView = mSeamless.findViewById(R.id.media_seamless_image);
-        TextView deviceName = mSeamless.findViewById(R.id.media_seamless_text);
-        deviceName.setTextColor(fgTintList);
-
-        if (device != null) {
-            Drawable icon = device.getIcon();
-            iconView.setVisibility(View.VISIBLE);
-            iconView.setImageTintList(fgTintList);
-
-            if (icon instanceof AdaptiveIcon) {
-                AdaptiveIcon aIcon = (AdaptiveIcon) icon;
-                aIcon.setBackgroundColor(mBackgroundColor);
-                iconView.setImageDrawable(aIcon);
-            } else {
-                iconView.setImageDrawable(icon);
-            }
-            deviceName.setText(device.getName());
-        } else {
-            // Reset to default
-            iconView.setVisibility(View.GONE);
-            deviceName.setText(com.android.internal.R.string.ext_media_seamless_action);
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index d2d9092..9ab4714 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -50,6 +50,7 @@
 import com.android.systemui.Dumpable;
 import com.android.systemui.R;
 import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.qs.DetailAdapter;
 import com.android.systemui.plugins.qs.QSTile;
@@ -60,6 +61,7 @@
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.settings.BrightnessController;
 import com.android.systemui.settings.ToggleSliderView;
+import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.policy.BrightnessMirrorController;
 import com.android.systemui.statusbar.policy.BrightnessMirrorController.BrightnessMirrorListener;
 import com.android.systemui.tuner.TunerService;
@@ -70,6 +72,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.concurrent.Executor;
 import java.util.stream.Collectors;
 
 import javax.inject.Inject;
@@ -94,6 +97,8 @@
 
     private final LinearLayout mMediaCarousel;
     private final ArrayList<QSMediaPlayer> mMediaPlayers = new ArrayList<>();
+    private final NotificationMediaManager mNotificationMediaManager;
+    private final Executor mBackgroundExecutor;
     private LocalMediaManager mLocalMediaManager;
     private MediaDevice mDevice;
     private boolean mUpdateCarousel = false;
@@ -128,7 +133,7 @@
             if (mDevice == null || !mDevice.equals(currentDevice)) {
                 mDevice = currentDevice;
                 for (QSMediaPlayer p : mMediaPlayers) {
-                    p.updateChip(mDevice);
+                    p.updateDevice(mDevice);
                 }
             }
         }
@@ -138,7 +143,7 @@
             if (mDevice == null || !mDevice.equals(device)) {
                 mDevice = device;
                 for (QSMediaPlayer p : mMediaPlayers) {
-                    p.updateChip(mDevice);
+                    p.updateDevice(mDevice);
                 }
             }
         }
@@ -150,12 +155,16 @@
             AttributeSet attrs,
             DumpManager dumpManager,
             BroadcastDispatcher broadcastDispatcher,
-            QSLogger qsLogger
+            QSLogger qsLogger,
+            NotificationMediaManager notificationMediaManager,
+            @Background Executor backgroundExecutor
     ) {
         super(context, attrs);
         mContext = context;
         mQSLogger = qsLogger;
         mDumpManager = dumpManager;
+        mNotificationMediaManager = notificationMediaManager;
+        mBackgroundExecutor = backgroundExecutor;
 
         setOrientation(VERTICAL);
 
@@ -255,7 +264,8 @@
 
         if (player == null) {
             Log.d(TAG, "creating new player");
-            player = new QSMediaPlayer(mContext, this);
+            player = new QSMediaPlayer(mContext, this, mNotificationMediaManager,
+                    mBackgroundExecutor);
 
             if (player.isPlaying()) {
                 mMediaCarousel.addView(player.getView(), 0, lp); // add in front
@@ -268,7 +278,7 @@
         }
 
         Log.d(TAG, "setting player session");
-        player.setMediaSession(this, token, icon, iconColor, bgColor, actionsContainer,
+        player.setMediaSession(token, icon, iconColor, bgColor, actionsContainer,
                 notif.getNotification(), mDevice);
 
         if (mMediaPlayers.size() > 0) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java
index 9018a37..4512afb 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSMediaPlayer.java
@@ -17,108 +17,47 @@
 package com.android.systemui.qs;
 
 import android.app.PendingIntent;
-import android.content.ComponentName;
 import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.content.res.ColorStateList;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
-import android.media.MediaMetadata;
 import android.media.session.MediaController;
 import android.media.session.MediaSession;
-import android.media.session.PlaybackState;
-import android.util.Log;
-import android.view.KeyEvent;
-import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ImageButton;
-import android.widget.ImageView;
 import android.widget.LinearLayout;
-import android.widget.TextView;
 
 import com.android.systemui.R;
+import com.android.systemui.media.MediaControlPanel;
+import com.android.systemui.statusbar.NotificationMediaManager;
 
-import java.util.List;
+import java.util.concurrent.Executor;
 
 /**
  * QQS mini media player
  */
-public class QuickQSMediaPlayer {
+public class QuickQSMediaPlayer extends MediaControlPanel {
 
     private static final String TAG = "QQSMediaPlayer";
 
-    private Context mContext;
-    private LinearLayout mMediaNotifView;
-    private MediaSession.Token mToken;
-    private MediaController mController;
-    private int mForegroundColor;
-    private ComponentName mRecvComponent;
-
-    private MediaController.Callback mSessionCallback = new MediaController.Callback() {
-        @Override
-        public void onSessionDestroyed() {
-            Log.d(TAG, "session destroyed");
-            mController.unregisterCallback(mSessionCallback);
-
-            // Hide all the old buttons
-            final int[] actionIds = {R.id.action0, R.id.action1, R.id.action2};
-            for (int i = 0; i < actionIds.length; i++) {
-                ImageButton thisBtn = mMediaNotifView.findViewById(actionIds[i]);
-                if (thisBtn != null) {
-                    thisBtn.setVisibility(View.GONE);
-                }
-            }
-
-            // Add a restart button
-            ImageButton btn = mMediaNotifView.findViewById(actionIds[0]);
-            btn.setOnClickListener(v -> {
-                Log.d(TAG, "Attempting to restart session");
-                // Send a media button event to previously found receiver
-                if (mRecvComponent != null) {
-                    Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON);
-                    intent.setComponent(mRecvComponent);
-                    int keyCode = KeyEvent.KEYCODE_MEDIA_PLAY;
-                    intent.putExtra(
-                            Intent.EXTRA_KEY_EVENT,
-                            new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
-                    mContext.sendBroadcast(intent);
-                } else {
-                    Log.d(TAG, "No receiver to restart");
-                    // If we don't have a receiver, try relaunching the activity instead
-                    try {
-                        mController.getSessionActivity().send();
-                    } catch (PendingIntent.CanceledException e) {
-                        Log.e(TAG, "Pending intent was canceled");
-                        e.printStackTrace();
-                    }
-                }
-            });
-            btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play));
-            btn.setImageTintList(ColorStateList.valueOf(mForegroundColor));
-            btn.setVisibility(View.VISIBLE);
-        }
-    };
+    // Button IDs for QS controls
+    private static final int[] QQS_ACTION_IDS = {R.id.action0, R.id.action1, R.id.action2};
 
     /**
-     *
+     * Initialize mini media player for QQS
      * @param context
      * @param parent
+     * @param manager
+     * @param backgroundExecutor
      */
-    public QuickQSMediaPlayer(Context context, ViewGroup parent) {
-        mContext = context;
-        LayoutInflater inflater = LayoutInflater.from(mContext);
-        mMediaNotifView = (LinearLayout) inflater.inflate(R.layout.qqs_media_panel, parent, false);
-    }
-
-    public View getView() {
-        return mMediaNotifView;
+    public QuickQSMediaPlayer(Context context, ViewGroup parent, NotificationMediaManager manager,
+            Executor backgroundExecutor) {
+        super(context, parent, manager, R.layout.qqs_media_panel, QQS_ACTION_IDS,
+                backgroundExecutor);
     }
 
     /**
-     *
+     * Update media panel view for the given media session
      * @param token token for this media session
      * @param icon app notification icon
      * @param iconColor foreground color (for text, icons)
@@ -130,84 +69,30 @@
      */
     public void setMediaSession(MediaSession.Token token, Icon icon, int iconColor, int bgColor,
             View actionsContainer, int[] actionsToShow, PendingIntent contentIntent) {
-        mToken = token;
-        mForegroundColor = iconColor;
-
+        // Only update if this is a different session and currently playing
         String oldPackage = "";
-        if (mController != null) {
-            oldPackage = mController.getPackageName();
+        if (getController() != null) {
+            oldPackage = getController().getPackageName();
         }
-        MediaController controller = new MediaController(mContext, token);
-        boolean samePlayer = mToken.equals(token) && oldPackage.equals(controller.getPackageName());
-        if (mController != null && !samePlayer && !isPlaying(controller)) {
-            // Only update if this is a different session and currently playing
-            return;
-        }
-        mController = controller;
-        MediaMetadata mMediaMetadata = mController.getMetadata();
-
-        // Try to find a receiver for the media button that matches this app
-        PackageManager pm = mContext.getPackageManager();
-        Intent it = new Intent(Intent.ACTION_MEDIA_BUTTON);
-        List<ResolveInfo> info = pm.queryBroadcastReceiversAsUser(it, 0, mContext.getUser());
-        if (info != null) {
-            for (ResolveInfo inf : info) {
-                if (inf.activityInfo.packageName.equals(mController.getPackageName())) {
-                    mRecvComponent = inf.getComponentInfo().getComponentName();
-                }
-            }
-        }
-        mController.registerCallback(mSessionCallback);
-
-        if (mMediaMetadata == null) {
-            Log.e(TAG, "Media metadata was null");
+        MediaController controller = new MediaController(getContext(), token);
+        MediaSession.Token currentToken = getMediaSessionToken();
+        boolean samePlayer = currentToken != null
+                && currentToken.equals(token)
+                && oldPackage.equals(controller.getPackageName());
+        if (getController() != null && !samePlayer && !isPlaying(controller)) {
             return;
         }
 
-        // Action
-        mMediaNotifView.setOnClickListener(v -> {
-            try {
-                contentIntent.send();
-                mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
-            } catch (PendingIntent.CanceledException e) {
-                Log.e(TAG, "Pending intent was canceled: " + e.getMessage());
-            }
-        });
+        super.setMediaSession(token, icon, iconColor, bgColor, contentIntent, null, null);
 
-        mMediaNotifView.setBackgroundTintList(ColorStateList.valueOf(bgColor));
-
-        // App icon
-        ImageView appIcon = mMediaNotifView.findViewById(R.id.icon);
-        Drawable iconDrawable = icon.loadDrawable(mContext);
-        iconDrawable.setTint(mForegroundColor);
-        appIcon.setImageDrawable(iconDrawable);
-
-        // Song name
-        TextView titleText = mMediaNotifView.findViewById(R.id.header_title);
-        String songName = mMediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE);
-        titleText.setText(songName);
-        titleText.setTextColor(mForegroundColor);
-
-        // Buttons we can display
-        final int[] actionIds = {R.id.action0, R.id.action1, R.id.action2};
-
-        // Existing buttons in the notification
         LinearLayout parentActionsLayout = (LinearLayout) actionsContainer;
-        final int[] notifActionIds = {
-                com.android.internal.R.id.action0,
-                com.android.internal.R.id.action1,
-                com.android.internal.R.id.action2,
-                com.android.internal.R.id.action3,
-                com.android.internal.R.id.action4
-        };
-
         int i = 0;
         if (actionsToShow != null) {
             int maxButtons = Math.min(actionsToShow.length, parentActionsLayout.getChildCount());
-            maxButtons = Math.min(maxButtons, actionIds.length);
+            maxButtons = Math.min(maxButtons, QQS_ACTION_IDS.length);
             for (; i < maxButtons; i++) {
-                ImageButton thisBtn = mMediaNotifView.findViewById(actionIds[i]);
-                int thatId = notifActionIds[actionsToShow[i]];
+                ImageButton thisBtn = mMediaNotifView.findViewById(QQS_ACTION_IDS[i]);
+                int thatId = NOTIF_ACTION_IDS[actionsToShow[i]];
                 ImageButton thatBtn = parentActionsLayout.findViewById(thatId);
                 if (thatBtn == null || thatBtn.getDrawable() == null
                         || thatBtn.getVisibility() != View.VISIBLE) {
@@ -225,38 +110,9 @@
         }
 
         // Hide any unused buttons
-        for (; i < actionIds.length; i++) {
-            ImageButton thisBtn = mMediaNotifView.findViewById(actionIds[i]);
+        for (; i < QQS_ACTION_IDS.length; i++) {
+            ImageButton thisBtn = mMediaNotifView.findViewById(QQS_ACTION_IDS[i]);
             thisBtn.setVisibility(View.GONE);
         }
     }
-
-    public MediaSession.Token getMediaSessionToken() {
-        return mToken;
-    }
-
-    /**
-     * Check whether the media controlled by this player is currently playing
-     * @return whether it is playing, or false if no controller information
-     */
-    public boolean isPlaying(MediaController controller) {
-        if (controller == null) {
-            return false;
-        }
-
-        PlaybackState state = controller.getPlaybackState();
-        if (state == null) {
-            return false;
-        }
-
-        return (state.getState() == PlaybackState.STATE_PLAYING);
-    }
-
-    /**
-     * Check whether this player has an attached media session.
-     * @return whether there is a controller with a current media session.
-     */
-    public boolean hasMediaSession() {
-        return mController != null && mController.getPlaybackState() != null;
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
index 20efbcb..3da767e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
@@ -29,18 +29,21 @@
 import com.android.systemui.Dependency;
 import com.android.systemui.R;
 import com.android.systemui.broadcast.BroadcastDispatcher;
+import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.plugins.qs.QSTile.SignalState;
 import com.android.systemui.plugins.qs.QSTile.State;
 import com.android.systemui.qs.customize.QSCustomizer;
 import com.android.systemui.qs.logging.QSLogger;
+import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.tuner.TunerService;
 import com.android.systemui.tuner.TunerService.Tunable;
 import com.android.systemui.util.Utils;
 
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
 import javax.inject.Named;
@@ -72,9 +75,12 @@
             AttributeSet attrs,
             DumpManager dumpManager,
             BroadcastDispatcher broadcastDispatcher,
-            QSLogger qsLogger
+            QSLogger qsLogger,
+            NotificationMediaManager notificationMediaManager,
+            @Background Executor backgroundExecutor
     ) {
-        super(context, attrs, dumpManager, broadcastDispatcher, qsLogger);
+        super(context, attrs, dumpManager, broadcastDispatcher, qsLogger, notificationMediaManager,
+                backgroundExecutor);
         if (mFooter != null) {
             removeView(mFooter.getView());
         }
@@ -93,7 +99,8 @@
             mHorizontalLinearLayout.setClipToPadding(false);
 
             int marginSize = (int) mContext.getResources().getDimension(R.dimen.qqs_media_spacing);
-            mMediaPlayer = new QuickQSMediaPlayer(mContext, mHorizontalLinearLayout);
+            mMediaPlayer = new QuickQSMediaPlayer(mContext, mHorizontalLinearLayout,
+                    notificationMediaManager, backgroundExecutor);
             LayoutParams lp2 = new LayoutParams(0, LayoutParams.MATCH_PARENT, 1);
             lp2.setMarginEnd(marginSize);
             lp2.setMarginStart(0);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NavigationBarController.java b/packages/SystemUI/src/com/android/systemui/statusbar/NavigationBarController.java
index ebac4b2..1b752452 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NavigationBarController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NavigationBarController.java
@@ -165,6 +165,7 @@
     private void removeNavigationBar(int displayId) {
         NavigationBarFragment navBar = mNavigationBars.get(displayId);
         if (navBar != null) {
+            navBar.setAutoHideController(/* autoHideController */ null);
             View navigationWindow = navBar.getView().getRootView();
             WindowManagerGlobal.getInstance()
                     .removeView(navigationWindow, true /* immediate */);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ScrimView.java b/packages/SystemUI/src/com/android/systemui/statusbar/ScrimView.java
index 04f1c32..7f30009 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/ScrimView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ScrimView.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar;
 
+import static java.lang.Float.isNaN;
+
 import android.annotation.NonNull;
 import android.content.Context;
 import android.graphics.Canvas;
@@ -179,6 +181,9 @@
      * @param alpha Gradient alpha from 0 to 1.
      */
     public void setViewAlpha(float alpha) {
+        if (isNaN(alpha)) {
+            throw new IllegalArgumentException("alpha cannot be NaN: " + alpha);
+        }
         if (alpha != mViewAlpha) {
             mViewAlpha = alpha;
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubViewController.kt
index 62d3612..7f42fe0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/people/PeopleHubViewController.kt
@@ -164,8 +164,8 @@
         // Immediately report current value of setting
         updateListener(listener)
         val observer = object : ContentObserver(handler) {
-            override fun onChange(selfChange: Boolean, uri: Uri?, userId: Int) {
-                super.onChange(selfChange, uri, userId)
+            override fun onChange(selfChange: Boolean, uri: Uri?, flags: Int) {
+                super.onChange(selfChange, uri, flags)
                 updateListener(listener)
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java
index 02cf8cc..b119f0b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NavigationBarFragment.java
@@ -205,6 +205,28 @@
 
     private final Handler mHandler;
 
+    private final AutoHideUiElement mAutoHideUiElement = new AutoHideUiElement() {
+        @Override
+        public void synchronizeState() {
+            checkNavBarModes();
+        }
+
+        @Override
+        public boolean shouldHideOnTouch() {
+            return !mNotificationRemoteInputManager.getController().isRemoteInputActive();
+        }
+
+        @Override
+        public boolean isVisible() {
+            return isTransientShown();
+        }
+
+        @Override
+        public void hide() {
+            clearTransient();
+        }
+    };
+
     private final OverviewProxyListener mOverviewProxyListener = new OverviewProxyListener() {
         @Override
         public void onConnectionChanged(boolean isConnected) {
@@ -1052,28 +1074,13 @@
 
     /** Sets {@link AutoHideController} to the navigation bar. */
     public void setAutoHideController(AutoHideController autoHideController) {
+        if (mAutoHideController != null) {
+            mAutoHideController.removeAutoHideUiElement(mAutoHideUiElement);
+        }
         mAutoHideController = autoHideController;
-        mAutoHideController.addAutoHideUiElement(new AutoHideUiElement() {
-            @Override
-            public void synchronizeState() {
-                checkNavBarModes();
-            }
-
-            @Override
-            public boolean shouldHideOnTouch() {
-                return !mNotificationRemoteInputManager.getController().isRemoteInputActive();
-            }
-
-            @Override
-            public boolean isVisible() {
-                return isTransientShown();
-            }
-
-            @Override
-            public void hide() {
-                clearTransient();
-            }
-        });
+        if (mAutoHideController != null) {
+            mAutoHideController.addAutoHideUiElement(mAutoHideUiElement);
+        }
     }
 
     private boolean isTransientShown() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
index 945a9db..d1ff32d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.phone;
 
+import static java.lang.Float.isNaN;
+
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ValueAnimator;
@@ -289,6 +291,10 @@
         mInFrontAlpha = state.getFrontAlpha();
         mBehindAlpha = state.getBehindAlpha();
         mBubbleAlpha = state.getBubbleAlpha();
+        if (isNaN(mBehindAlpha) || isNaN(mInFrontAlpha)) {
+            throw new IllegalStateException("Scrim opacity is NaN for state: " + state + ", front: "
+                    + mInFrontAlpha + ", back: " + mBehindAlpha);
+        }
         applyExpansionToAlpha();
 
         // Scrim might acquire focus when user is navigating with a D-pad or a keyboard.
@@ -416,6 +422,9 @@
      * @param fraction From 0 to 1 where 0 means collapsed and 1 expanded.
      */
     public void setPanelExpansion(float fraction) {
+        if (isNaN(fraction)) {
+            throw new IllegalArgumentException("Fraction should not be NaN");
+        }
         if (mExpansionFraction != fraction) {
             mExpansionFraction = fraction;
 
@@ -493,6 +502,10 @@
             mBehindTint = ColorUtils.blendARGB(ScrimState.BOUNCER.getBehindTint(),
                     mState.getBehindTint(), interpolatedFract);
         }
+        if (isNaN(mBehindAlpha) || isNaN(mInFrontAlpha)) {
+            throw new IllegalStateException("Scrim opacity is NaN for state: " + mState
+                    + ", front: " + mInFrontAlpha + ", back: " + mBehindAlpha);
+        }
     }
 
     /**
@@ -548,6 +561,10 @@
             float newBehindAlpha = mState.getBehindAlpha();
             if (mBehindAlpha != newBehindAlpha) {
                 mBehindAlpha = newBehindAlpha;
+                if (isNaN(mBehindAlpha)) {
+                    throw new IllegalStateException("Scrim opacity is NaN for state: " + mState
+                            + ", back: " + mBehindAlpha);
+                }
                 updateScrims();
             }
         }
@@ -948,8 +965,11 @@
         pw.print(" tint=0x");
         pw.println(Integer.toHexString(mScrimForBubble.getTint()));
 
-        pw.print("   mTracking=");
+        pw.print("  mTracking=");
         pw.println(mTracking);
+
+        pw.print("  mExpansionFraction=");
+        pw.println(mExpansionFraction);
     }
 
     public void setWallpaperSupportsAmbientMode(boolean wallpaperSupportsAmbientMode) {
@@ -996,6 +1016,10 @@
         // in this case, back-scrim needs to be re-evaluated
         if (mState == ScrimState.AOD || mState == ScrimState.PULSING) {
             float newBehindAlpha = mState.getBehindAlpha();
+            if (isNaN(newBehindAlpha)) {
+                throw new IllegalStateException("Scrim opacity is NaN for state: " + mState
+                        + ", back: " + mBehindAlpha);
+            }
             if (mBehindAlpha != newBehindAlpha) {
                 mBehindAlpha = newBehindAlpha;
                 updateScrims();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImpl.java
index a3e2e76..7280a88 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImpl.java
@@ -59,7 +59,7 @@
         mUserSetupUri = Secure.getUriFor(Secure.USER_SETUP_COMPLETE);
         mSettingsObserver = new ContentObserver(mainHandler) {
             @Override
-            public void onChange(boolean selfChange, Uri uri, int userId) {
+            public void onChange(boolean selfChange, Uri uri, int flags) {
                 Log.d(TAG, "Setting change: " + uri);
                 if (mUserSetupUri.equals(uri)) {
                     notifySetupChanged();
diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
index 31b9952..30db37c 100644
--- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
@@ -99,8 +99,10 @@
                 Settings.Secure.getUriFor(Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES),
                 false,
                 new ContentObserver(mBgHandler) {
+
                     @Override
-                    public void onChange(boolean selfChange, Uri uri, int userId) {
+                    public void onChange(boolean selfChange, Iterable<Uri> uris, int flags,
+                            int userId) {
                         if (DEBUG) Log.d(TAG, "Overlay changed for user: " + userId);
                         if (ActivityManager.getCurrentUser() == userId) {
                             updateThemeOverlays();
diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
index 142fdc2..b2a5f5b 100644
--- a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java
@@ -262,10 +262,13 @@
         }
 
         @Override
-        public void onChange(boolean selfChange, Uri uri, int userId) {
+        public void onChange(boolean selfChange, Iterable<Uri> uris, int flags, int userId) {
             if (userId == ActivityManager.getCurrentUser()) {
-                reloadSetting(uri);
+                for (Uri u : uris) {
+                    reloadSetting(u);
+                }
             }
         }
+
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/util/DismissCircleView.java b/packages/SystemUI/src/com/android/systemui/util/DismissCircleView.java
new file mode 100644
index 0000000..6c3538c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/DismissCircleView.java
@@ -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.systemui.util;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.view.Gravity;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import com.android.systemui.R;
+
+/**
+ * Circular view with a semitransparent, circular background with an 'X' inside it.
+ *
+ * This is used by both Bubbles and PIP as the dismiss target.
+ */
+public class DismissCircleView extends FrameLayout {
+
+    private final ImageView mIconView = new ImageView(getContext());
+
+    public DismissCircleView(Context context) {
+        super(context);
+        final Resources res = getResources();
+
+        setBackground(res.getDrawable(R.drawable.dismiss_circle_background));
+
+        mIconView.setImageDrawable(res.getDrawable(R.drawable.dismiss_target_x));
+        addView(mIconView);
+
+        setViewSizes();
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        setViewSizes();
+    }
+
+    /** Retrieves the current dimensions for the icon and circle and applies them. */
+    private void setViewSizes() {
+        final Resources res = getResources();
+        final int iconSize = res.getDimensionPixelSize(R.dimen.dismiss_target_x_size);
+        mIconView.setLayoutParams(
+                new FrameLayout.LayoutParams(iconSize, iconSize, Gravity.CENTER));
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/ExpandedAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/ExpandedAnimationControllerTest.java
index ae4581a..ec6d3e9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/ExpandedAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/ExpandedAnimationControllerTest.java
@@ -17,7 +17,6 @@
 package com.android.systemui.bubbles.animation;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
 import static org.mockito.Mockito.verify;
 
 import android.content.res.Configuration;
@@ -118,118 +117,6 @@
         testBubblesInCorrectExpandedPositions();
     }
 
-    @Test
-    @Ignore
-    public void testBubbleDraggedNotDismissedSnapsBack() throws InterruptedException {
-        expand();
-
-        final View draggedBubble = mViews.get(0);
-        mExpandedController.prepareForBubbleDrag(draggedBubble);
-        mExpandedController.dragBubbleOut(draggedBubble, 500f, 500f);
-
-        assertEquals(500f, draggedBubble.getTranslationX(), 1f);
-        assertEquals(500f, draggedBubble.getTranslationY(), 1f);
-
-        // Snap it back and make sure it made it back correctly.
-        mExpandedController.snapBubbleBack(draggedBubble, 0f, 0f);
-        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
-        testBubblesInCorrectExpandedPositions();
-    }
-
-    @Test
-    @Ignore
-    public void testBubbleDismissed() throws InterruptedException {
-        expand();
-
-        final View draggedBubble = mViews.get(0);
-        mExpandedController.prepareForBubbleDrag(draggedBubble);
-        mExpandedController.dragBubbleOut(draggedBubble, 500f, 500f);
-
-        assertEquals(500f, draggedBubble.getTranslationX(), 1f);
-        assertEquals(500f, draggedBubble.getTranslationY(), 1f);
-
-        mLayout.removeView(draggedBubble);
-        waitForLayoutMessageQueue();
-        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
-
-        assertEquals(-1, mLayout.indexOfChild(draggedBubble));
-        testBubblesInCorrectExpandedPositions();
-    }
-
-    @Test
-    @Ignore("Flaky")
-    public void testMagnetToDismiss_dismiss() throws InterruptedException {
-        expand();
-
-        final View draggedOutView = mViews.get(0);
-        final Runnable after = Mockito.mock(Runnable.class);
-
-        mExpandedController.prepareForBubbleDrag(draggedOutView);
-        mExpandedController.dragBubbleOut(draggedOutView, 25, 25);
-
-        // Magnet to dismiss, verify the bubble is at the dismiss target and the callback was
-        // called.
-        mExpandedController.magnetBubbleToDismiss(
-                mViews.get(0), 100 /* velX */, 100 /* velY */, 1000 /* destY */, after);
-        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
-        verify(after).run();
-        assertEquals(1000, mViews.get(0).getTranslationY(), .1f);
-
-        // Dismiss the now-magneted bubble, verify that the callback was called.
-        final Runnable afterDismiss = Mockito.mock(Runnable.class);
-        mExpandedController.dismissDraggedOutBubble(draggedOutView, afterDismiss);
-        waitForPropertyAnimations(DynamicAnimation.ALPHA);
-        verify(after).run();
-
-        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
-
-        assertEquals(mBubblePaddingTop, mViews.get(1).getTranslationX(), 1f);
-    }
-
-    @Test
-    @Ignore("Flaky")
-    public void testMagnetToDismiss_demagnetizeThenDrag() throws InterruptedException {
-        expand();
-
-        final View draggedOutView = mViews.get(0);
-        final Runnable after = Mockito.mock(Runnable.class);
-
-        mExpandedController.prepareForBubbleDrag(draggedOutView);
-        mExpandedController.dragBubbleOut(draggedOutView, 25, 25);
-
-        // Magnet to dismiss, verify the bubble is at the dismiss target and the callback was
-        // called.
-        mExpandedController.magnetBubbleToDismiss(
-                draggedOutView, 100 /* velX */, 100 /* velY */, 1000 /* destY */, after);
-        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
-        verify(after).run();
-        assertEquals(1000, mViews.get(0).getTranslationY(), .1f);
-
-        // Demagnetize the bubble towards (25, 25).
-        mExpandedController.demagnetizeBubbleTo(25 /* x */, 25 /* y */, 100, 100);
-
-        // Start dragging towards (20, 20).
-        mExpandedController.dragBubbleOut(draggedOutView, 20, 20);
-
-        // Since we just demagnetized, the bubble shouldn't be at (20, 20), it should be animating
-        // towards it.
-        assertNotEquals(20, draggedOutView.getTranslationX());
-        assertNotEquals(20, draggedOutView.getTranslationY());
-        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
-
-        // Waiting for the animations should result in the bubble ending at (20, 20) since the
-        // animation end value was updated.
-        assertEquals(20, draggedOutView.getTranslationX(), 1f);
-        assertEquals(20, draggedOutView.getTranslationY(), 1f);
-
-        // Drag to (30, 30).
-        mExpandedController.dragBubbleOut(draggedOutView, 30, 30);
-
-        // It should go there instantly since the animations finished.
-        assertEquals(30, draggedOutView.getTranslationX(), 1f);
-        assertEquals(30, draggedOutView.getTranslationY(), 1f);
-    }
-
     /** Expand the stack and wait for animations to finish. */
     private void expand() throws InterruptedException {
         mExpandedController.expandFromStack(Mockito.mock(Runnable.class));
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java
index 9cc0349..e3187cb9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/animation/StackAnimationControllerTest.java
@@ -17,7 +17,6 @@
 package com.android.systemui.bubbles.animation;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
@@ -41,7 +40,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
-import org.mockito.Mockito;
 
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
@@ -242,61 +240,6 @@
     }
 
     @Test
-    @Ignore("Flaky")
-    public void testMagnetToDismiss_dismiss() throws InterruptedException {
-        final Runnable after = Mockito.mock(Runnable.class);
-
-        // Magnet to dismiss, verify the stack is at the dismiss target and the callback was
-        // called.
-        mStackController.magnetToDismiss(100 /* velX */, 100 /* velY */, 1000 /* destY */, after);
-        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
-        verify(after).run();
-        assertEquals(1000, mViews.get(0).getTranslationY(), .1f);
-
-        // Dismiss the stack, verify that the callback was called.
-        final Runnable afterImplode = Mockito.mock(Runnable.class);
-        mStackController.implodeStack(afterImplode);
-        waitForPropertyAnimations(
-                DynamicAnimation.ALPHA, DynamicAnimation.SCALE_X, DynamicAnimation.SCALE_Y);
-        verify(after).run();
-    }
-
-    @Test
-    @Ignore("Flaking")
-    public void testMagnetToDismiss_demagnetizeThenDrag() throws InterruptedException {
-        final Runnable after = Mockito.mock(Runnable.class);
-
-        // Magnet to dismiss, verify the stack is at the dismiss target and the callback was
-        // called.
-        mStackController.magnetToDismiss(100 /* velX */, 100 /* velY */, 1000 /* destY */, after);
-        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
-        verify(after).run();
-
-        assertEquals(1000, mViews.get(0).getTranslationY(), .1f);
-
-        // Demagnetize towards (25, 25) and then send a touch event.
-        mStackController.demagnetizeFromDismissToPoint(25, 25, 0, 0);
-        waitForLayoutMessageQueue();
-        mStackController.moveStackFromTouch(20, 20);
-
-        // Since the stack is demagnetizing, it shouldn't be at the stack position yet.
-        assertNotEquals(20, mStackController.getStackPosition().x, 1f);
-        assertNotEquals(20, mStackController.getStackPosition().y, 1f);
-
-        waitForPropertyAnimations(DynamicAnimation.TRANSLATION_X, DynamicAnimation.TRANSLATION_Y);
-
-        // Once the animation is done it should end at the touch position coordinates.
-        assertEquals(20, mStackController.getStackPosition().x, 1f);
-        assertEquals(20, mStackController.getStackPosition().y, 1f);
-
-        mStackController.moveStackFromTouch(30, 30);
-
-        // Touches after the animation are done should change the stack position instantly.
-        assertEquals(30, mStackController.getStackPosition().x, 1f);
-        assertEquals(30, mStackController.getStackPosition().y, 1f);
-    }
-
-    @Test
     public void testFloatingCoordinator() {
         // We should have called onContentAdded only once while adding all of the bubbles in
         // setup().
diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
index 45ea3c9..183dde8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/controls/controller/ControlsControllerImplTest.kt
@@ -408,7 +408,7 @@
     fun testDisableFeature_notAvailable() {
         Settings.Secure.putIntForUser(mContext.contentResolver,
                 ControlsControllerImpl.CONTROLS_AVAILABLE, 0, user)
-        controller.settingObserver.onChange(false, ControlsControllerImpl.URI, 0)
+        controller.settingObserver.onChange(false, listOf(ControlsControllerImpl.URI), 0, 0)
         assertFalse(controller.available)
     }
 
@@ -421,7 +421,7 @@
 
         Settings.Secure.putIntForUser(mContext.contentResolver,
                 ControlsControllerImpl.CONTROLS_AVAILABLE, 0, user)
-        controller.settingObserver.onChange(false, ControlsControllerImpl.URI, user)
+        controller.settingObserver.onChange(false, listOf(ControlsControllerImpl.URI), 0, user)
         assertTrue(controller.getFavorites().isEmpty())
     }
 
@@ -432,7 +432,7 @@
 
         Settings.Secure.putIntForUser(mContext.contentResolver,
                 ControlsControllerImpl.CONTROLS_AVAILABLE, 0, otherUser)
-        controller.settingObserver.onChange(false, ControlsControllerImpl.URI, otherUser)
+        controller.settingObserver.onChange(false, listOf(ControlsControllerImpl.URI), 0, otherUser)
 
         assertTrue(controller.available)
         assertFalse(controller.getFavorites().isEmpty())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/pip/PipAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/pip/PipAnimationControllerTest.java
index cd46110..a2ab784 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/pip/PipAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/pip/PipAnimationControllerTest.java
@@ -16,9 +16,10 @@
 
 package com.android.systemui.pip;
 
+import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_TO_FULLSCREEN;
+import static com.android.systemui.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP;
+
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.verify;
@@ -64,7 +65,7 @@
     @Test
     public void getAnimator_withAlpha_returnFloatAnimator() {
         final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController
-                .getAnimator(mLeash, true /* scheduleFinishPip */, new Rect(), 0f, 1f);
+                .getAnimator(mLeash, new Rect(), 0f, 1f);
 
         assertEquals("Expect ANIM_TYPE_ALPHA animation",
                 animator.getAnimationType(), PipAnimationController.ANIM_TYPE_ALPHA);
@@ -73,7 +74,7 @@
     @Test
     public void getAnimator_withBounds_returnBoundsAnimator() {
         final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController
-                .getAnimator(mLeash, true /* scheduleFinishPip */, new Rect(), new Rect());
+                .getAnimator(mLeash, new Rect(), new Rect());
 
         assertEquals("Expect ANIM_TYPE_BOUNDS animation",
                 animator.getAnimationType(), PipAnimationController.ANIM_TYPE_BOUNDS);
@@ -85,12 +86,12 @@
         final Rect endValue1 = new Rect(100, 100, 200, 200);
         final Rect endValue2 = new Rect(200, 200, 300, 300);
         final PipAnimationController.PipTransitionAnimator oldAnimator = mPipAnimationController
-                .getAnimator(mLeash, true /* scheduleFinishPip */, startValue, endValue1);
+                .getAnimator(mLeash, startValue, endValue1);
         oldAnimator.setSurfaceControlTransactionFactory(DummySurfaceControlTx::new);
         oldAnimator.start();
 
         final PipAnimationController.PipTransitionAnimator newAnimator = mPipAnimationController
-                .getAnimator(mLeash, true /* scheduleFinishPip */, startValue, endValue2);
+                .getAnimator(mLeash, startValue, endValue2);
 
         assertEquals("getAnimator with same type returns same animator",
                 oldAnimator, newAnimator);
@@ -99,23 +100,28 @@
     }
 
     @Test
-    public void getAnimator_scheduleFinishPip() {
+    public void getAnimator_setTransitionDirection() {
         PipAnimationController.PipTransitionAnimator animator = mPipAnimationController
-                .getAnimator(mLeash, true /* scheduleFinishPip */, new Rect(), 0f, 1f);
-        assertTrue("scheduleFinishPip is true", animator.shouldScheduleFinishPip());
+                .getAnimator(mLeash, new Rect(), 0f, 1f)
+                .setTransitionDirection(TRANSITION_DIRECTION_TO_PIP);
+        assertEquals("Transition to PiP mode",
+                animator.getTransitionDirection(), TRANSITION_DIRECTION_TO_PIP);
 
         animator = mPipAnimationController
-                .getAnimator(mLeash, false /* scheduleFinishPip */, new Rect(), 0f, 1f);
-        assertFalse("scheduleFinishPip is false", animator.shouldScheduleFinishPip());
+                .getAnimator(mLeash, new Rect(), 0f, 1f)
+                .setTransitionDirection(TRANSITION_DIRECTION_TO_FULLSCREEN);
+        assertEquals("Transition to fullscreen mode",
+                animator.getTransitionDirection(), TRANSITION_DIRECTION_TO_FULLSCREEN);
     }
 
     @Test
+    @SuppressWarnings("unchecked")
     public void pipTransitionAnimator_updateEndValue() {
         final Rect startValue = new Rect(0, 0, 100, 100);
         final Rect endValue1 = new Rect(100, 100, 200, 200);
         final Rect endValue2 = new Rect(200, 200, 300, 300);
         final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController
-                .getAnimator(mLeash, true /* scheduleFinishPip */, startValue, endValue1);
+                .getAnimator(mLeash, startValue, endValue1);
 
         animator.updateEndValue(endValue2);
 
@@ -127,7 +133,7 @@
         final Rect startValue = new Rect(0, 0, 100, 100);
         final Rect endValue = new Rect(100, 100, 200, 200);
         final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController
-                .getAnimator(mLeash, true /* scheduleFinishPip */, startValue, endValue);
+                .getAnimator(mLeash, startValue, endValue);
         animator.setSurfaceControlTransactionFactory(DummySurfaceControlTx::new);
 
         animator.setPipAnimationCallback(mPipAnimationCallback);
@@ -167,6 +173,11 @@
         }
 
         @Override
+        public SurfaceControl.Transaction setCornerRadius(SurfaceControl leash, float radius) {
+            return this;
+        }
+
+        @Override
         public void apply() {}
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java
index 45c0cdd..616399a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java
@@ -42,6 +42,7 @@
 import com.android.systemui.qs.customize.QSCustomizer;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
+import com.android.systemui.statusbar.NotificationMediaManager;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -53,6 +54,7 @@
 import java.io.PrintWriter;
 import java.io.StringWriter;
 import java.util.Collections;
+import java.util.concurrent.Executor;
 
 @RunWith(AndroidTestingRunner.class)
 @RunWithLooper
@@ -79,6 +81,10 @@
     private QSDetail.Callback mCallback;
     @Mock
     private QSTileView mQSTileView;
+    @Mock
+    private NotificationMediaManager mNotificationMediaManager;
+    @Mock
+    private Executor mBackgroundExecutor;
 
     @Before
     public void setup() throws Exception {
@@ -88,7 +94,7 @@
         mTestableLooper.runWithLooper(() -> {
             mMetricsLogger = mDependency.injectMockDependency(MetricsLogger.class);
             mQsPanel = new QSPanel(mContext, null, mDumpManager, mBroadcastDispatcher,
-                    mQSLogger);
+                    mQSLogger, mNotificationMediaManager, mBackgroundExecutor);
             // Provides a parent with non-zero size for QSPanel
             mParentView = new FrameLayout(mContext);
             mParentView.addView(mQsPanel);
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 84ce34b..4cc6590 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -94,6 +94,7 @@
         "services-stubs",
         "services.net",
         "android.hardware.light-V2.0-java",
+        "android.hardware.power-java",
         "android.hardware.power-V1.0-java",
         "android.hardware.tv.cec-V1.0-java",
         "android.hardware.vibrator-java",
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index 45c3aeb..7774633 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -138,6 +138,7 @@
 import android.util.Xml;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.Immutable;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.IAppOpsActiveCallback;
 import com.android.internal.app.IAppOpsAsyncNotedCallback;
@@ -155,11 +156,14 @@
 import com.android.server.LocalServices;
 import com.android.server.LockGuard;
 import com.android.server.SystemServerInitThreadPool;
+import com.android.server.SystemServiceManager;
 import com.android.server.pm.PackageList;
 import com.android.server.pm.parsing.pkg.AndroidPackage;
 
 import libcore.util.EmptyArray;
 
+import org.json.JSONException;
+import org.json.JSONObject;
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 import org.xmlpull.v1.XmlSerializer;
@@ -169,6 +173,7 @@
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
+import java.io.FileWriter;
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.nio.charset.StandardCharsets;
@@ -184,6 +189,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Scanner;
 import java.util.concurrent.ThreadLocalRandom;
 import java.util.function.Consumer;
 
@@ -191,6 +197,11 @@
     static final String TAG = "AppOps";
     static final boolean DEBUG = false;
 
+    /**
+     * Used for data access validation collection, we wish to only log a specific access once
+     */
+    private final ArraySet<NoteOpTrace> mNoteOpCallerStacktraces = new ArraySet<>();
+
     private static final int NO_VERSION = -1;
     /** Increment by one every time and add the corresponding upgrade logic in
      *  {@link #upgradeLocked(int)} below. The first version was 1 */
@@ -241,6 +252,7 @@
 
     final Context mContext;
     final AtomicFile mFile;
+    private final @Nullable File mNoteOpCallerStacktracesFile;
     final Handler mHandler;
 
     /** Pool for {@link OpEventProxyInfoPool} to avoid to constantly reallocate new objects */
@@ -278,6 +290,8 @@
     private final ArrayMap<Pair<String, Integer>, ArrayList<AsyncNotedAppOp>>
             mUnforwardedAsyncNotedOps = new ArrayMap<>();
 
+    boolean mWriteNoteOpsScheduled;
+
     boolean mWriteScheduled;
     boolean mFastWriteScheduled;
     final Runnable mWriteRunner = new Runnable() {
@@ -1397,11 +1411,42 @@
         featureOp.onClientDeath(clientId);
     }
 
+
+    /**
+     * Loads the OpsValidation file results into a hashmap {@link #mNoteOpCallerStacktraces}
+     * so that we do not log the same operation twice between instances
+     */
+    private void readNoteOpCallerStackTraces() {
+        try {
+            if (!mNoteOpCallerStacktracesFile.exists()) {
+                mNoteOpCallerStacktracesFile.createNewFile();
+                return;
+            }
+
+            try (Scanner read = new Scanner(mNoteOpCallerStacktracesFile)) {
+                read.useDelimiter("\\},");
+                while (read.hasNext()) {
+                    String jsonOps = read.next();
+                    mNoteOpCallerStacktraces.add(NoteOpTrace.fromJson(jsonOps));
+                }
+            }
+        } catch (Exception e) {
+            Slog.e(TAG, "Cannot parse traces noteOps", e);
+        }
+    }
+
     public AppOpsService(File storagePath, Handler handler, Context context) {
         mContext = context;
 
         LockGuard.installLock(this, LockGuard.INDEX_APP_OPS);
         mFile = new AtomicFile(storagePath, "appops");
+        if (AppOpsManager.NOTE_OP_COLLECTION_ENABLED) {
+            mNoteOpCallerStacktracesFile = new File(SystemServiceManager.ensureSystemDir(),
+                    "noteOpStackTraces.json");
+            readNoteOpCallerStackTraces();
+        } else {
+            mNoteOpCallerStacktracesFile = null;
+        }
         mHandler = handler;
         mConstants = new Constants(mHandler);
         readState();
@@ -1802,6 +1847,9 @@
         if (doWrite) {
             writeState();
         }
+        if (AppOpsManager.NOTE_OP_COLLECTION_ENABLED && mWriteNoteOpsScheduled) {
+            writeNoteOps();
+        }
     }
 
     private ArrayList<AppOpsManager.OpEntry> collectOps(Ops pkgOps, int[] ops) {
@@ -6051,4 +6099,142 @@
             setMode(code, uid, packageName, mode, callback);
         }
     }
+
+
+    /**
+     * Async task for writing note op stack trace, op code, package name and version to file
+     * More specifically, writes all the collected ops from {@link #mNoteOpCallerStacktraces}
+     */
+    private void writeNoteOps() {
+        synchronized (this) {
+            mWriteNoteOpsScheduled = false;
+        }
+        synchronized (mNoteOpCallerStacktracesFile) {
+            try (FileWriter writer = new FileWriter(mNoteOpCallerStacktracesFile)) {
+                int numTraces = mNoteOpCallerStacktraces.size();
+                for (int i = 0; i < numTraces; i++) {
+                    // Writing json formatted string into file
+                    writer.write(mNoteOpCallerStacktraces.valueAt(i).asJson());
+                    // Comma separation, so we can wrap the entire log as a JSON object
+                    // when all results are collected
+                    writer.write(",");
+                }
+            } catch (IOException e) {
+                Slog.w(TAG, "Failed to load opsValidation file for FileWriter", e);
+            }
+        }
+    }
+
+    /**
+     * This class represents a NoteOp Trace object amd contains the necessary fields that will
+     * be written to file to use for permissions data validation in JSON format
+     */
+    @Immutable
+    static class NoteOpTrace {
+        static final String STACKTRACE = "stackTrace";
+        static final String OP = "op";
+        static final String PACKAGENAME = "packageName";
+        static final String VERSION = "version";
+
+        private final @NonNull String mStackTrace;
+        private final int mOp;
+        private final @Nullable String mPackageName;
+        private final long mVersion;
+
+        /**
+         * Initialize a NoteOp object using a JSON object containing the necessary fields
+         *
+         * @param jsonTrace JSON object represented as a string
+         *
+         * @return NoteOpTrace object initialized with JSON fields
+         */
+        static NoteOpTrace fromJson(String jsonTrace) {
+            try {
+                // Re-add closing bracket which acted as a delimiter by the reader
+                JSONObject obj = new JSONObject(jsonTrace.concat("}"));
+                return new NoteOpTrace(obj.getString(STACKTRACE), obj.getInt(OP),
+                        obj.getString(PACKAGENAME), obj.getLong(VERSION));
+            } catch (JSONException e) {
+                // Swallow error, only meant for logging ops, should not affect flow of the code
+                Slog.e(TAG, "Error constructing NoteOpTrace object "
+                        + "JSON trace format incorrect", e);
+                return null;
+            }
+        }
+
+        NoteOpTrace(String stackTrace, int op, String packageName, long version) {
+            mStackTrace = stackTrace;
+            mOp = op;
+            mPackageName = packageName;
+            mVersion = version;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            NoteOpTrace that = (NoteOpTrace) o;
+            return mOp == that.mOp
+                    && mVersion == that.mVersion
+                    && mStackTrace.equals(that.mStackTrace)
+                    && Objects.equals(mPackageName, that.mPackageName);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mStackTrace, mOp, mPackageName, mVersion);
+        }
+
+        /**
+         * The object is formatted as a JSON object and returned as a String
+         *
+         * @return JSON formatted string
+         */
+        public String asJson() {
+            return  "{"
+                    + "\"" + STACKTRACE + "\":\"" + mStackTrace.replace("\n", "\\n")
+                    + '\"' + ",\"" + OP + "\":" + mOp
+                    + ",\"" + PACKAGENAME + "\":\"" + mPackageName + '\"'
+                    + ",\"" + VERSION + "\":" + mVersion
+                    + '}';
+        }
+    }
+
+    /**
+     * Collects noteOps, noteProxyOps and startOps from AppOpsManager and writes it into a file
+     * which will be used for permissions data validation, the given parameters to this method
+     * will be logged in json format
+     *
+     * @param stackTrace stacktrace from the most recent call in AppOpsManager
+     * @param op op code
+     * @param packageName package making call
+     * @param version android version for this call
+     */
+    @Override
+    public void collectNoteOpCallsForValidation(String stackTrace, int op, String packageName,
+            long version) {
+        if (!AppOpsManager.NOTE_OP_COLLECTION_ENABLED) {
+            return;
+        }
+
+        Objects.requireNonNull(stackTrace);
+        Preconditions.checkArgument(op >= 0);
+        Preconditions.checkArgument(op < AppOpsManager._NUM_OP);
+        Objects.requireNonNull(version);
+
+        NoteOpTrace noteOpTrace = new NoteOpTrace(stackTrace, op, packageName, version);
+
+        boolean noteOpSetWasChanged;
+        synchronized (this) {
+            noteOpSetWasChanged = mNoteOpCallerStacktraces.add(noteOpTrace);
+            if (noteOpSetWasChanged && !mWriteNoteOpsScheduled) {
+                mWriteNoteOpsScheduled = true;
+                mHandler.postDelayed(PooledLambda.obtainRunnable((that) -> {
+                    AsyncTask.execute(() -> {
+                        that.writeNoteOps();
+                    });
+                }, this), 2500);
+            }
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java
index 858c157..ecdafb0 100644
--- a/services/core/java/com/android/server/biometrics/BiometricService.java
+++ b/services/core/java/com/android/server/biometrics/BiometricService.java
@@ -1138,9 +1138,10 @@
 
             biometricStatus = result.second;
 
-            Slog.d(TAG, "Authenticator ID: " + authenticator.id
+            Slog.d(TAG, "Package: " + opPackageName
+                    + " Authenticator ID: " + authenticator.id
                     + " Modality: " + authenticator.modality
-                    + " ReportedModality: " + result.first
+                    + " Reported Modality: " + result.first
                     + " Status: " + biometricStatus);
 
             if (firstBiometricModality == TYPE_NONE) {
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index f04be0b..294deba 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -42,6 +42,8 @@
 import android.hardware.display.AmbientDisplayConfiguration;
 import android.hardware.display.DisplayManagerInternal;
 import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest;
+import android.hardware.power.Boost;
+import android.hardware.power.Mode;
 import android.hardware.power.V1_0.PowerHint;
 import android.net.Uri;
 import android.os.BatteryManager;
@@ -732,6 +734,16 @@
             PowerManagerService.nativeSendPowerHint(hintId, data);
         }
 
+        /** Wrapper for PowerManager.nativeSetPowerBoost */
+        public void nativeSetPowerBoost(int boost, int durationMs) {
+            PowerManagerService.nativeSetPowerBoost(boost, durationMs);
+        }
+
+        /** Wrapper for PowerManager.nativeSetPowerMode */
+        public void nativeSetPowerMode(int mode, boolean enabled) {
+            PowerManagerService.nativeSetPowerMode(mode, enabled);
+        }
+
         /** Wrapper for PowerManager.nativeSetFeature */
         public void nativeSetFeature(int featureId, int data) {
             PowerManagerService.nativeSetFeature(featureId, data);
@@ -817,6 +829,8 @@
     private static native void nativeSetInteractive(boolean enable);
     private static native void nativeSetAutoSuspend(boolean enable);
     private static native void nativeSendPowerHint(int hintId, int data);
+    private static native void nativeSetPowerBoost(int boost, int durationMs);
+    private static native void nativeSetPowerMode(int mode, boolean enabled);
     private static native void nativeSetFeature(int featureId, int data);
     private static native boolean nativeForceSuspend();
 
@@ -3608,6 +3622,16 @@
         mNativeWrapper.nativeSendPowerHint(hintId, data);
     }
 
+    private void setPowerBoostInternal(int boost, int durationMs) {
+        // Maybe filter the event.
+        mNativeWrapper.nativeSetPowerBoost(boost, durationMs);
+    }
+
+    private void setPowerModeInternal(int mode, boolean enabled) {
+        // Maybe filter the event.
+        mNativeWrapper.nativeSetPowerMode(mode, enabled);
+    }
+
     @VisibleForTesting
     boolean wasDeviceIdleForInternal(long ms) {
         synchronized (mLock) {
@@ -4664,6 +4688,26 @@
         }
 
         @Override // Binder call
+        public void setPowerBoost(int boost, int durationMs) {
+            if (!mSystemReady) {
+                // Service not ready yet, so who the heck cares about power hints, bah.
+                return;
+            }
+            mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, null);
+            setPowerBoostInternal(boost, durationMs);
+        }
+
+        @Override // Binder call
+        public void setPowerMode(int mode, boolean enabled) {
+            if (!mSystemReady) {
+                // Service not ready yet, so who the heck cares about power hints, bah.
+                return;
+            }
+            mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DEVICE_POWER, null);
+            setPowerModeInternal(mode, enabled);
+        }
+
+        @Override // Binder call
         public void acquireWakeLock(IBinder lock, int flags, String tag, String packageName,
                 WorkSource ws, String historyTag) {
             if (lock == null) {
@@ -5457,6 +5501,15 @@
         }
 
         @Override
+        public void setPowerBoost(int boost, int durationMs) {
+            setPowerBoostInternal(boost, durationMs);
+        }
+
+        @Override
+        public void setPowerMode(int mode, boolean enabled) {
+            setPowerModeInternal(mode, enabled);
+        }
+       @Override
         public boolean wasDeviceIdleFor(long ms) {
             return wasDeviceIdleForInternal(ms);
         }
diff --git a/services/core/java/com/android/server/tv/tunerresourcemanager/ClientProfile.java b/services/core/java/com/android/server/tv/tunerresourcemanager/ClientProfile.java
index 4eff954f..e100ff8 100644
--- a/services/core/java/com/android/server/tv/tunerresourcemanager/ClientProfile.java
+++ b/services/core/java/com/android/server/tv/tunerresourcemanager/ClientProfile.java
@@ -15,8 +15,8 @@
  */
 package com.android.server.tv.tunerresourcemanager;
 
-import java.util.ArrayList;
-import java.util.List;
+import java.util.HashSet;
+import java.util.Set;
 
 /**
   * A client profile object used by the Tuner Resource Manager to record the registered clients'
@@ -65,7 +65,7 @@
     /**
      * List of the frontend ids that are used by the current client.
      */
-    private List<Integer> mUsingFrontendIds = new ArrayList<>();
+    private Set<Integer> mUsingFrontendIds = new HashSet<>();
 
     /**
      * Optional arbitrary priority value given by the client.
@@ -131,7 +131,7 @@
         mUsingFrontendIds.add(frontendId);
     }
 
-    public List<Integer> getInUseFrontendIds() {
+    public Iterable<Integer> getInUseFrontendIds() {
         return mUsingFrontendIds;
     }
 
diff --git a/services/core/java/com/android/server/tv/tunerresourcemanager/FrontendResource.java b/services/core/java/com/android/server/tv/tunerresourcemanager/FrontendResource.java
index a109265..56f6159 100644
--- a/services/core/java/com/android/server/tv/tunerresourcemanager/FrontendResource.java
+++ b/services/core/java/com/android/server/tv/tunerresourcemanager/FrontendResource.java
@@ -15,12 +15,11 @@
  */
 package com.android.server.tv.tunerresourcemanager;
 
-import android.annotation.Nullable;
 import android.media.tv.tuner.frontend.FrontendSettings.Type;
 
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
 
 /**
  * A frontend resource object used by the Tuner Resource Manager to record the tuner frontend
@@ -50,7 +49,7 @@
     /**
      * An array to save all the FE ids under the same exclisive group.
      */
-    private List<Integer> mExclusiveGroupMemberFeIds = new ArrayList<>();
+    private Set<Integer> mExclusiveGroupMemberFeIds = new HashSet<>();
 
     /**
      * If the current resource is in use. Once resources under the same exclusive group id is in use
@@ -82,12 +81,12 @@
         return mExclusiveGroupId;
     }
 
-    public List<Integer> getExclusiveGroupMemberFeIds() {
+    public Set<Integer> getExclusiveGroupMemberFeIds() {
         return mExclusiveGroupMemberFeIds;
     }
 
     /**
-     * Add one id into the exclusive group member id list.
+     * Add one id into the exclusive group member id collection.
      *
      * @param id the id to be added.
      */
@@ -96,21 +95,21 @@
     }
 
     /**
-     * Add one id list to the exclusive group member id list.
+     * Add one id collection to the exclusive group member id collection.
      *
-     * @param ids the id list to be added.
+     * @param ids the id collection to be added.
      */
-    public void addExclusiveGroupMemberFeId(List<Integer> ids) {
+    public void addExclusiveGroupMemberFeIds(Collection<Integer> ids) {
         mExclusiveGroupMemberFeIds.addAll(ids);
     }
 
     /**
-     * Remove one id from the exclusive group member id list.
+     * Remove one id from the exclusive group member id collection.
      *
      * @param id the id to be removed.
      */
     public void removeExclusiveGroupMemberFeId(int id) {
-        mExclusiveGroupMemberFeIds.remove(new Integer(id));
+        mExclusiveGroupMemberFeIds.remove(id);
     }
 
     public boolean isInUse() {
@@ -143,22 +142,10 @@
     public String toString() {
         return "FrontendResource[id=" + this.mId + ", type=" + this.mType
                 + ", exclusiveGId=" + this.mExclusiveGroupId + ", exclusiveGMemeberIds="
-                + Arrays.toString(this.mExclusiveGroupMemberFeIds.toArray())
+                + this.mExclusiveGroupMemberFeIds
                 + ", isInUse=" + this.mIsInUse + ", ownerClientId=" + this.mOwnerClientId + "]";
     }
 
-    @Override
-    public boolean equals(@Nullable Object o) {
-        if (o instanceof FrontendResource) {
-            FrontendResource fe = (FrontendResource) o;
-            return mId == fe.getId() && mType == fe.getType()
-                    && mExclusiveGroupId == fe.getExclusiveGroupId()
-                    && mExclusiveGroupMemberFeIds.equals(fe.getExclusiveGroupMemberFeIds())
-                    && mIsInUse == fe.isInUse() && mOwnerClientId == fe.getOwnerClientId();
-        }
-        return false;
-    }
-
     /**
      * Builder class for {@link FrontendResource}.
      */
diff --git a/services/core/java/com/android/server/tv/tunerresourcemanager/TunerResourceManagerService.java b/services/core/java/com/android/server/tv/tunerresourcemanager/TunerResourceManagerService.java
index cb31a50..04d551d 100644
--- a/services/core/java/com/android/server/tv/tunerresourcemanager/TunerResourceManagerService.java
+++ b/services/core/java/com/android/server/tv/tunerresourcemanager/TunerResourceManagerService.java
@@ -29,16 +29,19 @@
 import android.media.tv.tunerresourcemanager.TunerLnbRequest;
 import android.media.tv.tunerresourcemanager.TunerResourceManager;
 import android.os.Binder;
+import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Log;
 import android.util.Slog;
-import android.util.SparseArray;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.SystemService;
 
-import java.util.ArrayList;
-import java.util.List;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
 
 /**
  * This class provides a system service that manages the TV tuner resources.
@@ -52,18 +55,15 @@
     public static final int INVALID_CLIENT_ID = -1;
     private static final int MAX_CLIENT_PRIORITY = 1000;
 
-    // Array of the registered client profiles
-    @VisibleForTesting private SparseArray<ClientProfile> mClientProfiles = new SparseArray<>();
+    // Map of the registered client profiles
+    private Map<Integer, ClientProfile> mClientProfiles = new HashMap<>();
     private int mNextUnusedClientId = 0;
-    private List<Integer> mRegisteredClientIds = new ArrayList<Integer>();
 
-    // Array of the current available frontend resources
-    @VisibleForTesting
-    private SparseArray<FrontendResource> mFrontendResources = new SparseArray<>();
-    // Array of the current available frontend ids
-    private List<Integer> mAvailableFrontendIds = new ArrayList<Integer>();
+    // Map of the current available frontend resources
+    private Map<Integer, FrontendResource> mFrontendResources = new HashMap<>();
 
-    private SparseArray<IResourcesReclaimListener> mListeners = new SparseArray<>();
+    @GuardedBy("mLock")
+    private Map<Integer, ResourcesReclaimListenerRecord> mListeners = new HashMap<>();
 
     private TvInputManager mManager;
     private UseCasePriorityHints mPriorityCongfig = new UseCasePriorityHints();
@@ -103,6 +103,10 @@
                 throw new RemoteException("clientId can't be null!");
             }
 
+            if (listener == null) {
+                throw new RemoteException("IResourcesReclaimListener can't be null!");
+            }
+
             if (!mPriorityCongfig.isDefinedUseCase(profile.getUseCase())) {
                 throw new RemoteException("Use undefined client use case:" + profile.getUseCase());
             }
@@ -261,9 +265,7 @@
                                               .build();
         clientProfile.setPriority(getClientPriority(profile.getUseCase(), pid));
 
-        mClientProfiles.append(clientId[0], clientProfile);
-        mListeners.append(clientId[0], listener);
-        mRegisteredClientIds.add(clientId[0]);
+        addClientProfile(clientId[0], clientProfile, listener);
     }
 
     @VisibleForTesting
@@ -271,15 +273,7 @@
         if (DEBUG) {
             Slog.d(TAG, "unregisterClientProfile(clientId=" + clientId + ")");
         }
-        for (int id : getClientProfile(clientId).getInUseFrontendIds()) {
-            getFrontendResource(id).removeOwner();
-            for (int groupMemberId : getFrontendResource(id).getExclusiveGroupMemberFeIds()) {
-                getFrontendResource(groupMemberId).removeOwner();
-            }
-        }
-        mClientProfiles.remove(clientId);
-        mListeners.remove(clientId);
-        mRegisteredClientIds.remove(clientId);
+        removeClientProfile(clientId);
     }
 
     @VisibleForTesting
@@ -313,56 +307,32 @@
             }
         }
 
-        // An arrayList to record the frontends pending on updating. Ids will be removed
-        // from this list once its updating finished. Any frontend left in this list when all
-        // the updates are done will be removed from mAvailableFrontendIds and
-        // mFrontendResources.
-        List<Integer> updatingFrontendIds = new ArrayList<>(mAvailableFrontendIds);
+        // A set to record the frontends pending on updating. Ids will be removed
+        // from this set once its updating finished. Any frontend left in this set when all
+        // the updates are done will be removed from mFrontendResources.
+        Set<Integer> updatingFrontendIds = new HashSet<>(getFrontendResources().keySet());
 
-        // Update frontendResources sparse array and other mappings accordingly
+        // Update frontendResources map and other mappings accordingly
         for (int i = 0; i < infos.length; i++) {
             if (getFrontendResource(infos[i].getId()) != null) {
                 if (DEBUG) {
                     Slog.d(TAG, "Frontend id=" + infos[i].getId() + "exists.");
                 }
-                updatingFrontendIds.remove(new Integer(infos[i].getId()));
+                updatingFrontendIds.remove(infos[i].getId());
             } else {
                 // Add a new fe resource
                 FrontendResource newFe = new FrontendResource.Builder(infos[i].getId())
                                                  .type(infos[i].getFrontendType())
                                                  .exclusiveGroupId(infos[i].getExclusiveGroupId())
                                                  .build();
-                // Update the exclusive group member list in all the existing Frontend resource
-                for (Integer feId : mAvailableFrontendIds) {
-                    FrontendResource fe = getFrontendResource(feId.intValue());
-                    if (fe.getExclusiveGroupId() == newFe.getExclusiveGroupId()) {
-                        newFe.addExclusiveGroupMemberFeId(fe.getId());
-                        newFe.addExclusiveGroupMemberFeId(fe.getExclusiveGroupMemberFeIds());
-                        for (Integer excGroupmemberFeId : fe.getExclusiveGroupMemberFeIds()) {
-                            getFrontendResource(excGroupmemberFeId.intValue())
-                                    .addExclusiveGroupMemberFeId(newFe.getId());
-                        }
-                        fe.addExclusiveGroupMemberFeId(newFe.getId());
-                        break;
-                    }
-                }
-                // Update resource list and available id list
-                mFrontendResources.append(newFe.getId(), newFe);
-                mAvailableFrontendIds.add(newFe.getId());
+                addFrontendResource(newFe);
             }
         }
 
         // TODO check if the removing resource is in use or not. Handle the conflict.
-        for (Integer removingId : updatingFrontendIds) {
-            // update the exclusive group id memver list
-            FrontendResource fe = getFrontendResource(removingId.intValue());
-            fe.removeExclusiveGroupMemberFeId(new Integer(fe.getId()));
-            for (Integer excGroupmemberFeId : fe.getExclusiveGroupMemberFeIds()) {
-                getFrontendResource(excGroupmemberFeId.intValue())
-                        .removeExclusiveGroupMemberFeId(new Integer(fe.getId()));
-            }
-            mFrontendResources.remove(removingId.intValue());
-            mAvailableFrontendIds.remove(removingId);
+        for (int removingId : updatingFrontendIds) {
+            // update the exclusive group id member list
+            removeFrontendResource(removingId);
         }
     }
 
@@ -383,25 +353,24 @@
         int inUseLowestPriorityFrId = -1;
         // Priority max value is 1000
         int currentLowestPriority = MAX_CLIENT_PRIORITY + 1;
-        for (int id : mAvailableFrontendIds) {
-            FrontendResource fr = getFrontendResource(id);
+        for (FrontendResource fr : getFrontendResources().values()) {
             if (fr.getType() == request.getFrontendType()) {
                 if (!fr.isInUse()) {
                     // Grant unused frontend with no exclusive group members first.
-                    if (fr.getExclusiveGroupMemberFeIds().size() == 0) {
-                        grantingFrontendId = id;
+                    if (fr.getExclusiveGroupMemberFeIds().isEmpty()) {
+                        grantingFrontendId = fr.getId();
                         break;
                     } else if (grantingFrontendId < 0) {
                         // Grant the unused frontend with lower id first if all the unused
                         // frontends have exclusive group members.
-                        grantingFrontendId = id;
+                        grantingFrontendId = fr.getId();
                     }
                 } else if (grantingFrontendId < 0) {
                     // Record the frontend id with the lowest client priority among all the
                     // in use frontends when no available frontend has been found.
-                    int priority = getOwnerClientPriority(id);
+                    int priority = getOwnerClientPriority(fr);
                     if (currentLowestPriority > priority) {
-                        inUseLowestPriorityFrId = id;
+                        inUseLowestPriorityFrId = fr.getId();
                         currentLowestPriority = priority;
                     }
                 }
@@ -428,6 +397,62 @@
     }
 
     @VisibleForTesting
+    protected class ResourcesReclaimListenerRecord implements IBinder.DeathRecipient {
+        private final IResourcesReclaimListener mListener;
+        private final int mClientId;
+
+        public ResourcesReclaimListenerRecord(IResourcesReclaimListener listener, int clientId) {
+            mListener = listener;
+            mClientId = clientId;
+        }
+
+        @Override
+        public void binderDied() {
+            synchronized (mLock) {
+                removeClientProfile(mClientId);
+            }
+        }
+
+        public int getId() {
+            return mClientId;
+        }
+
+        public IResourcesReclaimListener getListener() {
+            return mListener;
+        }
+    }
+
+    private void addResourcesReclaimListener(int clientId, IResourcesReclaimListener listener) {
+        if (listener == null) {
+            if (DEBUG) {
+                Slog.w(TAG, "Listener is null when client " + clientId + " registered!");
+            }
+            return;
+        }
+
+        ResourcesReclaimListenerRecord record =
+                new ResourcesReclaimListenerRecord(listener, clientId);
+
+        try {
+            listener.asBinder().linkToDeath(record, 0);
+        } catch (RemoteException e) {
+            Slog.w(TAG, "Listener already died.");
+            return;
+        }
+
+        mListeners.put(clientId, record);
+    }
+
+    @VisibleForTesting
+    protected void reclaimFrontendResource(int reclaimingId) {
+        try {
+            mListeners.get(reclaimingId).getListener().onReclaimResources();
+        } catch (RemoteException e) {
+            Slog.e(TAG, "Failed to reclaim resources on client " + reclaimingId, e);
+        }
+    }
+
+    @VisibleForTesting
     protected int getClientPriority(int useCase, int pid) {
         if (DEBUG) {
             Slog.d(TAG, "getClientPriority useCase=" + useCase
@@ -446,17 +471,6 @@
         return true;
     }
 
-    @VisibleForTesting
-    protected void reclaimFrontendResource(int reclaimingId) throws RemoteException {
-        if (mListeners.get(reclaimingId) != null) {
-            try {
-                mListeners.get(reclaimingId).onReclaimResources();
-            } catch (RemoteException e) {
-                throw e.rethrowFromSystemServer();
-            }
-        }
-    }
-
     private void updateFrontendClientMappingOnNewGrant(int grantingId, int ownerClientId) {
         FrontendResource grantingFrontend = getFrontendResource(grantingId);
         ClientProfile ownerProfile = getClientProfile(ownerClientId);
@@ -471,33 +485,77 @@
     /**
      * Get the owner client's priority from the frontend id.
      *
-     * @param frontendId an in use frontend id.
+     * @param frontend an in use frontend.
      * @return the priority of the owner client of the frontend.
      */
-    private int getOwnerClientPriority(int frontendId) {
-        return getClientProfile(getFrontendResource(frontendId).getOwnerClientId()).getPriority();
+    private int getOwnerClientPriority(FrontendResource frontend) {
+        return getClientProfile(frontend.getOwnerClientId()).getPriority();
     }
 
-    private ClientProfile getClientProfile(int clientId) {
-        return mClientProfiles.get(clientId);
-    }
-
+    @VisibleForTesting
+    @Nullable
     protected FrontendResource getFrontendResource(int frontendId) {
         return mFrontendResources.get(frontendId);
     }
 
     @VisibleForTesting
-    protected SparseArray<ClientProfile> getClientProfiles() {
-        return mClientProfiles;
-    }
-
-    @VisibleForTesting
-    protected SparseArray<FrontendResource> getFrontendResources() {
+    protected Map<Integer, FrontendResource> getFrontendResources() {
         return mFrontendResources;
     }
 
-    private boolean checkClientExists(int clientId) {
-        return mRegisteredClientIds.contains(clientId);
+    private void addFrontendResource(FrontendResource newFe) {
+        // Update the exclusive group member list in all the existing Frontend resource
+        for (FrontendResource fe : getFrontendResources().values()) {
+            if (fe.getExclusiveGroupId() == newFe.getExclusiveGroupId()) {
+                newFe.addExclusiveGroupMemberFeId(fe.getId());
+                newFe.addExclusiveGroupMemberFeIds(fe.getExclusiveGroupMemberFeIds());
+                for (int excGroupmemberFeId : fe.getExclusiveGroupMemberFeIds()) {
+                    getFrontendResource(excGroupmemberFeId)
+                            .addExclusiveGroupMemberFeId(newFe.getId());
+                }
+                fe.addExclusiveGroupMemberFeId(newFe.getId());
+                break;
+            }
+        }
+        // Update resource list and available id list
+        mFrontendResources.put(newFe.getId(), newFe);
+    }
+
+    private void removeFrontendResource(int removingId) {
+        FrontendResource fe = getFrontendResource(removingId);
+        for (int excGroupmemberFeId : fe.getExclusiveGroupMemberFeIds()) {
+            getFrontendResource(excGroupmemberFeId)
+                    .removeExclusiveGroupMemberFeId(fe.getId());
+        }
+        mFrontendResources.remove(removingId);
+    }
+
+    @VisibleForTesting
+    @Nullable
+    protected ClientProfile getClientProfile(int clientId) {
+        return mClientProfiles.get(clientId);
+    }
+
+    private void addClientProfile(int clientId, ClientProfile profile,
+            IResourcesReclaimListener listener) {
+        mClientProfiles.put(clientId, profile);
+        addResourcesReclaimListener(clientId, listener);
+    }
+
+    private void removeClientProfile(int clientId) {
+        for (int id : getClientProfile(clientId).getInUseFrontendIds()) {
+            getFrontendResource(id).removeOwner();
+            for (int groupMemberId : getFrontendResource(id).getExclusiveGroupMemberFeIds()) {
+                getFrontendResource(groupMemberId).removeOwner();
+            }
+        }
+        mClientProfiles.remove(clientId);
+        mListeners.remove(clientId);
+    }
+
+    @VisibleForTesting
+    protected boolean checkClientExists(int clientId) {
+        return mClientProfiles.keySet().contains(clientId);
     }
 
     private void enforceAccessPermission() {
diff --git a/services/core/java/com/android/server/tv/tunerresourcemanager/UseCasePriorityHints.java b/services/core/java/com/android/server/tv/tunerresourcemanager/UseCasePriorityHints.java
index 8c2de47..367b966 100644
--- a/services/core/java/com/android/server/tv/tunerresourcemanager/UseCasePriorityHints.java
+++ b/services/core/java/com/android/server/tv/tunerresourcemanager/UseCasePriorityHints.java
@@ -31,8 +31,8 @@
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.HashSet;
+import java.util.Set;
 
 /**
  * This class provides the Tuner Resource Manager use case priority hints config info including a
@@ -56,7 +56,7 @@
      */
     SparseArray<int[]> mPriorityHints = new SparseArray<>();
 
-    List<Integer> mVendorDefinedUseCase = new ArrayList<>();
+    Set<Integer> mVendorDefinedUseCase = new HashSet<>();
 
     private int mDefaultForeground = 150;
     private int mDefaultBackground = 50;
diff --git a/services/core/jni/Android.bp b/services/core/jni/Android.bp
index c781a5a..74982c6 100644
--- a/services/core/jni/Android.bp
+++ b/services/core/jni/Android.bp
@@ -146,6 +146,7 @@
         "android.hardware.light@2.0",
         "android.hardware.power@1.0",
         "android.hardware.power@1.1",
+        "android.hardware.power-cpp",
         "android.hardware.power.stats@1.0",
         "android.hardware.thermal@1.0",
         "android.hardware.tv.cec@1.0",
diff --git a/services/core/jni/com_android_server_am_BatteryStatsService.cpp b/services/core/jni/com_android_server_am_BatteryStatsService.cpp
index 9cbb58d..b08868e 100644
--- a/services/core/jni/com_android_server_am_BatteryStatsService.cpp
+++ b/services/core/jni/com_android_server_am_BatteryStatsService.cpp
@@ -67,9 +67,9 @@
 
 static bool wakeup_init = false;
 static sem_t wakeup_sem;
-extern sp<IPowerV1_0> getPowerHalV1_0();
-extern sp<IPowerV1_1> getPowerHalV1_1();
-extern bool processPowerHalReturn(const Return<void> &ret, const char* functionName);
+extern sp<IPowerV1_0> getPowerHalHidlV1_0();
+extern sp<IPowerV1_1> getPowerHalHidlV1_1();
+extern bool processPowerHalReturn(bool isOk, const char* functionName);
 extern sp<ISuspendControlService> getSuspendControl();
 
 // Java methods used in getLowPowerStats
@@ -596,7 +596,7 @@
 
 // The caller must be holding powerHalMutex.
 static void getPowerHalLowPowerData(JNIEnv* env, jobject jrpmStats) {
-    sp<IPowerV1_0> powerHalV1_0 = getPowerHalV1_0();
+    sp<IPowerV1_0> powerHalV1_0 = getPowerHalHidlV1_0();
     if (powerHalV1_0 == nullptr) {
         ALOGE("Power Hal not loaded");
         return;
@@ -629,12 +629,12 @@
                 }
             }
     });
-    if (!processPowerHalReturn(ret, "getPlatformLowPowerStats")) {
+    if (!processPowerHalReturn(ret.isOk(), "getPlatformLowPowerStats")) {
         return;
     }
 
     // Trying to get IPower 1.1, this will succeed only for devices supporting 1.1
-    sp<IPowerV1_1> powerHal_1_1 = getPowerHalV1_1();
+    sp<IPowerV1_1> powerHal_1_1 = getPowerHalHidlV1_1();
     if (powerHal_1_1 == nullptr) {
         // This device does not support IPower@1.1, exiting gracefully
         return;
@@ -665,7 +665,7 @@
             }
         }
     });
-    processPowerHalReturn(ret, "getSubsystemLowPowerStats");
+    processPowerHalReturn(ret.isOk(), "getSubsystemLowPowerStats");
 }
 
 static jint getPowerHalPlatformData(JNIEnv* env, jobject outBuf) {
@@ -675,7 +675,7 @@
     int total_added = -1;
 
     {
-        sp<IPowerV1_0> powerHalV1_0 = getPowerHalV1_0();
+        sp<IPowerV1_0> powerHalV1_0 = getPowerHalHidlV1_0();
         if (powerHalV1_0 == nullptr) {
             ALOGE("Power Hal not loaded");
             return -1;
@@ -733,7 +733,7 @@
             }
         );
 
-        if (!processPowerHalReturn(ret, "getPlatformLowPowerStats")) {
+        if (!processPowerHalReturn(ret.isOk(), "getPlatformLowPowerStats")) {
             return -1;
         }
     }
@@ -753,7 +753,7 @@
 
     {
         // Trying to get 1.1, this will succeed only for devices supporting 1.1
-        powerHal_1_1 = getPowerHalV1_1();
+        powerHal_1_1 = getPowerHalHidlV1_1();
         if (powerHal_1_1 == nullptr) {
             //This device does not support IPower@1.1, exiting gracefully
             return 0;
@@ -820,7 +820,7 @@
         }
         );
 
-        if (!processPowerHalReturn(ret, "getSubsystemLowPowerStats")) {
+        if (!processPowerHalReturn(ret.isOk(), "getSubsystemLowPowerStats")) {
             return -1;
         }
     }
diff --git a/services/core/jni/com_android_server_power_PowerManagerService.cpp b/services/core/jni/com_android_server_power_PowerManagerService.cpp
index 4e04348..239a101 100644
--- a/services/core/jni/com_android_server_power_PowerManagerService.cpp
+++ b/services/core/jni/com_android_server_power_PowerManagerService.cpp
@@ -19,6 +19,9 @@
 //#define LOG_NDEBUG 0
 
 #include <android/hardware/power/1.1/IPower.h>
+#include <android/hardware/power/Boost.h>
+#include <android/hardware/power/IPower.h>
+#include <android/hardware/power/Mode.h>
 #include <android/system/suspend/1.0/ISystemSuspend.h>
 #include <android/system/suspend/ISuspendControlService.h>
 #include <nativehelper/JNIHelp.h>
@@ -45,6 +48,8 @@
 
 using android::hardware::Return;
 using android::hardware::Void;
+using android::hardware::power::Boost;
+using android::hardware::power::Mode;
 using android::hardware::power::V1_0::PowerHint;
 using android::hardware::power::V1_0::Feature;
 using android::String8;
@@ -54,6 +59,7 @@
 using android::system::suspend::ISuspendControlService;
 using IPowerV1_1 = android::hardware::power::V1_1::IPower;
 using IPowerV1_0 = android::hardware::power::V1_0::IPower;
+using IPowerAidl = android::hardware::power::IPower;
 
 namespace android {
 
@@ -66,11 +72,18 @@
 // ----------------------------------------------------------------------------
 
 static jobject gPowerManagerServiceObj;
-// Use getPowerHal* to retrieve a copy
-static sp<IPowerV1_0> gPowerHalV1_0_ = nullptr;
-static sp<IPowerV1_1> gPowerHalV1_1_ = nullptr;
-static bool gPowerHalExists = true;
+static sp<IPowerV1_0> gPowerHalHidlV1_0_ = nullptr;
+static sp<IPowerV1_1> gPowerHalHidlV1_1_ = nullptr;
+static sp<IPowerAidl> gPowerHalAidl_ = nullptr;
 static std::mutex gPowerHalMutex;
+
+enum class HalVersion {
+    NONE,
+    HIDL_1_0,
+    HIDL_1_1,
+    AIDL,
+};
+
 static nsecs_t gLastEventTime[USER_ACTIVITY_EVENT_LAST + 1];
 
 // Throttling interval for user activity calls.
@@ -88,68 +101,207 @@
     return false;
 }
 
-// Check validity of current handle to the power HAL service, and call getService() if necessary.
+// Check validity of current handle to the power HAL service, and connect to it if necessary.
 // The caller must be holding gPowerHalMutex.
-static void connectPowerHalLocked() {
-    if (gPowerHalExists && gPowerHalV1_0_ == nullptr) {
-        gPowerHalV1_0_ = IPowerV1_0::getService();
-        if (gPowerHalV1_0_ != nullptr) {
-            ALOGI("Loaded power HAL 1.0 service");
-            // Try cast to powerHAL V1_1
-            gPowerHalV1_1_ =  IPowerV1_1::castFrom(gPowerHalV1_0_);
-            if (gPowerHalV1_1_ == nullptr) {
-            } else {
-                ALOGI("Loaded power HAL 1.1 service");
-            }
+static HalVersion connectPowerHalLocked() {
+    static bool gPowerHalHidlExists = true;
+    static bool gPowerHalAidlExists = true;
+    if (!gPowerHalHidlExists && !gPowerHalAidlExists) {
+        return HalVersion::NONE;
+    }
+    if (gPowerHalAidlExists) {
+        if (!gPowerHalAidl_) {
+            gPowerHalAidl_ = waitForVintfService<IPowerAidl>();
+        }
+        if (gPowerHalAidl_) {
+            ALOGI("Successfully connected to Power HAL AIDL service.");
+            return HalVersion::AIDL;
         } else {
-            ALOGI("Couldn't load power HAL service");
-            gPowerHalExists = false;
+            gPowerHalAidlExists = false;
         }
     }
+    if (gPowerHalHidlExists && gPowerHalHidlV1_0_ == nullptr) {
+        gPowerHalHidlV1_0_ = IPowerV1_0::getService();
+        if (gPowerHalHidlV1_0_) {
+            ALOGI("Successfully connected to Power HAL HIDL 1.0 service.");
+            // Try cast to powerHAL HIDL V1_1
+            gPowerHalHidlV1_1_ = IPowerV1_1::castFrom(gPowerHalHidlV1_0_);
+            if (gPowerHalHidlV1_1_) {
+                ALOGI("Successfully connected to Power HAL HIDL 1.1 service.");
+            }
+        } else {
+            ALOGI("Couldn't load power HAL HIDL service");
+            gPowerHalHidlExists = false;
+            return HalVersion::NONE;
+        }
+    }
+    if (gPowerHalHidlV1_1_) {
+        return HalVersion::HIDL_1_1;
+    } else if (gPowerHalHidlV1_0_) {
+        return HalVersion::HIDL_1_0;
+    }
+    return HalVersion::NONE;
 }
 
-// Retrieve a copy of PowerHAL V1_0
-sp<IPowerV1_0> getPowerHalV1_0() {
+// Retrieve a copy of PowerHAL HIDL V1_0
+sp<IPowerV1_0> getPowerHalHidlV1_0() {
     std::lock_guard<std::mutex> lock(gPowerHalMutex);
-    connectPowerHalLocked();
-    return gPowerHalV1_0_;
+    HalVersion halVersion = connectPowerHalLocked();
+    if (halVersion == HalVersion::HIDL_1_0 || halVersion == HalVersion::HIDL_1_1) {
+        return gPowerHalHidlV1_0_;
+    }
+
+    return nullptr;
 }
 
-// Retrieve a copy of PowerHAL V1_1
-sp<IPowerV1_1> getPowerHalV1_1() {
+// Retrieve a copy of PowerHAL HIDL V1_1
+sp<IPowerV1_1> getPowerHalHidlV1_1() {
     std::lock_guard<std::mutex> lock(gPowerHalMutex);
-    connectPowerHalLocked();
-    return gPowerHalV1_1_;
+    if (connectPowerHalLocked() == HalVersion::HIDL_1_1) {
+        return gPowerHalHidlV1_1_;
+    }
+
+    return nullptr;
 }
 
 // Check if a call to a power HAL function failed; if so, log the failure and invalidate the
 // current handle to the power HAL service.
-bool processPowerHalReturn(const Return<void> &ret, const char* functionName) {
-    if (!ret.isOk()) {
+bool processPowerHalReturn(bool isOk, const char* functionName) {
+    if (!isOk) {
         ALOGE("%s() failed: power HAL service not available.", functionName);
         gPowerHalMutex.lock();
-        gPowerHalV1_0_ = nullptr;
-        gPowerHalV1_1_ = nullptr;
+        gPowerHalHidlV1_0_ = nullptr;
+        gPowerHalHidlV1_1_ = nullptr;
+        gPowerHalAidl_ = nullptr;
         gPowerHalMutex.unlock();
     }
-    return ret.isOk();
+    return isOk;
 }
 
 static void sendPowerHint(PowerHint hintId, uint32_t data) {
-    sp<IPowerV1_1> powerHalV1_1 = getPowerHalV1_1();
-    Return<void> ret;
-    if (powerHalV1_1 != nullptr) {
-        ret = powerHalV1_1->powerHintAsync(hintId, data);
-        processPowerHalReturn(ret, "powerHintAsync");
-    } else {
-        sp<IPowerV1_0> powerHalV1_0 = getPowerHalV1_0();
-        if (powerHalV1_0 != nullptr) {
-            ret = powerHalV1_0->powerHint(hintId, data);
-            processPowerHalReturn(ret, "powerHint");
+    std::unique_lock<std::mutex> lock(gPowerHalMutex);
+    switch (connectPowerHalLocked()) {
+        case HalVersion::NONE:
+            return;
+        case HalVersion::HIDL_1_0: {
+            sp<IPowerV1_0> handle = gPowerHalHidlV1_0_;
+            lock.unlock();
+            auto ret = handle->powerHint(hintId, data);
+            processPowerHalReturn(ret.isOk(), "powerHint");
+            break;
+        }
+        case HalVersion::HIDL_1_1: {
+            sp<IPowerV1_1> handle = gPowerHalHidlV1_1_;
+            lock.unlock();
+            auto ret = handle->powerHintAsync(hintId, data);
+            processPowerHalReturn(ret.isOk(), "powerHintAsync");
+            break;
+        }
+        case HalVersion::AIDL: {
+            if (hintId == PowerHint::INTERACTION) {
+                sp<IPowerAidl> handle = gPowerHalAidl_;
+                lock.unlock();
+                auto ret = handle->setBoost(Boost::INTERACTION, data);
+                processPowerHalReturn(ret.isOk(), "setBoost");
+                break;
+            } else if (hintId == PowerHint::LAUNCH) {
+                sp<IPowerAidl> handle = gPowerHalAidl_;
+                lock.unlock();
+                auto ret = handle->setMode(Mode::LAUNCH, static_cast<bool>(data));
+                processPowerHalReturn(ret.isOk(), "setMode");
+                break;
+            } else {
+                ALOGE("Unsupported power hint: %s.", toString(hintId).c_str());
+                return;
+            }
+        }
+        default: {
+            ALOGE("Unknown power HAL state");
+            return;
+        }
+    }
+    SurfaceComposerClient::notifyPowerHint(static_cast<int32_t>(hintId));
+}
+
+enum class HalSupport {
+    UNKNOWN = 0,
+    ON,
+    OFF,
+};
+
+static void setPowerBoost(Boost boost, int32_t durationMs) {
+    // Android framework only sends boost upto DISPLAY_UPDATE_IMMINENT.
+    // Need to increase the array size if more boost supported.
+    static std::array<std::atomic<HalSupport>,
+                      static_cast<int32_t>(Boost::DISPLAY_UPDATE_IMMINENT) + 1>
+        boostSupportedArray = {HalSupport::UNKNOWN};
+
+    // Quick return if boost is not supported by HAL
+    if (boost > Boost::DISPLAY_UPDATE_IMMINENT ||
+        boostSupportedArray[static_cast<int32_t>(boost)] == HalSupport::OFF) {
+        ALOGV("Skipped setPowerBoost %s because HAL doesn't support it", toString(boost).c_str());
+        return;
+    }
+
+    std::unique_lock<std::mutex> lock(gPowerHalMutex);
+    if (connectPowerHalLocked() != HalVersion::AIDL) {
+        ALOGV("Power HAL AIDL not available");
+        return;
+    }
+    sp<IPowerAidl> handle = gPowerHalAidl_;
+    lock.unlock();
+
+    if (boostSupportedArray[static_cast<int32_t>(boost)] == HalSupport::UNKNOWN) {
+        bool isSupported = false;
+        handle->isBoostSupported(boost, &isSupported);
+        boostSupportedArray[static_cast<int32_t>(boost)] =
+            isSupported ? HalSupport::ON : HalSupport::OFF;
+        if (!isSupported) {
+            ALOGV("Skipped setPowerBoost %s because HAL doesn't support it",
+                  toString(boost).c_str());
+            return;
         }
     }
 
-    SurfaceComposerClient::notifyPowerHint(static_cast<int32_t>(hintId));
+    auto ret = handle->setBoost(boost, durationMs);
+    processPowerHalReturn(ret.isOk(), "setPowerBoost");
+}
+
+static void setPowerMode(Mode mode, bool enabled) {
+    // Android framework only sends mode upto DISPLAY_INACTIVE.
+    // Need to increase the array if more mode supported.
+    static std::array<std::atomic<HalSupport>,
+                      static_cast<int32_t>(Mode::DISPLAY_INACTIVE) + 1>
+        modeSupportedArray = {HalSupport::UNKNOWN};
+
+    // Quick return if mode is not supported by HAL
+    if (mode > Mode::DISPLAY_INACTIVE ||
+        modeSupportedArray[static_cast<int32_t>(mode)] == HalSupport::OFF) {
+        ALOGV("Skipped setPowerMode %s because HAL doesn't support it", toString(mode).c_str());
+        return;
+    }
+
+    std::unique_lock<std::mutex> lock(gPowerHalMutex);
+    if (connectPowerHalLocked() != HalVersion::AIDL) {
+        ALOGV("Power HAL AIDL not available");
+        return;
+    }
+    sp<IPowerAidl> handle = gPowerHalAidl_;
+    lock.unlock();
+
+    if (modeSupportedArray[static_cast<int32_t>(mode)] == HalSupport::UNKNOWN) {
+        bool isSupported = false;
+        handle->isModeSupported(mode, &isSupported);
+        modeSupportedArray[static_cast<int32_t>(mode)] =
+            isSupported ? HalSupport::ON : HalSupport::OFF;
+        if (!isSupported) {
+            ALOGV("Skipped setPowerMode %s because HAL doesn't support it", toString(mode).c_str());
+            return;
+        }
+    }
+
+    auto ret = handle->setMode(mode, enabled);
+    processPowerHalReturn(ret.isOk(), "setPowerMode");
 }
 
 void android_server_PowerManagerService_userActivity(nsecs_t eventTime, int32_t eventType) {
@@ -253,14 +405,34 @@
 }
 
 static void nativeSetInteractive(JNIEnv* /* env */, jclass /* clazz */, jboolean enable) {
-    sp<IPowerV1_0> powerHalV1_0 = getPowerHalV1_0();
-    if (powerHalV1_0 != nullptr) {
-        android::base::Timer t;
-        Return<void> ret = powerHalV1_0->setInteractive(enable);
-        processPowerHalReturn(ret, "setInteractive");
-        if (t.duration() > 20ms) {
-            ALOGD("Excessive delay in setInteractive(%s) while turning screen %s",
-                  enable ? "true" : "false", enable ? "on" : "off");
+    std::unique_lock<std::mutex> lock(gPowerHalMutex);
+    switch (connectPowerHalLocked()) {
+        case HalVersion::NONE:
+            return;
+        case HalVersion::HIDL_1_0:
+            FALLTHROUGH_INTENDED;
+        case HalVersion::HIDL_1_1: {
+            android::base::Timer t;
+            sp<IPowerV1_0> handle = gPowerHalHidlV1_0_;
+            lock.unlock();
+            auto ret = handle->setInteractive(enable);
+            processPowerHalReturn(ret.isOk(), "setInteractive");
+            if (t.duration() > 20ms) {
+                ALOGD("Excessive delay in setInteractive(%s) while turning screen %s",
+                      enable ? "true" : "false", enable ? "on" : "off");
+            }
+            return;
+        }
+        case HalVersion::AIDL: {
+            sp<IPowerAidl> handle = gPowerHalAidl_;
+            lock.unlock();
+            auto ret = handle->setMode(Mode::INTERACTIVE, enable);
+            processPowerHalReturn(ret.isOk(), "setMode");
+            return;
+        }
+        default: {
+            ALOGE("Unknown power HAL state");
+            return;
         }
     }
 }
@@ -285,11 +457,38 @@
     sendPowerHint(static_cast<PowerHint>(hintId), data);
 }
 
+static void nativeSetPowerBoost(JNIEnv* /* env */, jclass /* clazz */, jint boost,
+                                jint durationMs) {
+    setPowerBoost(static_cast<Boost>(boost), durationMs);
+}
+
+static void nativeSetPowerMode(JNIEnv* /* env */, jclass /* clazz */, jint mode, jboolean enabled) {
+    setPowerMode(static_cast<Mode>(mode), enabled);
+}
+
 static void nativeSetFeature(JNIEnv* /* env */, jclass /* clazz */, jint featureId, jint data) {
-    sp<IPowerV1_0> powerHalV1_0 = getPowerHalV1_0();
-    if (powerHalV1_0 != nullptr) {
-        Return<void> ret = powerHalV1_0->setFeature((Feature)featureId, static_cast<bool>(data));
-        processPowerHalReturn(ret, "setFeature");
+    std::unique_lock<std::mutex> lock(gPowerHalMutex);
+    switch (connectPowerHalLocked()) {
+        case HalVersion::NONE:
+            return;
+        case HalVersion::HIDL_1_0:
+            FALLTHROUGH_INTENDED;
+        case HalVersion::HIDL_1_1: {
+            sp<IPowerV1_0> handle = gPowerHalHidlV1_0_;
+            lock.unlock();
+            auto ret = handle->setFeature(static_cast<Feature>(featureId), static_cast<bool>(data));
+            processPowerHalReturn(ret.isOk(), "setFeature");
+            return;
+        }
+        case HalVersion::AIDL: {
+            auto ret = gPowerHalAidl_->setMode(Mode::DOUBLE_TAP_TO_WAKE, static_cast<bool>(data));
+            processPowerHalReturn(ret.isOk(), "setMode");
+            return;
+        }
+        default: {
+            ALOGE("Unknown power HAL state");
+            return;
+        }
     }
 }
 
@@ -317,6 +516,10 @@
             (void*) nativeSetAutoSuspend },
     { "nativeSendPowerHint", "(II)V",
             (void*) nativeSendPowerHint },
+    { "nativeSetPowerBoost", "(II)V",
+            (void*) nativeSetPowerBoost },
+    { "nativeSetPowerMode", "(IZ)V",
+            (void*) nativeSetPowerMode },
     { "nativeSetFeature", "(II)V",
             (void*) nativeSetFeature },
 };
diff --git a/services/tests/servicestests/src/com/android/server/tv/tunerresourcemanager/TunerResourceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/tv/tunerresourcemanager/TunerResourceManagerServiceTest.java
index bd63f3c..8da3bdf 100644
--- a/services/tests/servicestests/src/com/android/server/tv/tunerresourcemanager/TunerResourceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/tv/tunerresourcemanager/TunerResourceManagerServiceTest.java
@@ -26,12 +26,12 @@
 import android.media.tv.TvInputManager;
 import android.media.tv.TvInputService;
 import android.media.tv.tuner.frontend.FrontendSettings;
+import android.media.tv.tunerresourcemanager.IResourcesReclaimListener;
 import android.media.tv.tunerresourcemanager.ResourceClientProfile;
 import android.media.tv.tunerresourcemanager.TunerFrontendInfo;
 import android.media.tv.tunerresourcemanager.TunerFrontendRequest;
 import android.media.tv.tunerresourcemanager.TunerResourceManager;
 import android.os.RemoteException;
-import android.util.SparseArray;
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
@@ -45,9 +45,8 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.List;
+import java.util.Map;
 
 /**
  * Tests for {@link TunerResourceManagerService} class.
@@ -59,7 +58,19 @@
     private Context mContextSpy;
     @Mock private ITvInputManager mITvInputManagerMock;
     private TunerResourceManagerService mTunerResourceManagerService;
-    private int mReclaimingId;
+
+    private static final class TestResourcesReclaimListener extends IResourcesReclaimListener.Stub {
+        boolean mReclaimed;
+
+        @Override
+        public void onReclaimResources() {
+            mReclaimed = true;
+        }
+
+        public boolean isRelaimed() {
+            return mReclaimed;
+        }
+    }
 
     // A correspondence to compare a FrontendResource and a TunerFrontendInfo.
     private static final Correspondence<FrontendResource, TunerFrontendInfo> FR_TFI_COMPARE =
@@ -81,31 +92,14 @@
             }
         };
 
-    private static <T> List<T> sparseArrayToList(SparseArray<T> sparseArray) {
-        if (sparseArray == null) {
-            return null;
-        }
-        List<T> arrayList = new ArrayList<T>(sparseArray.size());
-        for (int i = 0; i < sparseArray.size(); i++) {
-            arrayList.add(sparseArray.valueAt(i));
-        }
-        return arrayList;
-    }
-
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
         TvInputManager tvInputManager = new TvInputManager(mITvInputManagerMock, 0);
         mContextSpy = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext()));
         when(mContextSpy.getSystemService(Context.TV_INPUT_SERVICE)).thenReturn(tvInputManager);
-        mTunerResourceManagerService = new TunerResourceManagerService(mContextSpy) {
-            @Override
-            protected void reclaimFrontendResource(int reclaimingId) {
-                mReclaimingId = reclaimingId;
-            }
-        };
+        mTunerResourceManagerService = new TunerResourceManagerService(mContextSpy);
         mTunerResourceManagerService.onStart(true /*isForTesting*/);
-        mReclaimingId = -1;
     }
 
     @Test
@@ -118,7 +112,7 @@
                 new TunerFrontendInfo(1 /*id*/, FrontendSettings.TYPE_DVBT, 1 /*exclusiveGroupId*/);
         mTunerResourceManagerService.setFrontendInfoListInternal(infos);
 
-        SparseArray<FrontendResource> resources =
+        Map<Integer, FrontendResource> resources =
                 mTunerResourceManagerService.getFrontendResources();
         for (int id = 0; id < infos.length; id++) {
             assertThat(resources.get(infos[id].getId())
@@ -128,7 +122,7 @@
             assertThat(resources.get(infos[id].getId())
                     .getExclusiveGroupMemberFeIds().size()).isEqualTo(0);
         }
-        assertThat(sparseArrayToList(resources)).comparingElementsUsing(FR_TFI_COMPARE)
+        assertThat(resources.values()).comparingElementsUsing(FR_TFI_COMPARE)
                 .containsExactlyElementsIn(Arrays.asList(infos));
     }
 
@@ -146,19 +140,15 @@
                 new TunerFrontendInfo(3 /*id*/, FrontendSettings.TYPE_ATSC, 1 /*exclusiveGroupId*/);
         mTunerResourceManagerService.setFrontendInfoListInternal(infos);
 
-        SparseArray<FrontendResource> resources =
+        Map<Integer, FrontendResource> resources =
                 mTunerResourceManagerService.getFrontendResources();
-        assertThat(sparseArrayToList(resources)).comparingElementsUsing(FR_TFI_COMPARE)
+        assertThat(resources.values()).comparingElementsUsing(FR_TFI_COMPARE)
                 .containsExactlyElementsIn(Arrays.asList(infos));
 
-        assertThat(resources.get(0).getExclusiveGroupMemberFeIds())
-                .isEqualTo(new ArrayList<Integer>());
-        assertThat(resources.get(1).getExclusiveGroupMemberFeIds())
-                .isEqualTo(new ArrayList<Integer>(Arrays.asList(2, 3)));
-        assertThat(resources.get(2).getExclusiveGroupMemberFeIds())
-                .isEqualTo(new ArrayList<Integer>(Arrays.asList(1, 3)));
-        assertThat(resources.get(3).getExclusiveGroupMemberFeIds())
-                .isEqualTo(new ArrayList<Integer>(Arrays.asList(1, 2)));
+        assertThat(resources.get(0).getExclusiveGroupMemberFeIds()).isEmpty();
+        assertThat(resources.get(1).getExclusiveGroupMemberFeIds()).containsExactly(2, 3);
+        assertThat(resources.get(2).getExclusiveGroupMemberFeIds()).containsExactly(1, 3);
+        assertThat(resources.get(3).getExclusiveGroupMemberFeIds()).containsExactly(1, 2);
     }
 
     @Test
@@ -171,11 +161,11 @@
                 new TunerFrontendInfo(1 /*id*/, FrontendSettings.TYPE_DVBS, 1 /*exclusiveGroupId*/);
 
         mTunerResourceManagerService.setFrontendInfoListInternal(infos);
-        SparseArray<FrontendResource> resources0 =
+        Map<Integer, FrontendResource> resources0 =
                 mTunerResourceManagerService.getFrontendResources();
 
         mTunerResourceManagerService.setFrontendInfoListInternal(infos);
-        SparseArray<FrontendResource> resources1 =
+        Map<Integer, FrontendResource> resources1 =
                 mTunerResourceManagerService.getFrontendResources();
 
         assertThat(resources0).isEqualTo(resources1);
@@ -198,13 +188,13 @@
                 new TunerFrontendInfo(1 /*id*/, FrontendSettings.TYPE_DVBT, 1 /*exclusiveGroupId*/);
         mTunerResourceManagerService.setFrontendInfoListInternal(infos1);
 
-        SparseArray<FrontendResource> resources =
+        Map<Integer, FrontendResource> resources =
                 mTunerResourceManagerService.getFrontendResources();
         for (int id = 0; id < infos1.length; id++) {
             assertThat(resources.get(infos1[id].getId())
                     .getExclusiveGroupMemberFeIds().size()).isEqualTo(0);
         }
-        assertThat(sparseArrayToList(resources)).comparingElementsUsing(FR_TFI_COMPARE)
+        assertThat(resources.values()).comparingElementsUsing(FR_TFI_COMPARE)
                 .containsExactlyElementsIn(Arrays.asList(infos1));
     }
 
@@ -225,13 +215,13 @@
                 new TunerFrontendInfo(1 /*id*/, FrontendSettings.TYPE_DVBT, 1 /*exclusiveGroupId*/);
         mTunerResourceManagerService.setFrontendInfoListInternal(infos1);
 
-        SparseArray<FrontendResource> resources =
+        Map<Integer, FrontendResource> resources =
                 mTunerResourceManagerService.getFrontendResources();
         for (int id = 0; id < infos1.length; id++) {
             assertThat(resources.get(infos1[id].getId())
                     .getExclusiveGroupMemberFeIds().size()).isEqualTo(0);
         }
-        assertThat(sparseArrayToList(resources)).comparingElementsUsing(FR_TFI_COMPARE)
+        assertThat(resources.values()).comparingElementsUsing(FR_TFI_COMPARE)
                 .containsExactlyElementsIn(Arrays.asList(infos1));
     }
 
@@ -352,9 +342,9 @@
             throw e.rethrowFromSystemServer();
         }
         assertThat(frontendId[0]).isEqualTo(infos[1].getId());
-        assertThat(mTunerResourceManagerService.getFrontendResources().get(infos[1].getId())
+        assertThat(mTunerResourceManagerService.getFrontendResource(infos[1].getId())
                 .isInUse()).isTrue();
-        assertThat(mTunerResourceManagerService.getFrontendResources().get(infos[2].getId())
+        assertThat(mTunerResourceManagerService.getFrontendResource(infos[2].getId())
                 .isInUse()).isTrue();
     }
 
@@ -369,15 +359,17 @@
         int[] clientPriorities = {100, 50};
         int[] clientId0 = new int[1];
         int[] clientId1 = new int[1];
+        TestResourcesReclaimListener listener = new TestResourcesReclaimListener();
+
         mTunerResourceManagerService.registerClientProfileInternal(
-                profiles[0], null /*listener*/, clientId0);
+                profiles[0], listener, clientId0);
         assertThat(clientId0[0]).isNotEqualTo(TunerResourceManagerService.INVALID_CLIENT_ID);
-        mTunerResourceManagerService.getClientProfiles().get(clientId0[0])
+        mTunerResourceManagerService.getClientProfile(clientId0[0])
                 .setPriority(clientPriorities[0]);
         mTunerResourceManagerService.registerClientProfileInternal(
-                profiles[1], null /*listener*/, clientId1);
+                profiles[1], new TestResourcesReclaimListener(), clientId1);
         assertThat(clientId1[0]).isNotEqualTo(TunerResourceManagerService.INVALID_CLIENT_ID);
-        mTunerResourceManagerService.getClientProfiles().get(clientId1[0])
+        mTunerResourceManagerService.getClientProfile(clientId1[0])
                 .setPriority(clientPriorities[1]);
 
         // Init frontend resources.
@@ -403,17 +395,17 @@
         try {
             assertThat(mTunerResourceManagerService.requestFrontendInternal(request, frontendId))
                     .isFalse();
+            assertThat(listener.isRelaimed()).isFalse();
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
-        assertThat(mReclaimingId).isEqualTo(-1);
 
         request =
                 new TunerFrontendRequest(clientId1[0] /*clientId*/, FrontendSettings.TYPE_DVBS);
         try {
             assertThat(mTunerResourceManagerService.requestFrontendInternal(request, frontendId))
                     .isFalse();
-            assertThat(mReclaimingId).isEqualTo(-1);
+            assertThat(listener.isRelaimed()).isFalse();
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -430,15 +422,16 @@
         int[] clientPriorities = {100, 500};
         int[] clientId0 = new int[1];
         int[] clientId1 = new int[1];
+        TestResourcesReclaimListener listener = new TestResourcesReclaimListener();
         mTunerResourceManagerService.registerClientProfileInternal(
-                profiles[0], null /*listener*/, clientId0);
+                profiles[0], listener, clientId0);
         assertThat(clientId0[0]).isNotEqualTo(TunerResourceManagerService.INVALID_CLIENT_ID);
-        mTunerResourceManagerService.getClientProfiles().get(clientId0[0])
+        mTunerResourceManagerService.getClientProfile(clientId0[0])
                 .setPriority(clientPriorities[0]);
         mTunerResourceManagerService.registerClientProfileInternal(
-                profiles[1], null /*listener*/, clientId1);
+                profiles[1], new TestResourcesReclaimListener(), clientId1);
         assertThat(clientId1[0]).isNotEqualTo(TunerResourceManagerService.INVALID_CLIENT_ID);
-        mTunerResourceManagerService.getClientProfiles().get(clientId1[0])
+        mTunerResourceManagerService.getClientProfile(clientId1[0])
                 .setPriority(clientPriorities[1]);
 
         // Init frontend resources.
@@ -469,15 +462,15 @@
             throw e.rethrowFromSystemServer();
         }
         assertThat(frontendId[0]).isEqualTo(infos[1].getId());
-        assertThat(mTunerResourceManagerService.getFrontendResources().get(infos[0].getId())
+        assertThat(mTunerResourceManagerService.getFrontendResource(infos[0].getId())
                 .isInUse()).isTrue();
-        assertThat(mTunerResourceManagerService.getFrontendResources().get(infos[1].getId())
+        assertThat(mTunerResourceManagerService.getFrontendResource(infos[1].getId())
                 .isInUse()).isTrue();
-        assertThat(mTunerResourceManagerService.getFrontendResources()
-                .get(infos[0].getId()).getOwnerClientId()).isEqualTo(clientId1[0]);
-        assertThat(mTunerResourceManagerService.getFrontendResources()
-                .get(infos[1].getId()).getOwnerClientId()).isEqualTo(clientId1[0]);
-        assertThat(mReclaimingId).isEqualTo(clientId0[0]);
+        assertThat(mTunerResourceManagerService.getFrontendResource(infos[0].getId())
+                .getOwnerClientId()).isEqualTo(clientId1[0]);
+        assertThat(mTunerResourceManagerService.getFrontendResource(infos[1].getId())
+                .getOwnerClientId()).isEqualTo(clientId1[0]);
+        assertThat(listener.isRelaimed()).isTrue();
     }
 
     @Test
@@ -508,17 +501,18 @@
             throw e.rethrowFromSystemServer();
         }
         assertThat(frontendId[0]).isEqualTo(infos[0].getId());
-        assertThat(mTunerResourceManagerService.getFrontendResources().get(infos[0].getId())
+        assertThat(mTunerResourceManagerService.getFrontendResource(infos[0].getId())
                 .isInUse()).isTrue();
-        assertThat(mTunerResourceManagerService.getFrontendResources().get(infos[1].getId())
+        assertThat(mTunerResourceManagerService.getFrontendResource(infos[1].getId())
                 .isInUse()).isTrue();
 
         // Unregister client when using frontend
         mTunerResourceManagerService.unregisterClientProfileInternal(clientId[0]);
-        assertThat(mTunerResourceManagerService.getFrontendResources().get(infos[0].getId())
+        assertThat(mTunerResourceManagerService.getFrontendResource(infos[0].getId())
                 .isInUse()).isFalse();
-        assertThat(mTunerResourceManagerService.getFrontendResources().get(infos[1].getId())
+        assertThat(mTunerResourceManagerService.getFrontendResource(infos[1].getId())
                 .isInUse()).isFalse();
+        assertThat(mTunerResourceManagerService.checkClientExists(clientId[0])).isFalse();
 
     }
 }
diff --git a/telecomm/java/android/telecom/Connection.java b/telecomm/java/android/telecom/Connection.java
index 5f33a3d..9dfa3ac 100755
--- a/telecomm/java/android/telecom/Connection.java
+++ b/telecomm/java/android/telecom/Connection.java
@@ -2331,7 +2331,6 @@
      *        See {@link TelecomManager} for valid values.
      */
     public final void setAddress(Uri address, int presentation) {
-        checkImmutable();
         Log.d(this, "setAddress %s", address);
         mAddress = address;
         mAddressPresentation = presentation;
@@ -3358,6 +3357,7 @@
         private boolean mImmutable = false;
         public FailureSignalingConnection(DisconnectCause disconnectCause) {
             setDisconnected(disconnectCause);
+            mImmutable = true;
         }
 
         public void checkImmutable() {