Merge "Initial commit of PowerStatsService"
diff --git a/apct-tests/perftests/core/src/android/os/ParcelObtainPerfTest.java b/apct-tests/perftests/core/src/android/os/ParcelObtainPerfTest.java
new file mode 100644
index 0000000..760ae12
--- /dev/null
+++ b/apct-tests/perftests/core/src/android/os/ParcelObtainPerfTest.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package android.os;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.LargeTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class ParcelObtainPerfTest {
+    private static final int ITERATIONS = 1_000_000;
+
+    @Test
+    public void timeContention_01() throws Exception {
+        timeContention(1);
+    }
+
+    @Test
+    public void timeContention_04() throws Exception {
+        timeContention(4);
+    }
+
+    @Test
+    public void timeContention_16() throws Exception {
+        timeContention(16);
+    }
+
+    private static void timeContention(int numThreads) throws Exception {
+        final long start = SystemClock.elapsedRealtime();
+        {
+            final ObtainThread[] threads = new ObtainThread[numThreads];
+            for (int i = 0; i < numThreads; i++) {
+                final ObtainThread thread = new ObtainThread(ITERATIONS / numThreads);
+                thread.start();
+                threads[i] = thread;
+            }
+            for (int i = 0; i < numThreads; i++) {
+                threads[i].join();
+            }
+        }
+        final long duration = SystemClock.elapsedRealtime() - start;
+
+        final Bundle results = new Bundle();
+        results.putLong("duration", duration);
+        InstrumentationRegistry.getInstrumentation().sendStatus(0, results);
+    }
+
+    public static class ObtainThread extends Thread {
+        public int iterations;
+
+        public ObtainThread(int iterations) {
+            this.iterations = iterations;
+        }
+
+        @Override
+        public void run() {
+            while (iterations-- > 0) {
+                final Parcel data = Parcel.obtain();
+                final Parcel reply = Parcel.obtain();
+                try {
+                    data.writeInt(32);
+                    reply.writeInt(32);
+                } finally {
+                    reply.recycle();
+                    data.recycle();
+                }
+            }
+        }
+    }
+}
diff --git a/apct-tests/perftests/core/src/android/os/ParcelPerfTest.java b/apct-tests/perftests/core/src/android/os/ParcelPerfTest.java
index 4db9262..be2f9d7 100644
--- a/apct-tests/perftests/core/src/android/os/ParcelPerfTest.java
+++ b/apct-tests/perftests/core/src/android/os/ParcelPerfTest.java
@@ -159,21 +159,6 @@
     }
 
     @Test
-    public void timeObtainRecycle() {
-        // Use up the pooled instances.
-        // A lot bigger than the actual size but in case someone increased it.
-        final int POOL_SIZE = 100;
-        for (int i = 0; i < POOL_SIZE; i++) {
-            Parcel.obtain();
-        }
-
-        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
-        while (state.keepRunning()) {
-            Parcel.obtain().recycle();
-        }
-    }
-
-    @Test
     public void timeWriteException() {
         timeWriteException(false);
     }
diff --git a/api/system-current.txt b/api/system-current.txt
index 84a8a1d..f4029f0 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -671,6 +671,9 @@
     method @Nullable public android.content.ComponentName getAllowedNotificationAssistant();
     method public boolean isNotificationAssistantAccessGranted(@NonNull android.content.ComponentName);
     method public void setNotificationAssistantAccessGranted(@Nullable android.content.ComponentName, boolean);
+    field @RequiresPermission(android.Manifest.permission.STATUS_BAR_SERVICE) public static final String ACTION_CLOSE_NOTIFICATION_HANDLER_PANEL = "android.app.action.CLOSE_NOTIFICATION_HANDLER_PANEL";
+    field @RequiresPermission(android.Manifest.permission.STATUS_BAR_SERVICE) public static final String ACTION_OPEN_NOTIFICATION_HANDLER_PANEL = "android.app.action.OPEN_NOTIFICATION_HANDLER_PANEL";
+    field @RequiresPermission(android.Manifest.permission.STATUS_BAR_SERVICE) public static final String ACTION_TOGGLE_NOTIFICATION_HANDLER_PANEL = "android.app.action.TOGGLE_NOTIFICATION_HANDLER_PANEL";
   }
 
   public final class RuntimeAppOpAccessMessage implements android.os.Parcelable {
diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java
index 0627bc8..fe89366 100644
--- a/core/java/android/app/NotificationManager.java
+++ b/core/java/android/app/NotificationManager.java
@@ -19,6 +19,7 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
 import android.annotation.SdkConstant;
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
@@ -131,6 +132,73 @@
             "android.app.action.NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED";
 
     /**
+     * Activity action: Toggle notification panel of the specified handler.
+     *
+     * <p><strong>Important:</strong>You must protect the activity that handles this action with
+     * the {@link android.Manifest.permission#STATUS_BAR_SERVICE} permission to ensure that only
+     * the SystemUI can launch this activity. Activities that are not properly protected will not
+     * be launched.
+     *
+     * <p class="note">This is currently only used on TV to allow a system app to handle the
+     * notification panel. The package handling the notification panel has to be specified by
+     * config_notificationHandlerPackage in values/config.xml.
+     *
+     * Input: nothing
+     * Output: nothing
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.STATUS_BAR_SERVICE)
+    @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_TOGGLE_NOTIFICATION_HANDLER_PANEL =
+            "android.app.action.TOGGLE_NOTIFICATION_HANDLER_PANEL";
+
+    /**
+     * Activity action: Open notification panel of the specified handler.
+     *
+     * <p><strong>Important:</strong>You must protect the activity that handles this action with
+     * the {@link android.Manifest.permission#STATUS_BAR_SERVICE} permission to ensure that only
+     * the SystemUI can launch this activity. Activities that are not properly protected will
+     * not be launched.
+     *
+     * <p class="note"> This is currently only used on TV to allow a system app to handle the
+     * notification panel. The package handling the notification panel has to be specified by
+     * config_notificationHandlerPackage in values/config.xml.
+     *
+     * Input: nothing
+     * Output: nothing
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.STATUS_BAR_SERVICE)
+    @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_OPEN_NOTIFICATION_HANDLER_PANEL =
+            "android.app.action.OPEN_NOTIFICATION_HANDLER_PANEL";
+
+    /**
+     * Intent that is broadcast when the notification panel of the specified handler is to be
+     * closed.
+     *
+     * <p><strong>Important:</strong>You should protect the receiver that handles this action with
+     * the {@link android.Manifest.permission#STATUS_BAR_SERVICE} permission to ensure that only
+     * the SystemUI can send this broadcast to the notification handler.
+     *
+     * <p class="note"> This is currently only used on TV to allow a system app to handle the
+     * notification panel. The package handling the notification panel has to be specified by
+     * config_notificationHandlerPackage in values/config.xml. This is a protected intent that can
+     * only be sent by the system.
+     *
+     * Input: nothing.
+     * Output: nothing.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.STATUS_BAR_SERVICE)
+    @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String ACTION_CLOSE_NOTIFICATION_HANDLER_PANEL =
+            "android.app.action.CLOSE_NOTIFICATION_HANDLER_PANEL";
+
+    /**
      * Extra for {@link #ACTION_NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED} containing the id of the
      * {@link NotificationChannel} which has a new blocked state.
      *
diff --git a/core/java/android/hardware/HardwareBuffer.java b/core/java/android/hardware/HardwareBuffer.java
index dd34930..a9b6132 100644
--- a/core/java/android/hardware/HardwareBuffer.java
+++ b/core/java/android/hardware/HardwareBuffer.java
@@ -26,6 +26,7 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 
+import dalvik.annotation.optimization.CriticalNative;
 import dalvik.annotation.optimization.FastNative;
 import dalvik.system.CloseGuard;
 
@@ -141,8 +142,6 @@
     /** Usage: The buffer contains a complete mipmap hierarchy */
     public static final long USAGE_GPU_MIPMAP_COMPLETE    = 1 << 26;
 
-    // The approximate size of a native AHardwareBuffer object.
-    private static final long NATIVE_HARDWARE_BUFFER_SIZE = 232;
     /**
      * Creates a new <code>HardwareBuffer</code> instance.
      *
@@ -239,10 +238,10 @@
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
     private HardwareBuffer(long nativeObject) {
         mNativeObject = nativeObject;
-
+        long bufferSize = nEstimateSize(nativeObject);
         ClassLoader loader = HardwareBuffer.class.getClassLoader();
         NativeAllocationRegistry registry = new NativeAllocationRegistry(
-                loader, nGetNativeFinalizer(), NATIVE_HARDWARE_BUFFER_SIZE);
+                loader, nGetNativeFinalizer(), bufferSize);
         mCleaner = registry.registerNativeAllocation(this, mNativeObject);
         mCloseGuard.open("close");
     }
@@ -429,4 +428,6 @@
     private static native long nGetUsage(long nativeObject);
     private static native boolean nIsSupported(int width, int height, int format, int layers,
             long usage);
+    @CriticalNative
+    private static native long nEstimateSize(long nativeObject);
 }
diff --git a/core/java/android/hardware/camera2/params/StreamConfigurationMap.java b/core/java/android/hardware/camera2/params/StreamConfigurationMap.java
index 52251ba..9d32a809 100644
--- a/core/java/android/hardware/camera2/params/StreamConfigurationMap.java
+++ b/core/java/android/hardware/camera2/params/StreamConfigurationMap.java
@@ -1320,6 +1320,8 @@
                 return ImageFormat.DEPTH16;
             case HAL_PIXEL_FORMAT_RAW16:
                 return ImageFormat.RAW_DEPTH;
+            case HAL_PIXEL_FORMAT_RAW10:
+                return ImageFormat.RAW_DEPTH10;
             case ImageFormat.JPEG:
                 throw new IllegalArgumentException(
                         "ImageFormat.JPEG is an unknown internal format");
@@ -1393,6 +1395,8 @@
                 return HAL_PIXEL_FORMAT_Y16;
             case ImageFormat.RAW_DEPTH:
                 return HAL_PIXEL_FORMAT_RAW16;
+            case ImageFormat.RAW_DEPTH10:
+                return HAL_PIXEL_FORMAT_RAW10;
             default:
                 return format;
         }
@@ -1437,6 +1441,7 @@
             case ImageFormat.DEPTH_POINT_CLOUD:
             case ImageFormat.DEPTH16:
             case ImageFormat.RAW_DEPTH:
+            case ImageFormat.RAW_DEPTH10:
                 return HAL_DATASPACE_DEPTH;
             case ImageFormat.DEPTH_JPEG:
                 return HAL_DATASPACE_DYNAMIC_DEPTH;
@@ -1878,6 +1883,8 @@
                 return "DEPTH_JPEG";
             case ImageFormat.RAW_DEPTH:
                 return "RAW_DEPTH";
+            case ImageFormat.RAW_DEPTH10:
+                return "RAW_DEPTH10";
             case ImageFormat.PRIVATE:
                 return "PRIVATE";
             case ImageFormat.HEIC:
diff --git a/core/java/android/hardware/display/AmbientDisplayConfiguration.java b/core/java/android/hardware/display/AmbientDisplayConfiguration.java
index ece5c28..7dc1eaa 100644
--- a/core/java/android/hardware/display/AmbientDisplayConfiguration.java
+++ b/core/java/android/hardware/display/AmbientDisplayConfiguration.java
@@ -138,6 +138,11 @@
     }
 
     /** {@hide} */
+    public String udfpsLongPressSensorType() {
+        return mContext.getResources().getString(R.string.config_dozeUdfpsLongPressSensorType);
+    }
+
+    /** {@hide} */
     public boolean pulseOnLongPressEnabled(int user) {
         return pulseOnLongPressAvailable() && boolSettingDefaultOff(
                 Settings.Secure.DOZE_PULSE_ON_LONG_PRESS, user);
diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java
index 415e5a6..1bddc49 100644
--- a/core/java/android/os/Parcel.java
+++ b/core/java/android/os/Parcel.java
@@ -33,9 +33,10 @@
 import android.util.SparseBooleanArray;
 import android.util.SparseIntArray;
 
+import com.android.internal.annotations.GuardedBy;
+
 import dalvik.annotation.optimization.CriticalNative;
 import dalvik.annotation.optimization.FastNative;
-import dalvik.system.VMRuntime;
 
 import libcore.util.ArrayUtils;
 import libcore.util.SneakyThrow;
@@ -222,9 +223,31 @@
      */
     private static boolean sParcelExceptionStackTrace;
 
-    private static final int POOL_SIZE = 6;
-    private static final Parcel[] sOwnedPool = new Parcel[POOL_SIZE];
-    private static final Parcel[] sHolderPool = new Parcel[POOL_SIZE];
+    private static final Object sPoolSync = new Object();
+
+    /** Next item in the linked list pool, if any */
+    @GuardedBy("sPoolSync")
+    private Parcel mPoolNext;
+
+    /** Head of a linked list pool of {@link Parcel} objects */
+    @GuardedBy("sPoolSync")
+    private static Parcel sOwnedPool;
+    /** Head of a linked list pool of {@link Parcel} objects */
+    @GuardedBy("sPoolSync")
+    private static Parcel sHolderPool;
+
+    /** Total size of pool with head at {@link #sOwnedPool} */
+    @GuardedBy("sPoolSync")
+    private static int sOwnedPoolSize = 0;
+    /** Total size of pool with head at {@link #sHolderPool} */
+    @GuardedBy("sPoolSync")
+    private static int sHolderPoolSize = 0;
+
+    /**
+     * We're willing to pool up to 32 objects, which is sized to accommodate
+     * both a data and reply Parcel for the maximum of 16 Binder threads.
+     */
+    private static final int POOL_SIZE = 32;
 
     // Keep in sync with frameworks/native/include/private/binder/ParcelValTypes.h.
     private static final int VAL_NULL = -1;
@@ -285,7 +308,7 @@
     @CriticalNative
     private static native int nativeDataCapacity(long nativePtr);
     @FastNative
-    private static native long nativeSetDataSize(long nativePtr, int size);
+    private static native void nativeSetDataSize(long nativePtr, int size);
     @CriticalNative
     private static native void nativeSetDataPosition(long nativePtr, int pos);
     @FastNative
@@ -314,7 +337,7 @@
     @FastNative
     private static native void nativeWriteStrongBinder(long nativePtr, IBinder val);
     @FastNative
-    private static native long nativeWriteFileDescriptor(long nativePtr, FileDescriptor val);
+    private static native void nativeWriteFileDescriptor(long nativePtr, FileDescriptor val);
 
     private static native byte[] nativeCreateByteArray(long nativePtr);
     private static native boolean nativeReadByteArray(long nativePtr, byte[] dest, int destLen);
@@ -337,14 +360,14 @@
     private static native FileDescriptor nativeReadFileDescriptor(long nativePtr);
 
     private static native long nativeCreate();
-    private static native long nativeFreeBuffer(long nativePtr);
+    private static native void nativeFreeBuffer(long nativePtr);
     private static native void nativeDestroy(long nativePtr);
 
     private static native byte[] nativeMarshall(long nativePtr);
-    private static native long nativeUnmarshall(
+    private static native void nativeUnmarshall(
             long nativePtr, byte[] data, int offset, int length);
     private static native int nativeCompareData(long thisNativePtr, long otherNativePtr);
-    private static native long nativeAppendFrom(
+    private static native void nativeAppendFrom(
             long thisNativePtr, long otherNativePtr, int offset, int length);
     @CriticalNative
     private static native boolean nativeHasFileDescriptors(long nativePtr);
@@ -420,22 +443,27 @@
      */
     @NonNull
     public static Parcel obtain() {
-        final Parcel[] pool = sOwnedPool;
-        synchronized (pool) {
-            Parcel p;
-            for (int i=0; i<POOL_SIZE; i++) {
-                p = pool[i];
-                if (p != null) {
-                    pool[i] = null;
-                    if (DEBUG_RECYCLE) {
-                        p.mStack = new RuntimeException();
-                    }
-                    p.mReadWriteHelper = ReadWriteHelper.DEFAULT;
-                    return p;
-                }
+        Parcel res = null;
+        synchronized (sPoolSync) {
+            if (sOwnedPool != null) {
+                res = sOwnedPool;
+                sOwnedPool = res.mPoolNext;
+                res.mPoolNext = null;
+                sOwnedPoolSize--;
             }
         }
-        return new Parcel(0);
+
+        // When no cache found above, create from scratch; otherwise prepare the
+        // cached object to be used
+        if (res == null) {
+            res = new Parcel(0);
+        } else {
+            if (DEBUG_RECYCLE) {
+                res.mStack = new RuntimeException();
+            }
+            res.mReadWriteHelper = ReadWriteHelper.DEFAULT;
+        }
+        return res;
     }
 
     /**
@@ -446,19 +474,21 @@
         if (DEBUG_RECYCLE) mStack = null;
         freeBuffer();
 
-        final Parcel[] pool;
         if (mOwnsNativeParcelObject) {
-            pool = sOwnedPool;
+            synchronized (sPoolSync) {
+                if (sOwnedPoolSize < POOL_SIZE) {
+                    mPoolNext = sOwnedPool;
+                    sOwnedPool = this;
+                    sOwnedPoolSize++;
+                }
+            }
         } else {
             mNativePtr = 0;
-            pool = sHolderPool;
-        }
-
-        synchronized (pool) {
-            for (int i=0; i<POOL_SIZE; i++) {
-                if (pool[i] == null) {
-                    pool[i] = this;
-                    return;
+            synchronized (sPoolSync) {
+                if (sHolderPoolSize < POOL_SIZE) {
+                    mPoolNext = sHolderPool;
+                    sHolderPool = this;
+                    sHolderPoolSize++;
                 }
             }
         }
@@ -532,7 +562,7 @@
      * @param size The new number of bytes in the Parcel.
      */
     public final void setDataSize(int size) {
-        updateNativeSize(nativeSetDataSize(mNativePtr, size));
+        nativeSetDataSize(mNativePtr, size);
     }
 
     /**
@@ -584,11 +614,11 @@
      * Set the bytes in data to be the raw bytes of this Parcel.
      */
     public final void unmarshall(@NonNull byte[] data, int offset, int length) {
-        updateNativeSize(nativeUnmarshall(mNativePtr, data, offset, length));
+        nativeUnmarshall(mNativePtr, data, offset, length);
     }
 
     public final void appendFrom(Parcel parcel, int offset, int length) {
-        updateNativeSize(nativeAppendFrom(mNativePtr, parcel.mNativePtr, offset, length));
+        nativeAppendFrom(mNativePtr, parcel.mNativePtr, offset, length);
     }
 
     /** @hide */
@@ -871,24 +901,7 @@
      * if {@link Parcelable#PARCELABLE_WRITE_RETURN_VALUE} is set.</p>
      */
     public final void writeFileDescriptor(@NonNull FileDescriptor val) {
-        updateNativeSize(nativeWriteFileDescriptor(mNativePtr, val));
-    }
-
-    private void updateNativeSize(long newNativeSize) {
-        if (mOwnsNativeParcelObject) {
-            if (newNativeSize > Integer.MAX_VALUE) {
-                newNativeSize = Integer.MAX_VALUE;
-            }
-            if (newNativeSize != mNativeSize) {
-                int delta = (int) (newNativeSize - mNativeSize);
-                if (delta > 0) {
-                    VMRuntime.getRuntime().registerNativeAllocation(delta);
-                } else {
-                    VMRuntime.getRuntime().registerNativeFree(-delta);
-                }
-                mNativeSize = newNativeSize;
-            }
-        }
+        nativeWriteFileDescriptor(mNativePtr, val);
     }
 
     /**
@@ -3496,22 +3509,27 @@
 
     /** @hide */
     static protected final Parcel obtain(long obj) {
-        final Parcel[] pool = sHolderPool;
-        synchronized (pool) {
-            Parcel p;
-            for (int i=0; i<POOL_SIZE; i++) {
-                p = pool[i];
-                if (p != null) {
-                    pool[i] = null;
-                    if (DEBUG_RECYCLE) {
-                        p.mStack = new RuntimeException();
-                    }
-                    p.init(obj);
-                    return p;
-                }
+        Parcel res = null;
+        synchronized (sPoolSync) {
+            if (sHolderPool != null) {
+                res = sHolderPool;
+                sHolderPool = res.mPoolNext;
+                res.mPoolNext = null;
+                sHolderPoolSize--;
             }
         }
-        return new Parcel(obj);
+
+        // When no cache found above, create from scratch; otherwise prepare the
+        // cached object to be used
+        if (res == null) {
+            res = new Parcel(obj);
+        } else {
+            if (DEBUG_RECYCLE) {
+                res.mStack = new RuntimeException();
+            }
+            res.init(obj);
+        }
+        return res;
     }
 
     private Parcel(long nativePtr) {
@@ -3535,7 +3553,7 @@
     private void freeBuffer() {
         resetSqaushingState();
         if (mOwnsNativeParcelObject) {
-            updateNativeSize(nativeFreeBuffer(mNativePtr));
+            nativeFreeBuffer(mNativePtr);
         }
         mReadWriteHelper = ReadWriteHelper.DEFAULT;
     }
@@ -3545,7 +3563,6 @@
         if (mNativePtr != 0) {
             if (mOwnsNativeParcelObject) {
                 nativeDestroy(mNativePtr);
-                updateNativeSize(0);
             }
             mNativePtr = 0;
         }
diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java
index ed38b3f..411b58d 100644
--- a/core/java/android/os/PowerManager.java
+++ b/core/java/android/os/PowerManager.java
@@ -274,7 +274,6 @@
 
     /**
      * Brightness value for fully off in float.
-     * TODO(brightnessfloat): rename this to BRIGHTNES_OFF and remove the integer-based constant.
      * @hide
      */
     public static final float BRIGHTNESS_OFF_FLOAT = -1.0f;
diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java
index efea953..d7393ca 100644
--- a/core/java/android/os/Process.java
+++ b/core/java/android/os/Process.java
@@ -32,6 +32,7 @@
 import libcore.io.IoUtils;
 
 import java.io.FileDescriptor;
+import java.io.IOException;
 import java.util.Map;
 import java.util.concurrent.TimeoutException;
 
@@ -1317,33 +1318,16 @@
      */
     public static void waitForProcessDeath(int pid, int timeout)
             throws InterruptedException, TimeoutException {
-        FileDescriptor pidfd = null;
-        if (sPidFdSupported == PIDFD_UNKNOWN) {
-            int fd = -1;
+        boolean fallback = supportsPidFd();
+        if (!fallback) {
+            FileDescriptor pidfd = null;
             try {
-                fd = nativePidFdOpen(pid, 0);
-                sPidFdSupported = PIDFD_SUPPORTED;
-            } catch (ErrnoException e) {
-                sPidFdSupported = e.errno != OsConstants.ENOSYS
-                    ? PIDFD_SUPPORTED : PIDFD_UNSUPPORTED;
-            } finally {
+                final int fd = nativePidFdOpen(pid, 0);
                 if (fd >= 0) {
                     pidfd = new FileDescriptor();
                     pidfd.setInt$(fd);
-                }
-            }
-        }
-        boolean fallback = sPidFdSupported == PIDFD_UNSUPPORTED;
-        if (!fallback) {
-            try {
-                if (pidfd == null) {
-                    int fd = nativePidFdOpen(pid, 0);
-                    if (fd >= 0) {
-                        pidfd = new FileDescriptor();
-                        pidfd.setInt$(fd);
-                    } else {
-                        fallback = true;
-                    }
+                } else {
+                    fallback = true;
                 }
                 if (pidfd != null) {
                     StructPollfd[] fds = new StructPollfd[] {
@@ -1392,5 +1376,59 @@
         throw new TimeoutException();
     }
 
+    /**
+     * Determine whether the system supports pidfd APIs
+     *
+     * @return Returns true if the system supports pidfd APIs
+     * @hide
+     */
+    public static boolean supportsPidFd() {
+        if (sPidFdSupported == PIDFD_UNKNOWN) {
+            int fd = -1;
+            try {
+                fd = nativePidFdOpen(myPid(), 0);
+                sPidFdSupported = PIDFD_SUPPORTED;
+            } catch (ErrnoException e) {
+                sPidFdSupported = e.errno != OsConstants.ENOSYS
+                        ? PIDFD_SUPPORTED : PIDFD_UNSUPPORTED;
+            } finally {
+                if (fd >= 0) {
+                    final FileDescriptor f = new FileDescriptor();
+                    f.setInt$(fd);
+                    IoUtils.closeQuietly(f);
+                }
+            }
+        }
+        return sPidFdSupported == PIDFD_SUPPORTED;
+    }
+
+    /**
+     * Open process file descriptor for given pid.
+     *
+     * @param pid The process ID to open for
+     * @param flags Reserved, unused now, must be 0
+     * @return The process file descriptor for given pid
+     * @throws IOException if it can't be opened
+     *
+     * @hide
+     */
+    public static @Nullable FileDescriptor openPidFd(int pid, int flags) throws IOException {
+        if (!supportsPidFd()) {
+            return null;
+        }
+        if (flags != 0) {
+            throw new IllegalArgumentException();
+        }
+        try {
+            FileDescriptor pidfd = new FileDescriptor();
+            pidfd.setInt$(nativePidFdOpen(pid, flags));
+            return pidfd;
+        } catch (ErrnoException e) {
+            IOException ex = new IOException();
+            ex.initCause(e);
+            throw ex;
+        }
+    }
+
     private static native int nativePidFdOpen(int pid, int flags) throws ErrnoException;
 }
diff --git a/core/jni/android_hardware_HardwareBuffer.cpp b/core/jni/android_hardware_HardwareBuffer.cpp
index e78e08e..2944f72 100644
--- a/core/jni/android_hardware_HardwareBuffer.cpp
+++ b/core/jni/android_hardware_HardwareBuffer.cpp
@@ -30,8 +30,9 @@
 
 #include <binder/Parcel.h>
 
-#include <ui/GraphicBuffer.h>
 #include <private/gui/ComposerService.h>
+#include <ui/GraphicBuffer.h>
+#include <ui/PixelFormat.h>
 
 #include <hardware/gralloc1.h>
 #include <grallocusage/GrallocUsageConversion.h>
@@ -166,6 +167,20 @@
     return AHardwareBuffer_convertFromGrallocUsageBits(buffer->getUsage());
 }
 
+static jlong android_hardware_HardwareBuffer_estimateSize(jlong nativeObject) {
+    GraphicBuffer* buffer = GraphicBufferWrapper_to_GraphicBuffer(nativeObject);
+
+    uint32_t bpp = bytesPerPixel(buffer->getPixelFormat());
+    if (bpp == 0) {
+        // If the pixel format is not recognized, use 1 as default.
+        bpp = 1;
+    }
+
+    const uint32_t bufferStride =
+            buffer->getStride() > 0 ? buffer->getStride() : buffer->getWidth();
+    return static_cast<jlong>(buffer->getHeight() * bufferStride * bpp);
+}
+
 // ----------------------------------------------------------------------------
 // Serialization
 // ----------------------------------------------------------------------------
@@ -247,6 +262,7 @@
 
 const char* const kClassPathName = "android/hardware/HardwareBuffer";
 
+// clang-format off
 static const JNINativeMethod gMethods[] = {
     { "nCreateHardwareBuffer",  "(IIIIJ)J",
             (void*) android_hardware_HardwareBuffer_create },
@@ -267,7 +283,11 @@
     { "nGetFormat", "(J)I",     (void*) android_hardware_HardwareBuffer_getFormat },
     { "nGetLayers", "(J)I",     (void*) android_hardware_HardwareBuffer_getLayers },
     { "nGetUsage", "(J)J",      (void*) android_hardware_HardwareBuffer_getUsage },
+
+    // --------------- @CriticalNative ----------------------
+    { "nEstimateSize", "(J)J",  (void*) android_hardware_HardwareBuffer_estimateSize },
 };
+// clang-format on
 
 int register_android_hardware_HardwareBuffer(JNIEnv* env) {
     int err = RegisterMethodsOrDie(env, kClassPathName, gMethods,
diff --git a/core/jni/android_os_Parcel.cpp b/core/jni/android_os_Parcel.cpp
index 7cfe3bc..0892b70 100644
--- a/core/jni/android_os_Parcel.cpp
+++ b/core/jni/android_os_Parcel.cpp
@@ -114,7 +114,7 @@
     return parcel ? parcel->dataCapacity() : 0;
 }
 
-static jlong android_os_Parcel_setDataSize(JNIEnv* env, jclass clazz, jlong nativePtr, jint size)
+static void android_os_Parcel_setDataSize(JNIEnv* env, jclass clazz, jlong nativePtr, jint size)
 {
     Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
     if (parcel != NULL) {
@@ -122,9 +122,7 @@
         if (err != NO_ERROR) {
             signalExceptionForError(env, clazz, err);
         }
-        return parcel->getOpenAshmemSize();
     }
-    return 0;
 }
 
 static void android_os_Parcel_setDataPosition(jlong nativePtr, jint pos)
@@ -308,7 +306,7 @@
     }
 }
 
-static jlong android_os_Parcel_writeFileDescriptor(JNIEnv* env, jclass clazz, jlong nativePtr, jobject object)
+static void android_os_Parcel_writeFileDescriptor(JNIEnv* env, jclass clazz, jlong nativePtr, jobject object)
 {
     Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
     if (parcel != NULL) {
@@ -317,9 +315,7 @@
         if (err != NO_ERROR) {
             signalExceptionForError(env, clazz, err);
         }
-        return parcel->getOpenAshmemSize();
     }
-    return 0;
 }
 
 static jbyteArray android_os_Parcel_createByteArray(JNIEnv* env, jclass clazz, jlong nativePtr)
@@ -506,14 +502,12 @@
     return reinterpret_cast<jlong>(parcel);
 }
 
-static jlong android_os_Parcel_freeBuffer(JNIEnv* env, jclass clazz, jlong nativePtr)
+static void android_os_Parcel_freeBuffer(JNIEnv* env, jclass clazz, jlong nativePtr)
 {
     Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
     if (parcel != NULL) {
         parcel->freeData();
-        return parcel->getOpenAshmemSize();
     }
-    return 0;
 }
 
 static void android_os_Parcel_destroy(JNIEnv* env, jclass clazz, jlong nativePtr)
@@ -551,12 +545,12 @@
     return ret;
 }
 
-static jlong android_os_Parcel_unmarshall(JNIEnv* env, jclass clazz, jlong nativePtr,
+static void android_os_Parcel_unmarshall(JNIEnv* env, jclass clazz, jlong nativePtr,
                                           jbyteArray data, jint offset, jint length)
 {
     Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);
     if (parcel == NULL || length < 0) {
-       return 0;
+       return;
     }
 
     jbyte* array = (jbyte*)env->GetPrimitiveArrayCritical(data, 0);
@@ -570,7 +564,6 @@
 
         env->ReleasePrimitiveArrayCritical(data, array, 0);
     }
-    return parcel->getOpenAshmemSize();
 }
 
 static jint android_os_Parcel_compareData(JNIEnv* env, jclass clazz, jlong thisNativePtr,
@@ -588,23 +581,23 @@
     return thisParcel->compareData(*otherParcel);
 }
 
-static jlong android_os_Parcel_appendFrom(JNIEnv* env, jclass clazz, jlong thisNativePtr,
+static void android_os_Parcel_appendFrom(JNIEnv* env, jclass clazz, jlong thisNativePtr,
                                           jlong otherNativePtr, jint offset, jint length)
 {
     Parcel* thisParcel = reinterpret_cast<Parcel*>(thisNativePtr);
     if (thisParcel == NULL) {
-       return 0;
+       return;
     }
     Parcel* otherParcel = reinterpret_cast<Parcel*>(otherNativePtr);
     if (otherParcel == NULL) {
-       return thisParcel->getOpenAshmemSize();
+       return;
     }
 
     status_t err = thisParcel->appendFrom(otherParcel, offset, length);
     if (err != NO_ERROR) {
         signalExceptionForError(env, clazz, err);
     }
-    return thisParcel->getOpenAshmemSize();
+    return;
 }
 
 static jboolean android_os_Parcel_hasFileDescriptors(jlong nativePtr)
@@ -720,7 +713,7 @@
     // @CriticalNative
     {"nativeDataCapacity",        "(J)I", (void*)android_os_Parcel_dataCapacity},
     // @FastNative
-    {"nativeSetDataSize",         "(JI)J", (void*)android_os_Parcel_setDataSize},
+    {"nativeSetDataSize",         "(JI)V", (void*)android_os_Parcel_setDataSize},
     // @CriticalNative
     {"nativeSetDataPosition",     "(JI)V", (void*)android_os_Parcel_setDataPosition},
     // @FastNative
@@ -749,7 +742,7 @@
     // @FastNative
     {"nativeWriteStrongBinder",   "(JLandroid/os/IBinder;)V", (void*)android_os_Parcel_writeStrongBinder},
     // @FastNative
-    {"nativeWriteFileDescriptor", "(JLjava/io/FileDescriptor;)J", (void*)android_os_Parcel_writeFileDescriptor},
+    {"nativeWriteFileDescriptor", "(JLjava/io/FileDescriptor;)V", (void*)android_os_Parcel_writeFileDescriptor},
 
     {"nativeCreateByteArray",     "(J)[B", (void*)android_os_Parcel_createByteArray},
     {"nativeReadByteArray",       "(J[BI)Z", (void*)android_os_Parcel_readByteArray},
@@ -772,13 +765,13 @@
     {"nativeReadFileDescriptor",  "(J)Ljava/io/FileDescriptor;", (void*)android_os_Parcel_readFileDescriptor},
 
     {"nativeCreate",              "()J", (void*)android_os_Parcel_create},
-    {"nativeFreeBuffer",          "(J)J", (void*)android_os_Parcel_freeBuffer},
+    {"nativeFreeBuffer",          "(J)V", (void*)android_os_Parcel_freeBuffer},
     {"nativeDestroy",             "(J)V", (void*)android_os_Parcel_destroy},
 
     {"nativeMarshall",            "(J)[B", (void*)android_os_Parcel_marshall},
-    {"nativeUnmarshall",          "(J[BII)J", (void*)android_os_Parcel_unmarshall},
+    {"nativeUnmarshall",          "(J[BII)V", (void*)android_os_Parcel_unmarshall},
     {"nativeCompareData",         "(JJ)I", (void*)android_os_Parcel_compareData},
-    {"nativeAppendFrom",          "(JJII)J", (void*)android_os_Parcel_appendFrom},
+    {"nativeAppendFrom",          "(JJII)V", (void*)android_os_Parcel_appendFrom},
     // @CriticalNative
     {"nativeHasFileDescriptors",  "(J)Z", (void*)android_os_Parcel_hasFileDescriptors},
     {"nativeWriteInterfaceToken", "(JLjava/lang/String;)V", (void*)android_os_Parcel_writeInterfaceToken},
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 560e3c1..714f8cd 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -107,6 +107,8 @@
     <!-- @deprecated This is rarely used and will be phased out soon. -->
     <protected-broadcast android:name="android.os.action.SCREEN_BRIGHTNESS_BOOST_CHANGED" />
 
+    <protected-broadcast android:name="android.app.action.CLOSE_NOTIFICATION_HANDLER_PANEL" />
+
     <protected-broadcast android:name="android.app.action.ENTER_CAR_MODE" />
     <protected-broadcast android:name="android.app.action.EXIT_CAR_MODE" />
     <protected-broadcast android:name="android.app.action.ENTER_CAR_MODE_PRIORITIZED" />
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 550162a..1094462 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -1166,6 +1166,7 @@
             0 - Nothing
             1 - Launch all apps intent
             2 - Launch assist intent
+            3 - Launch notification panel
          This needs to match the constants in
          policy/src/com/android/internal/policy/impl/PhoneWindowManager.java
     -->
@@ -2127,6 +2128,9 @@
     <!-- Type of the long press sensor. Empty if long press is not supported. -->
     <string name="config_dozeLongPressSensorType" translatable="false"></string>
 
+    <!-- Type of the udfps long press sensor. Empty if long press is not supported. -->
+    <string name="config_dozeUdfpsLongPressSensorType" translatable="false"></string>
+
     <!-- If the sensor that wakes up the lock screen is available or not. -->
     <bool name="config_dozeWakeLockScreenSensorAvailable">false</bool>
     <integer name="config_dozeWakeLockScreenDebounce">300</integer>
@@ -3390,6 +3394,9 @@
          service. -->
     <string name="config_tvRemoteServicePackage" translatable="false"></string>
 
+    <!-- The package name of the package implementing the custom notification panel -->
+    <string name="config_notificationHandlerPackage" translatable="false"></string>
+
     <!-- True if the device supports persisting security logs across reboots.
          This requires the device's kernel to have pstore and pmsg enabled,
          and DRAM to be powered and refreshed through all stages of reboot. -->
@@ -4082,7 +4089,7 @@
     <!-- All of the paths defined for the batterymeter are defined on a 12x20 canvas, and must
      be parsable by android.utill.PathParser -->
     <string name="config_batterymeterPerimeterPath" translatable="false">
-		M3.5,2 v0 H1.33 C0.6,2 0,2.6 0,3.33 V13v5.67 C0,19.4 0.6,20 1.33,20 h9.33 C11.4,20 12,19.4 12,18.67 V13V3.33 C12,2.6 11.4,2 10.67,2 H8.5 V0 H3.5 z M2,18v-7V4h8v9v5H2L2,18z
+                M3.5,2 v0 H1.33 C0.6,2 0,2.6 0,3.33 V13v5.67 C0,19.4 0.6,20 1.33,20 h9.33 C11.4,20 12,19.4 12,18.67 V13V3.33 C12,2.6 11.4,2 10.67,2 H8.5 V0 H3.5 z M2,18v-7V4h8v9v5H2L2,18z
     </string>
     <string name="config_batterymeterErrorPerimeterPath" translatable="false">@string/config_batterymeterPerimeterPath</string>
     <string name="config_batterymeterFillMask" translatable="false">
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 35ce780..fdcd39a 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3047,6 +3047,9 @@
   <java-symbol type="string" name="config_tvRemoteServicePackage" />
   <java-symbol type="string" name="notification_messaging_title_template" />
 
+  <!-- Notification handler / dashboard package -->
+  <java-symbol type="string" name="config_notificationHandlerPackage" />
+
   <java-symbol type="bool" name="config_supportPreRebootSecurityLogs" />
 
   <java-symbol type="dimen" name="notification_media_image_margin_end" />
@@ -3484,6 +3487,7 @@
   <java-symbol type="array" name="config_hideWhenDisabled_packageNames" />
 
   <java-symbol type="string" name="config_dozeLongPressSensorType" />
+  <java-symbol type="string" name="config_dozeUdfpsLongPressSensorType" />
   <java-symbol type="bool" name="config_dozeWakeLockScreenSensorAvailable" />
   <java-symbol type="integer" name="config_dozeWakeLockScreenDebounce" />
 
diff --git a/data/keyboards/Generic.kl b/data/keyboards/Generic.kl
index bd2d4af..6ac73b1 100644
--- a/data/keyboards/Generic.kl
+++ b/data/keyboards/Generic.kl
@@ -223,7 +223,7 @@
 key 201   MEDIA_PAUSE
 # key 202 "KEY_PROG3"
 # key 203 "KEY_PROG4"
-# key 204 (undefined)
+key 204 NOTIFICATION
 # key 205 "KEY_SUSPEND"
 # key 206 "KEY_CLOSE"
 key 207   MEDIA_PLAY
diff --git a/graphics/java/android/graphics/ImageFormat.java b/graphics/java/android/graphics/ImageFormat.java
index 15d855e..a7d3f798 100644
--- a/graphics/java/android/graphics/ImageFormat.java
+++ b/graphics/java/android/graphics/ImageFormat.java
@@ -47,6 +47,7 @@
              DEPTH16,
              DEPTH_POINT_CLOUD,
              RAW_DEPTH,
+             RAW_DEPTH10,
              PRIVATE,
              HEIC
      })
@@ -725,6 +726,15 @@
     public static final int RAW_DEPTH = 0x1002;
 
     /**
+     * Unprocessed implementation-dependent raw
+     * depth measurements, opaque with 10 bit
+     * samples and device specific bit layout.
+     *
+     * @hide
+     */
+    public static final int RAW_DEPTH10 = 0x1003;
+
+    /**
      * Android private opaque image format.
      * <p>
      * The choices of the actual format and pixel data layout are entirely up to
@@ -797,6 +807,7 @@
             case RAW_DEPTH:
             case RAW_SENSOR:
                 return 16;
+            case RAW_DEPTH10:
             case RAW10:
                 return 10;
             case RAW12:
@@ -838,6 +849,7 @@
             case DEPTH_POINT_CLOUD:
             case PRIVATE:
             case RAW_DEPTH:
+            case RAW_DEPTH10:
             case Y8:
             case DEPTH_JPEG:
             case HEIC:
diff --git a/media/java/android/media/ImageUtils.java b/media/java/android/media/ImageUtils.java
index d8a0bb3..d248f61 100644
--- a/media/java/android/media/ImageUtils.java
+++ b/media/java/android/media/ImageUtils.java
@@ -63,6 +63,7 @@
             case ImageFormat.DEPTH16:
             case ImageFormat.DEPTH_POINT_CLOUD:
             case ImageFormat.RAW_DEPTH:
+            case ImageFormat.RAW_DEPTH10:
             case ImageFormat.DEPTH_JPEG:
             case ImageFormat.HEIC:
                 return 1;
@@ -110,6 +111,10 @@
             throw new IllegalArgumentException(
                     "Copy of RAW_DEPTH format has not been implemented");
         }
+        if (src.getFormat() == ImageFormat.RAW_DEPTH10) {
+            throw new IllegalArgumentException(
+                    "Copy of RAW_DEPTH10 format has not been implemented");
+        }
         if (!(dst.getOwner() instanceof ImageWriter)) {
             throw new IllegalArgumentException("Destination image is not from ImageWriter. Only"
                     + " the images from ImageWriter are writable");
@@ -202,6 +207,7 @@
                 estimatedBytePerPixel = 1.0;
                 break;
             case ImageFormat.RAW10:
+            case ImageFormat.RAW_DEPTH10:
                 estimatedBytePerPixel = 1.25;
                 break;
             case ImageFormat.YV12:
@@ -264,6 +270,7 @@
             case ImageFormat.RAW10:
             case ImageFormat.RAW12:
             case ImageFormat.RAW_DEPTH:
+            case ImageFormat.RAW_DEPTH10:
             case ImageFormat.HEIC:
                 return new Size(image.getWidth(), image.getHeight());
             case ImageFormat.PRIVATE:
diff --git a/non-updatable-api/system-current.txt b/non-updatable-api/system-current.txt
index a403f45..a8e8cd9 100644
--- a/non-updatable-api/system-current.txt
+++ b/non-updatable-api/system-current.txt
@@ -671,6 +671,9 @@
     method @Nullable public android.content.ComponentName getAllowedNotificationAssistant();
     method public boolean isNotificationAssistantAccessGranted(@NonNull android.content.ComponentName);
     method public void setNotificationAssistantAccessGranted(@Nullable android.content.ComponentName, boolean);
+    field @RequiresPermission(android.Manifest.permission.STATUS_BAR_SERVICE) public static final String ACTION_CLOSE_NOTIFICATION_HANDLER_PANEL = "android.app.action.CLOSE_NOTIFICATION_HANDLER_PANEL";
+    field @RequiresPermission(android.Manifest.permission.STATUS_BAR_SERVICE) public static final String ACTION_OPEN_NOTIFICATION_HANDLER_PANEL = "android.app.action.OPEN_NOTIFICATION_HANDLER_PANEL";
+    field @RequiresPermission(android.Manifest.permission.STATUS_BAR_SERVICE) public static final String ACTION_TOGGLE_NOTIFICATION_HANDLER_PANEL = "android.app.action.TOGGLE_NOTIFICATION_HANDLER_PANEL";
   }
 
   public final class RuntimeAppOpAccessMessage implements android.os.Parcelable {
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
index 126f9b9..41d6afc 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
@@ -46,6 +46,7 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.List;
 
 /**
  * MediaDevice represents a media device(such like Bluetooth device, cast device and phone device).
@@ -354,6 +355,13 @@
     }
 
     /**
+     * Gets the supported features of the route.
+     */
+    public List<String> getFeatures() {
+        return mRouteInfo.getFeatures();
+    }
+
+    /**
      * Check if it is CarKit device
      * @return true if it is CarKit device
      */
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index 06a97b1..57c15e3 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -772,5 +772,12 @@
             </intent-filter>
         </receiver>
 
+        <receiver android:name=".media.dialog.MediaOutDialogReceiver"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="com.android.systemui.action.LAUNCH_MEDIA_OUTPUT_DIALOG" />
+            </intent-filter>
+        </receiver>
+
     </application>
 </manifest>
diff --git a/packages/SystemUI/res/drawable/media_output_dialog_background.xml b/packages/SystemUI/res/drawable/media_output_dialog_background.xml
new file mode 100644
index 0000000..3ceb0f6
--- /dev/null
+++ b/packages/SystemUI/res/drawable/media_output_dialog_background.xml
@@ -0,0 +1,23 @@
+<?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.
+  -->
+
+<inset xmlns:android="http://schemas.android.com/apk/res/android">
+    <shape android:shape="rectangle">
+        <corners android:radius="8dp" />
+        <solid android:color="?android:attr/colorBackground" />
+    </shape>
+</inset>
diff --git a/packages/SystemUI/res/layout/media_output_dialog.xml b/packages/SystemUI/res/layout/media_output_dialog.xml
new file mode 100644
index 0000000..0229e6e
--- /dev/null
+++ b/packages/SystemUI/res/layout/media_output_dialog.xml
@@ -0,0 +1,136 @@
+<?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.
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/media_output_dialog"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="94dp"
+        android:gravity="start|center_vertical"
+        android:paddingStart="16dp"
+        android:orientation="horizontal">
+        <ImageView
+            android:id="@+id/header_icon"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:paddingEnd="16dp"/>
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginEnd="16dp"
+            android:orientation="vertical">
+            <TextView
+                android:id="@+id/header_title"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:ellipsize="end"
+                android:maxLines="1"
+                android:textColor="?android:attr/textColorPrimary"
+                android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
+                android:textSize="20sp"/>
+
+            <TextView
+                android:id="@+id/header_subtitle"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:ellipsize="end"
+                android:maxLines="1"
+                android:fontFamily="roboto-regular"
+                android:textSize="14sp"/>
+
+        </LinearLayout>
+    </LinearLayout>
+
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="1dp"
+        android:background="?android:attr/listDivider"/>
+
+    <LinearLayout
+        android:id="@+id/device_list"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="start|center_vertical"
+        android:orientation="vertical">
+
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="12dp"/>
+
+        <include
+            layout="@layout/media_output_list_item"
+            android:id="@+id/group_item_controller"
+            android:visibility="gone"/>
+
+        <View
+            android:id="@+id/group_item_divider"
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:background="?android:attr/listDivider"
+            android:visibility="gone"/>
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/list_result"
+            android:scrollbars="vertical"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:overScrollMode="never"/>
+
+        <View
+            android:id="@+id/list_bottom_padding"
+            android:layout_width="match_parent"
+            android:layout_height="12dp"/>
+    </LinearLayout>
+
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="1dp"
+        android:background="?android:attr/listDivider"/>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+
+        <Button
+            android:id="@+id/stop"
+            style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
+            android:layout_width="wrap_content"
+            android:layout_height="64dp"
+            android:text="@string/keyboard_key_media_stop"
+            android:visibility="gone"/>
+
+        <Space
+            android:layout_weight="1"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"/>
+
+        <Button
+            android:id="@+id/done"
+            style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
+            android:layout_width="wrap_content"
+            android:layout_height="64dp"
+            android:layout_marginEnd="0dp"
+            android:text="@string/inline_done_button"/>
+    </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/media_output_list_item.xml b/packages/SystemUI/res/layout/media_output_list_item.xml
new file mode 100644
index 0000000..92d0858
--- /dev/null
+++ b/packages/SystemUI/res/layout/media_output_list_item.xml
@@ -0,0 +1,112 @@
+<?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.
+  -->
+
+<FrameLayout
+    android:id="@+id/device_container"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="64dp">
+
+    <FrameLayout
+        android:layout_width="36dp"
+        android:layout_height="36dp"
+        android:layout_gravity="center_vertical"
+        android:layout_marginStart="16dp">
+        <ImageView
+            android:id="@+id/title_icon"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"/>
+    </FrameLayout>
+
+    <TextView
+        android:id="@+id/title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_vertical"
+        android:layout_marginStart="68dp"
+        android:ellipsize="end"
+        android:maxLines="1"
+        android:textColor="?android:attr/textColorPrimary"
+        android:textSize="14sp"/>
+
+    <RelativeLayout
+        android:id="@+id/two_line_layout"
+        android:layout_width="wrap_content"
+        android:layout_height="48dp"
+        android:layout_marginStart="52dp"
+        android:layout_marginEnd="69dp"
+        android:layout_marginTop="10dp">
+        <TextView
+            android:id="@+id/two_line_title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="16dp"
+            android:layout_marginEnd="15dp"
+            android:ellipsize="end"
+            android:maxLines="1"
+            android:textColor="?android:attr/textColorPrimary"
+            android:textSize="14sp"/>
+        <TextView
+            android:id="@+id/subtitle"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="16dp"
+            android:layout_marginEnd="15dp"
+            android:layout_marginBottom="7dp"
+            android:layout_alignParentBottom="true"
+            android:ellipsize="end"
+            android:maxLines="1"
+            android:textColor="?android:attr/textColorSecondary"
+            android:textSize="12sp"
+            android:fontFamily="roboto-regular"
+            android:visibility="gone"/>
+        <ProgressBar
+            android:id="@+id/volume_indeterminate_progress"
+            style="@*android:style/Widget.Material.ProgressBar.Horizontal"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="16dp"
+            android:layout_marginEnd="15dp"
+            android:layout_marginBottom="1dp"
+            android:layout_alignParentBottom="true"
+            android:indeterminate="true"
+            android:indeterminateOnly="true"
+            android:visibility="gone"/>
+        <SeekBar
+            android:id="@+id/volume_seekbar"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_alignParentBottom="true"/>
+    </RelativeLayout>
+
+    <View
+        android:layout_width="1dp"
+        android:layout_height="36dp"
+        android:layout_marginEnd="68dp"
+        android:layout_gravity="right|center_vertical"
+        android:background="?android:attr/listDivider"
+        android:visibility="gone"/>
+
+    <ImageView
+        android:id="@+id/end_icon"
+        android:layout_width="24dp"
+        android:layout_height="24dp"
+        android:layout_gravity="right|center_vertical"
+        android:layout_marginEnd="24dp"
+        android:visibility="gone"/>
+</FrameLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values-television/config.xml b/packages/SystemUI/res/values-television/config.xml
index 6630401..981a953 100644
--- a/packages/SystemUI/res/values-television/config.xml
+++ b/packages/SystemUI/res/values-television/config.xml
@@ -29,6 +29,7 @@
         <item>com.android.systemui.util.NotificationChannels</item>
         <item>com.android.systemui.volume.VolumeUI</item>
         <item>com.android.systemui.statusbar.tv.TvStatusBar</item>
+        <item>com.android.systemui.statusbar.tv.TvNotificationPanel</item>
         <item>com.android.systemui.usb.StorageNotification</item>
         <item>com.android.systemui.power.PowerUI</item>
         <item>com.android.systemui.media.RingtonePlayer</item>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 875fe14..98e8cde 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1366,4 +1366,11 @@
     <dimen name="config_rounded_mask_size">@*android:dimen/rounded_corner_radius</dimen>
     <dimen name="config_rounded_mask_size_top">@*android:dimen/rounded_corner_radius_top</dimen>
     <dimen name="config_rounded_mask_size_bottom">@*android:dimen/rounded_corner_radius_bottom</dimen>
+
+    <!-- Output switcher panel related dimensions -->
+    <dimen name="media_output_dialog_padding_top">11dp</dimen>
+    <dimen name="media_output_dialog_list_max_height">364dp</dimen>
+    <dimen name="media_output_dialog_header_album_icon_size">52dp</dimen>
+    <dimen name="media_output_dialog_header_back_icon_size">36dp</dimen>
+    <dimen name="media_output_dialog_icon_corner_radius">16dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index e577b96..a6a3903 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2799,4 +2799,19 @@
     <string name="udfps_hbm_enable_command" translatable="false"></string>
     <!-- Device-specific payload for disabling the high-brightness mode -->
     <string name="udfps_hbm_disable_command" translatable="false"></string>
+
+    <!-- Title for the media output group dialog with media related devices [CHAR LIMIT=50] -->
+    <string name="media_output_dialog_add_output">Add outputs</string>
+    <!-- Title for the media output slice with group devices [CHAR LIMIT=50] -->
+    <string name="media_output_dialog_group">Group</string>
+    <!-- Summary for media output group with only one device which is active [CHAR LIMIT=NONE] -->
+    <string name="media_output_dialog_single_device">1 device selected</string>
+    <!-- Summary for media output group with the active device count [CHAR LIMIT=NONE] -->
+    <string name="media_output_dialog_multiple_devices"><xliff:g id="count" example="2">%1$d</xliff:g> devices selected</string>
+    <!-- Summary for disconnected status [CHAR LIMIT=50] -->
+    <string name="media_output_dialog_disconnected"><xliff:g id="device_name" example="My device">%1$s</xliff:g> (disconnected)</string>
+    <!-- Summary for connecting error message [CHAR LIMIT=NONE] -->
+    <string name="media_output_dialog_connect_failed">Couldn\'t connect. Try again.</string>
+    <!-- Title for pairing item [CHAR LIMIT=60] -->
+    <string name="media_output_dialog_pairing_new">Pair new device</string>
 </resources>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 58563f4..2b0a963 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -388,6 +388,10 @@
         <item name="android:windowIsFloating">true</item>
     </style>
 
+    <style name="Theme.SystemUI.Dialog.MediaOutput">
+        <item name="android:windowBackground">@drawable/media_output_dialog_background</item>
+    </style>
+
     <style name="QSBorderlessButton">
         <item name="android:padding">12dp</item>
         <item name="android:background">@drawable/qs_btn_borderless_rect</item>
@@ -735,5 +739,4 @@
           * Title: headline, medium 20sp
           * Message: body, 16 sp -->
     <style name="Theme.ControlsRequestDialog" parent="@*android:style/Theme.DeviceDefault.Dialog.Alert"/>
-
 </resources>
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index ea18b11..b29eff6 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -57,6 +57,7 @@
 import java.util.List;
 
 import javax.inject.Inject;
+import javax.inject.Provider;
 
 /**
  * Receives messages sent from {@link com.android.server.biometrics.BiometricService} and shows the
@@ -72,6 +73,7 @@
     private final CommandQueue mCommandQueue;
     private final StatusBarStateController mStatusBarStateController;
     private final Injector mInjector;
+    private final Provider<UdfpsController> mUdfpsControllerFactory;
 
     // TODO: These should just be saved from onSaveState
     private SomeArgs mCurrentDialogArgs;
@@ -237,6 +239,34 @@
         }
     }
 
+    /**
+     * Requests fingerprint scan.
+     *
+     * @param screenX X position of long press
+     * @param screenY Y position of long press
+     */
+    public void onAodInterrupt(int screenX, int screenY) {
+        if (mUdfpsController == null) {
+            return;
+        }
+        mUdfpsController.onAodInterrupt(screenX, screenY);
+    }
+
+    /**
+     * Cancel a fingerprint scan.
+     *
+     * The sensor that triggers an AOD interrupt for fingerprint doesn't give
+     * ACTION_UP/ACTION_CANCEL events, so the scan needs to be cancelled manually. This should be
+     * called when authentication either succeeds or fails. Failing to cancel the scan will leave
+     * the screen in high brightness mode.
+     */
+    private void onCancelAodInterrupt() {
+        if (mUdfpsController == null) {
+            return;
+        }
+        mUdfpsController.onCancelAodInterrupt();
+    }
+
     private void sendResultAndCleanUp(@DismissedReason int reason,
             @Nullable byte[] credentialAttestation) {
         if (mReceiver == null) {
@@ -263,17 +293,21 @@
 
     @Inject
     public AuthController(Context context, CommandQueue commandQueue,
-            StatusBarStateController statusBarStateController) {
-        this(context, commandQueue, statusBarStateController, new Injector());
+            StatusBarStateController statusBarStateController,
+            Provider<UdfpsController> udfpsControllerFactory) {
+        this(context, commandQueue, statusBarStateController, new Injector(),
+                udfpsControllerFactory);
     }
 
     @VisibleForTesting
     AuthController(Context context, CommandQueue commandQueue,
-            StatusBarStateController statusBarStateController, Injector injector) {
+            StatusBarStateController statusBarStateController, Injector injector,
+            Provider<UdfpsController> udfpsControllerFactory) {
         super(context);
         mCommandQueue = commandQueue;
         mStatusBarStateController = statusBarStateController;
         mInjector = injector;
+        mUdfpsControllerFactory = udfpsControllerFactory;
 
         IntentFilter filter = new IntentFilter();
         filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
@@ -294,7 +328,7 @@
                     fpm.getSensorProperties();
             for (FingerprintSensorProperties props : fingerprintSensorProperties) {
                 if (props.sensorType == FingerprintSensorProperties.TYPE_UDFPS) {
-                    mUdfpsController = new UdfpsController(mContext, mStatusBarStateController);
+                    mUdfpsController = mUdfpsControllerFactory.get();
                     break;
                 }
             }
@@ -341,6 +375,7 @@
     @Override
     public void onBiometricAuthenticated() {
         mCurrentDialog.onAuthenticationSucceeded();
+        onCancelAodInterrupt();
     }
 
     @Override
@@ -390,6 +425,7 @@
             if (DEBUG) Log.d(TAG, "onBiometricError, hard error: " + errorMessage);
             mCurrentDialog.onError(errorMessage);
         }
+        onCancelAodInterrupt();
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 82fb808..06c190f 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -46,6 +46,8 @@
 import java.io.FileWriter;
 import java.io.IOException;
 
+import javax.inject.Inject;
+
 /**
  * Shows and hides the under-display fingerprint sensor (UDFPS) overlay, handles UDFPS touch events,
  * and coordinates triggering of the high-brightness mode (HBM).
@@ -54,6 +56,7 @@
     private static final String TAG = "UdfpsController";
     // Gamma approximation for the sRGB color space.
     private static final float DISPLAY_GAMMA = 2.2f;
+    private static final long AOD_INTERRUPT_TIMEOUT_MILLIS = 1000;
 
     private final FingerprintManager mFingerprintManager;
     private final WindowManager mWindowManager;
@@ -80,6 +83,13 @@
     private final float mDefaultBrightness;
     private boolean mIsOverlayShowing;
 
+    // The fingerprint AOD trigger doesn't provide an ACTION_UP/ACTION_CANCEL event to tell us when
+    // to turn off high brightness mode. To get around this limitation, the state of the AOD
+    // interrupt is being tracked and a timeout is used as a last resort to turn off high brightness
+    // mode.
+    private boolean mIsAodInterruptActive;
+    private final Runnable mAodInterruptTimeoutAction = this::onCancelAodInterrupt;
+
     public class UdfpsOverlayController extends IUdfpsOverlayController.Stub {
         @Override
         public void showUdfpsOverlay() {
@@ -126,6 +136,7 @@
         }
     };
 
+    @Inject
     UdfpsController(@NonNull Context context,
             @NonNull StatusBarStateController statusBarStateController) {
         mFingerprintManager = context.getSystemService(FingerprintManager.class);
@@ -240,6 +251,40 @@
         return BrightnessSynchronizer.brightnessFloatToInt(scrimOpacity);
     }
 
+    /**
+     * Request fingerprint scan.
+     *
+     * This is intented to be called in response to a sensor that triggers an AOD interrupt for the
+     * fingerprint sensor.
+     */
+    void onAodInterrupt(int screenX, int screenY) {
+        if (mIsAodInterruptActive) {
+            return;
+        }
+        mIsAodInterruptActive = true;
+        // Since the sensor that triggers the AOD interrupt doesn't provide ACTION_UP/ACTION_CANCEL,
+        // we need to be careful about not letting the screen accidentally remain in high brightness
+        // mode. As a mitigation, queue a call to cancel the fingerprint scan.
+        mHandler.postDelayed(mAodInterruptTimeoutAction, AOD_INTERRUPT_TIMEOUT_MILLIS);
+        // using a hard-coded value for major and minor until it is available from the sensor
+        onFingerDown(screenX, screenY, 13.0f, 13.0f);
+    }
+
+    /**
+     * Cancel fingerprint scan.
+     *
+     * This is intented to be called after the fingerprint scan triggered by the AOD interrupt
+     * either succeeds or fails.
+     */
+    void onCancelAodInterrupt() {
+        if (!mIsAodInterruptActive) {
+            return;
+        }
+        mHandler.removeCallbacks(mAodInterruptTimeoutAction);
+        mIsAodInterruptActive = false;
+        onFingerUp();
+    }
+
     private void onFingerDown(int x, int y, float minor, float major) {
         mView.setScrimAlpha(computeScrimOpacity());
         mView.showScrimAndDot();
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index 99875f8..b610602 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -458,6 +458,8 @@
 
         mSavedBubbleKeysPerUser = new SparseSetArray<>();
         mCurrentUserId = mNotifUserManager.getCurrentUserId();
+        mBubbleData.setCurrentUserId(mCurrentUserId);
+
         mNotifUserManager.addUserChangedListener(
                 new NotificationLockscreenUserManager.UserChangedListener() {
                     @Override
@@ -466,6 +468,7 @@
                         mBubbleData.dismissAll(DISMISS_USER_CHANGED);
                         BubbleController.this.restoreBubbles(newUserId);
                         mCurrentUserId = newUserId;
+                        mBubbleData.setCurrentUserId(newUserId);
                     }
                 });
 
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
index a747db6..bab18ec 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleData.java
@@ -35,6 +35,7 @@
 import com.android.systemui.R;
 import com.android.systemui.bubbles.BubbleController.DismissReason;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.shared.system.SysUiStatsLog;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 
@@ -61,6 +62,8 @@
 
     private BubbleLoggerImpl mLogger = new BubbleLoggerImpl();
 
+    private int mCurrentUserId;
+
     private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleData" : TAG_BUBBLES;
 
     private static final Comparator<Bubble> BUBBLES_BY_SORT_KEY_DESCENDING =
@@ -617,6 +620,10 @@
         mStateChange.selectionChanged = true;
     }
 
+    void setCurrentUserId(int uid) {
+        mCurrentUserId = uid;
+    }
+
     /**
      * Logs the bubble UI event.
      *
@@ -634,7 +641,9 @@
         if (provider == null) {
             mLogger.logStackUiChanged(packageName, action, bubbleCount, normalX, normalY);
         } else if (provider.getKey().equals(BubbleOverflow.KEY)) {
-            mLogger.logShowOverflow(packageName, action, bubbleCount, normalX, normalY);
+            if (action == SysUiStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED) {
+                mLogger.logShowOverflow(packageName, mCurrentUserId);
+            }
         } else {
             mLogger.logBubbleUiChanged((Bubble) provider, packageName, action, bubbleCount, normalX,
                     normalY, bubbleIndex);
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleLogger.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleLogger.java
index 1e6eb8c..86ba8c5 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleLogger.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleLogger.java
@@ -53,7 +53,10 @@
         BUBBLE_OVERFLOW_REMOVE_BACK_TO_STACK(489),
 
         @UiEvent(doc = "User blocked notification from bubbling, remove bubble from overflow.")
-        BUBBLE_OVERFLOW_REMOVE_BLOCKED(490);
+        BUBBLE_OVERFLOW_REMOVE_BLOCKED(490),
+
+        @UiEvent(doc = "User selected the overflow.")
+        BUBBLE_OVERFLOW_SELECTED(600);
 
         private final int mId;
 
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleLoggerImpl.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleLoggerImpl.java
index d702cc4..2d90c86 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleLoggerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleLoggerImpl.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.bubbles;
 
+import android.os.UserHandle;
+
 import com.android.internal.logging.UiEventLoggerImpl;
 import com.android.systemui.shared.system.SysUiStatsLog;
 
@@ -31,11 +33,7 @@
      * @param e UI event
      */
     public void log(Bubble b, UiEventEnum e) {
-        if (b.getInstanceId() == null) {
-            // Added from persistence -- TODO log this with specific event?
-            return;
-        }
-        logWithInstanceId(e, b.getAppUid(), b.getPackageName(), b.getInstanceId());
+        super.log(e, b.getUser().getIdentifier(), b.getPackageName());
     }
 
     /**
@@ -82,20 +80,9 @@
                 false /* isAppForeground (unused) */);
     }
 
-    void logShowOverflow(String packageName, int action, int bubbleCount, float normalX,
-            float normalY) {
-        SysUiStatsLog.write(SysUiStatsLog.BUBBLE_UI_CHANGED,
-                packageName,
-                BubbleOverflow.KEY  /* notification channel */,
-                0 /* notification ID */,
-                0 /* bubble position */,
-                bubbleCount,
-                action,
-                normalX,
-                normalY,
-                false /* unread bubble */,
-                false /* on-going bubble */,
-                false /* isAppForeground (unused) */);
+    void logShowOverflow(String packageName, int currentUserId) {
+        super.log(BubbleLogger.Event.BUBBLE_OVERFLOW_SELECTED, currentUserId,
+                packageName);
     }
 
     void logBubbleUiChanged(Bubble bubble, String packageName, int action, int bubbleCount,
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java
index 6e8d63b..307362f 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultBroadcastReceiverBinder.java
@@ -18,6 +18,7 @@
 
 import android.content.BroadcastReceiver;
 
+import com.android.systemui.media.dialog.MediaOutDialogReceiver;
 import com.android.systemui.screenshot.ActionProxyReceiver;
 import com.android.systemui.screenshot.DeleteScreenshotReceiver;
 import com.android.systemui.screenshot.SmartActionsReceiver;
@@ -59,4 +60,13 @@
     public abstract BroadcastReceiver bindSmartActionsReceiver(
             SmartActionsReceiver broadcastReceiver);
 
+    /**
+     *
+     */
+    @Binds
+    @IntoMap
+    @ClassKey(MediaOutDialogReceiver.class)
+    public abstract BroadcastReceiver bindMediaOutDialogReceiver(
+            MediaOutDialogReceiver broadcastReceiver);
+
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java
index 3a5ce4d..1f6288a 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIBinder.java
@@ -35,6 +35,7 @@
 import com.android.systemui.statusbar.dagger.StatusBarModule;
 import com.android.systemui.statusbar.notification.InstantAppNotifier;
 import com.android.systemui.statusbar.phone.StatusBar;
+import com.android.systemui.statusbar.tv.TvNotificationPanel;
 import com.android.systemui.statusbar.tv.TvStatusBar;
 import com.android.systemui.theme.ThemeOverlayController;
 import com.android.systemui.toast.ToastUI;
@@ -156,6 +157,12 @@
     @ClassKey(TvStatusBar.class)
     public abstract SystemUI bindsTvStatusBar(TvStatusBar sysui);
 
+    /** Inject into TvNotificationPanel. */
+    @Binds
+    @IntoMap
+    @ClassKey(TvNotificationPanel.class)
+    public abstract SystemUI bindsTvNotificationPanel(TvNotificationPanel sysui);
+
     /** Inject into VolumeUI. */
     @Binds
     @IntoMap
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
index 99d2651..424a824 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeLog.java
@@ -339,6 +339,7 @@
             case PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN: return "wakelockscreen";
             case REASON_SENSOR_WAKE_UP: return "wakeup";
             case REASON_SENSOR_TAP: return "tap";
+            case REASON_SENSOR_UDFPS_LONG_PRESS: return "udfps";
             default: throw new IllegalArgumentException("invalid reason: " + pulseReason);
         }
     }
@@ -347,7 +348,8 @@
     @IntDef({PULSE_REASON_NONE, PULSE_REASON_INTENT, PULSE_REASON_NOTIFICATION,
             PULSE_REASON_SENSOR_SIGMOTION, REASON_SENSOR_PICKUP, REASON_SENSOR_DOUBLE_TAP,
             PULSE_REASON_SENSOR_LONG_PRESS, PULSE_REASON_DOCKING, REASON_SENSOR_WAKE_UP,
-            PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN, REASON_SENSOR_TAP})
+            PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN, REASON_SENSOR_TAP,
+            REASON_SENSOR_UDFPS_LONG_PRESS})
     public @interface Reason {}
     public static final int PULSE_REASON_NONE = -1;
     public static final int PULSE_REASON_INTENT = 0;
@@ -360,6 +362,7 @@
     public static final int REASON_SENSOR_WAKE_UP = 7;
     public static final int PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN = 8;
     public static final int REASON_SENSOR_TAP = 9;
+    public static final int REASON_SENSOR_UDFPS_LONG_PRESS = 10;
 
-    public static final int TOTAL_REASONS = 10;
+    public static final int TOTAL_REASONS = 11;
 }
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
index 524d9c8..028870f 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeSensors.java
@@ -150,6 +150,15 @@
                         true /* reports touch coordinates */,
                         true /* touchscreen */,
                         dozeLog),
+                new TriggerSensor(
+                        findSensorWithType(config.udfpsLongPressSensorType()),
+                        Settings.Secure.DOZE_PULSE_ON_LONG_PRESS,
+                        false /* settingDef */,
+                        true /* configured */,
+                        DozeLog.REASON_SENSOR_UDFPS_LONG_PRESS,
+                        true /* reports touch coordinates */,
+                        true /* touchscreen */,
+                        dozeLog),
                 new PluginSensor(
                         new SensorManagerPlugin.Sensor(TYPE_WAKE_DISPLAY),
                         Settings.Secure.DOZE_WAKE_DISPLAY_GESTURE,
diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
index 8364b48..45e5c61 100644
--- a/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
+++ b/packages/SystemUI/src/com/android/systemui/doze/DozeTriggers.java
@@ -39,6 +39,7 @@
 import com.android.internal.logging.UiEventLoggerImpl;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.systemui.Dependency;
+import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.doze.dagger.DozeScope;
@@ -93,6 +94,7 @@
     private final DockManager mDockManager;
     private final ProximitySensor.ProximityCheck mProxCheck;
     private final BroadcastDispatcher mBroadcastDispatcher;
+    private final AuthController mAuthController;
 
     private long mNotificationPulseTime;
     private boolean mPulsePending;
@@ -165,7 +167,7 @@
             WakeLock wakeLock, DockManager dockManager,
             ProximitySensor proximitySensor, ProximitySensor.ProximityCheck proxCheck,
             DozeLog dozeLog, BroadcastDispatcher broadcastDispatcher,
-            SecureSettings secureSettings) {
+            SecureSettings secureSettings, AuthController authController) {
         mContext = context;
         mDozeHost = dozeHost;
         mConfig = config;
@@ -181,6 +183,7 @@
         mProxCheck = proxCheck;
         mDozeLog = dozeLog;
         mBroadcastDispatcher = broadcastDispatcher;
+        mAuthController = authController;
     }
 
     @Override
@@ -256,6 +259,7 @@
         boolean isLongPress = pulseReason == DozeLog.PULSE_REASON_SENSOR_LONG_PRESS;
         boolean isWakeDisplay = pulseReason == DozeLog.REASON_SENSOR_WAKE_UP;
         boolean isWakeLockScreen = pulseReason == DozeLog.PULSE_REASON_SENSOR_WAKE_LOCK_SCREEN;
+        boolean isUdfpsLongPress = pulseReason == DozeLog.REASON_SENSOR_UDFPS_LONG_PRESS;
         boolean wakeEvent = rawValues != null && rawValues.length > 0 && rawValues[0] != 0;
 
         if (isWakeDisplay) {
@@ -281,6 +285,11 @@
                     gentleWakeUp(pulseReason);
                 } else if (isPickup) {
                     gentleWakeUp(pulseReason);
+                } else if (isUdfpsLongPress) {
+                    gentleWakeUp(pulseReason);
+                    // Since the gesture won't be received by the UDFPS view, manually inject an
+                    // event.
+                    mAuthController.onAodInterrupt((int) screenX, (int) screenY);
                 } else {
                     mDozeHost.extendPulse(pulseReason);
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutDialogReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutDialogReceiver.kt
new file mode 100644
index 0000000..d607713
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutDialogReceiver.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.dialog
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.text.TextUtils
+import com.android.settingslib.media.MediaOutputSliceConstants
+import javax.inject.Inject
+
+/**
+ * BroadcastReceiver for handling media output intent
+ */
+class MediaOutDialogReceiver @Inject constructor(
+    private var mediaOutputDialogFactory: MediaOutputDialogFactory
+) : BroadcastReceiver() {
+    override fun onReceive(context: Context, intent: Intent) {
+        if (TextUtils.equals(MediaOutputSliceConstants.ACTION_LAUNCH_MEDIA_OUTPUT_DIALOG,
+                        intent.action)) {
+            mediaOutputDialogFactory.create(
+                    intent.getStringExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME), false)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
new file mode 100644
index 0000000..9fc64d5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media.dialog;
+
+import static android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE;
+
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.drawable.Drawable;
+import android.text.SpannableString;
+import android.text.style.ForegroundColorSpan;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.NonNull;
+
+import com.android.settingslib.Utils;
+import com.android.settingslib.media.LocalMediaManager.MediaDeviceState;
+import com.android.settingslib.media.MediaDevice;
+import com.android.systemui.R;
+
+import java.util.List;
+
+/**
+ * Adapter for media output dialog.
+ */
+public class MediaOutputAdapter extends MediaOutputBaseAdapter {
+
+    private static final String TAG = "MediaOutputAdapter";
+    private static final int PAIR_NEW = 1;
+
+    public MediaOutputAdapter(MediaOutputController controller) {
+        super(controller);
+    }
+
+    @Override
+    public MediaDeviceBaseViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup,
+            int viewType) {
+        super.onCreateViewHolder(viewGroup, viewType);
+
+        return new MediaDeviceViewHolder(mHolderView);
+    }
+
+    @Override
+    public void onBindViewHolder(@NonNull MediaDeviceBaseViewHolder viewHolder, int position) {
+        if (mController.isZeroMode() && position == (mController.getMediaDevices().size())) {
+            viewHolder.onBind(PAIR_NEW);
+        } else if (position < (mController.getMediaDevices().size())) {
+            viewHolder.onBind(((List<MediaDevice>) (mController.getMediaDevices())).get(position));
+        } else {
+            Log.d(TAG, "Incorrect position: " + position);
+        }
+    }
+
+    @Override
+    public int getItemCount() {
+        if (mController.isZeroMode()) {
+            // Add extra one for "pair new"
+            return mController.getMediaDevices().size() + 1;
+        }
+        return mController.getMediaDevices().size();
+    }
+
+    void onItemClick(MediaDevice device) {
+        mController.connectDevice(device);
+        device.setState(MediaDeviceState.STATE_CONNECTING);
+        notifyDataSetChanged();
+    }
+
+    void onItemClick(int customizedItem) {
+        if (customizedItem == PAIR_NEW) {
+            mController.launchBluetoothPairing();
+        }
+    }
+
+    @Override
+    CharSequence getItemTitle(MediaDevice device) {
+        if (device.getDeviceType() == MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE
+                && !device.isConnected()) {
+            final CharSequence deviceName = device.getName();
+            // Append status to title only for the disconnected Bluetooth device.
+            final SpannableString spannableTitle = new SpannableString(
+                    mContext.getString(R.string.media_output_dialog_disconnected, deviceName));
+            spannableTitle.setSpan(new ForegroundColorSpan(
+                    Utils.getColorAttrDefaultColor(mContext, android.R.attr.textColorSecondary)),
+                    deviceName.length(),
+                    spannableTitle.length(), SPAN_EXCLUSIVE_EXCLUSIVE);
+            return spannableTitle;
+        }
+        return super.getItemTitle(device);
+    }
+
+    class MediaDeviceViewHolder extends MediaDeviceBaseViewHolder {
+
+        MediaDeviceViewHolder(View view) {
+            super(view);
+        }
+
+        @Override
+        void onBind(MediaDevice device) {
+            super.onBind(device);
+            if (mController.isTransferring()) {
+                if (device.getState() == MediaDeviceState.STATE_CONNECTING
+                        && !mController.hasAdjustVolumeUserRestriction()) {
+                    setTwoLineLayout(device, true);
+                    mProgressBar.setVisibility(View.VISIBLE);
+                    mSeekBar.setVisibility(View.GONE);
+                    mSubTitleText.setVisibility(View.GONE);
+                } else {
+                    setSingleLineLayout(getItemTitle(device), false);
+                }
+            } else {
+                // Set different layout for each device
+                if (device.getState() == MediaDeviceState.STATE_CONNECTING_FAILED) {
+                    setTwoLineLayout(device, false);
+                    mSubTitleText.setVisibility(View.VISIBLE);
+                    mSeekBar.setVisibility(View.GONE);
+                    mProgressBar.setVisibility(View.GONE);
+                    mSubTitleText.setText(R.string.media_output_dialog_connect_failed);
+                    mFrameLayout.setOnClickListener(v -> onItemClick(device));
+                } else if (!mController.hasAdjustVolumeUserRestriction()
+                        && isCurrentConnected(device)) {
+                    setTwoLineLayout(device, true);
+                    mSeekBar.setVisibility(View.VISIBLE);
+                    mProgressBar.setVisibility(View.GONE);
+                    mSubTitleText.setVisibility(View.GONE);
+                    initSeekbar(device);
+                } else {
+                    setSingleLineLayout(getItemTitle(device), false);
+                    mFrameLayout.setOnClickListener(v -> onItemClick(device));
+                }
+            }
+        }
+
+        @Override
+        void onBind(int customizedItem) {
+            if (customizedItem == PAIR_NEW) {
+                setSingleLineLayout(mContext.getText(R.string.media_output_dialog_pairing_new),
+                        false);
+                final Drawable d = mContext.getDrawable(R.drawable.ic_add);
+                d.setColorFilter(new PorterDuffColorFilter(
+                        Utils.getColorAccentDefaultColor(mContext), PorterDuff.Mode.SRC_IN));
+                mTitleIcon.setImageDrawable(d);
+                mFrameLayout.setOnClickListener(v -> onItemClick(PAIR_NEW));
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
new file mode 100644
index 0000000..7579c25
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java
@@ -0,0 +1,165 @@
+/*
+ * 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.dialog;
+
+import android.content.Context;
+import android.graphics.Typeface;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.settingslib.media.MediaDevice;
+import com.android.systemui.R;
+
+/**
+ * Base adapter for media output dialog.
+ */
+public abstract class MediaOutputBaseAdapter extends
+        RecyclerView.Adapter<MediaOutputBaseAdapter.MediaDeviceBaseViewHolder> {
+
+    private static final String FONT_SELECTED_TITLE = "sans-serif-medium";
+    private static final String FONT_TITLE = "sans-serif";
+
+    final MediaOutputController mController;
+
+    private boolean mIsDragging;
+
+    Context mContext;
+    View mHolderView;
+
+    public MediaOutputBaseAdapter(MediaOutputController controller) {
+        mController = controller;
+        mIsDragging = false;
+    }
+
+    @Override
+    public MediaDeviceBaseViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup,
+            int viewType) {
+        mContext = viewGroup.getContext();
+        mHolderView = LayoutInflater.from(mContext).inflate(R.layout.media_output_list_item,
+                viewGroup, false);
+
+        return null;
+    }
+
+    CharSequence getItemTitle(MediaDevice device) {
+        return device.getName();
+    }
+
+    boolean isCurrentConnected(MediaDevice device) {
+        return TextUtils.equals(device.getId(),
+                mController.getCurrentConnectedMediaDevice().getId());
+    }
+
+    boolean isDragging() {
+        return mIsDragging;
+    }
+
+    /**
+     * ViewHolder for binding device view.
+     */
+    abstract class MediaDeviceBaseViewHolder extends RecyclerView.ViewHolder {
+        final FrameLayout mFrameLayout;
+        final TextView mTitleText;
+        final TextView mTwoLineTitleText;
+        final TextView mSubTitleText;
+        final ImageView mTitleIcon;
+        final ImageView mEndIcon;
+        final ProgressBar mProgressBar;
+        final SeekBar mSeekBar;
+        final RelativeLayout mTwoLineLayout;
+
+        MediaDeviceBaseViewHolder(View view) {
+            super(view);
+            mFrameLayout = view.requireViewById(R.id.device_container);
+            mTitleText = view.requireViewById(R.id.title);
+            mSubTitleText = view.requireViewById(R.id.subtitle);
+            mTwoLineLayout = view.requireViewById(R.id.two_line_layout);
+            mTwoLineTitleText = view.requireViewById(R.id.two_line_title);
+            mTitleIcon = view.requireViewById(R.id.title_icon);
+            mEndIcon = view.requireViewById(R.id.end_icon);
+            mProgressBar = view.requireViewById(R.id.volume_indeterminate_progress);
+            mSeekBar = view.requireViewById(R.id.volume_seekbar);
+        }
+
+        void onBind(MediaDevice device) {
+            mTitleIcon.setImageIcon(mController.getDeviceIconCompat(device).toIcon(mContext));
+        }
+
+        void onBind(int customizedItem) { }
+
+        void setSingleLineLayout(CharSequence title, boolean bFocused) {
+            mTitleText.setVisibility(View.VISIBLE);
+            mTwoLineLayout.setVisibility(View.GONE);
+            mTitleText.setText(title);
+            if (bFocused) {
+                mTitleText.setTypeface(Typeface.create(FONT_SELECTED_TITLE, Typeface.NORMAL));
+            } else {
+                mTitleText.setTypeface(Typeface.create(FONT_TITLE, Typeface.NORMAL));
+            }
+        }
+
+        void setTwoLineLayout(MediaDevice device, boolean bFocused) {
+            mTitleText.setVisibility(View.GONE);
+            mTwoLineLayout.setVisibility(View.VISIBLE);
+            mTwoLineTitleText.setText(getItemTitle(device));
+            if (bFocused) {
+                mTwoLineTitleText.setTypeface(Typeface.create(FONT_SELECTED_TITLE,
+                        Typeface.NORMAL));
+            } else {
+                mTwoLineTitleText.setTypeface(Typeface.create(FONT_TITLE, Typeface.NORMAL));
+            }
+        }
+
+        void initSeekbar(MediaDevice device) {
+            mSeekBar.setMax(device.getMaxVolume());
+            mSeekBar.setMin(0);
+            if (mSeekBar.getProgress() != device.getCurrentVolume()) {
+                mSeekBar.setProgress(device.getCurrentVolume());
+            }
+            mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+                @Override
+                public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+                    if (device == null || !fromUser) {
+                        return;
+                    }
+                    mController.adjustVolume(device, progress);
+                }
+
+                @Override
+                public void onStartTrackingTouch(SeekBar seekBar) {
+                    mIsDragging = true;
+                }
+
+                @Override
+                public void onStopTrackingTouch(SeekBar seekBar) {
+                    mIsDragging = false;
+                }
+            });
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
new file mode 100644
index 0000000..781bf8d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
@@ -0,0 +1,220 @@
+/*
+ * 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.dialog;
+
+import static android.view.WindowInsets.Type.navigationBars;
+import static android.view.WindowInsets.Type.statusBars;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.TextUtils;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.Window;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.settingslib.R;
+import com.android.systemui.statusbar.phone.SystemUIDialog;
+
+/**
+ * Base dialog for media output UI
+ */
+public abstract class MediaOutputBaseDialog extends SystemUIDialog implements
+        MediaOutputController.Callback {
+
+    private static final String TAG = "MediaOutputDialog";
+
+    private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
+    private final RecyclerView.LayoutManager mLayoutManager;
+
+    final Context mContext;
+    final MediaOutputController mMediaOutputController;
+
+    @VisibleForTesting
+    View mDialogView;
+    private TextView mHeaderTitle;
+    private TextView mHeaderSubtitle;
+    private ImageView mHeaderIcon;
+    private RecyclerView mDevicesRecyclerView;
+    private LinearLayout mDeviceListLayout;
+    private Button mDoneButton;
+    private Button mStopButton;
+    private View mListBottomPadding;
+    private int mListMaxHeight;
+
+    MediaOutputBaseAdapter mAdapter;
+    FrameLayout mGroupItemController;
+    View mGroupDivider;
+
+    private final ViewTreeObserver.OnGlobalLayoutListener mDeviceListLayoutListener = () -> {
+        // Set max height for list
+        if (mDeviceListLayout.getHeight() > mListMaxHeight) {
+            ViewGroup.LayoutParams params = mDeviceListLayout.getLayoutParams();
+            params.height = mListMaxHeight;
+            mDeviceListLayout.setLayoutParams(params);
+        }
+    };
+
+    public MediaOutputBaseDialog(Context context, MediaOutputController mediaOutputController) {
+        super(context, R.style.Theme_SystemUI_Dialog_MediaOutput);
+        mContext = context;
+        mMediaOutputController = mediaOutputController;
+        mLayoutManager = new LinearLayoutManager(mContext);
+        mListMaxHeight = context.getResources().getDimensionPixelSize(
+                R.dimen.media_output_dialog_list_max_height);
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        mDialogView = LayoutInflater.from(mContext).inflate(R.layout.media_output_dialog, null);
+        final Window window = getWindow();
+        final WindowManager.LayoutParams lp = window.getAttributes();
+        lp.gravity = Gravity.BOTTOM;
+        // Config insets to make sure the layout is above the navigation bar
+        lp.setFitInsetsTypes(statusBars() | navigationBars());
+        lp.setFitInsetsSides(WindowInsets.Side.all());
+        lp.setFitInsetsIgnoringVisibility(true);
+        window.setAttributes(lp);
+        window.setContentView(mDialogView);
+        window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+
+        mHeaderTitle = mDialogView.requireViewById(R.id.header_title);
+        mHeaderSubtitle = mDialogView.requireViewById(R.id.header_subtitle);
+        mHeaderIcon = mDialogView.requireViewById(R.id.header_icon);
+        mDevicesRecyclerView = mDialogView.requireViewById(R.id.list_result);
+        mGroupItemController = mDialogView.requireViewById(R.id.group_item_controller);
+        mGroupDivider = mDialogView.requireViewById(R.id.group_item_divider);
+        mDeviceListLayout = mDialogView.requireViewById(R.id.device_list);
+        mDoneButton = mDialogView.requireViewById(R.id.done);
+        mStopButton = mDialogView.requireViewById(R.id.stop);
+        mListBottomPadding = mDialogView.requireViewById(R.id.list_bottom_padding);
+
+        mDeviceListLayout.getViewTreeObserver().addOnGlobalLayoutListener(
+                mDeviceListLayoutListener);
+        // Init device list
+        mDevicesRecyclerView.setLayoutManager(mLayoutManager);
+        mDevicesRecyclerView.setAdapter(mAdapter);
+        // Init bottom buttons
+        mDoneButton.setOnClickListener(v -> dismiss());
+        mStopButton.setOnClickListener(v -> {
+            mMediaOutputController.releaseSession();
+            dismiss();
+        });
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        mMediaOutputController.start(this);
+    }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+        mMediaOutputController.stop();
+    }
+
+    @VisibleForTesting
+    void refresh() {
+        // Update header icon
+        final int iconRes = getHeaderIconRes();
+        final IconCompat iconCompat = getHeaderIcon();
+        if (iconRes != 0) {
+            mHeaderIcon.setVisibility(View.VISIBLE);
+            mHeaderIcon.setImageResource(iconRes);
+        } else if (iconCompat != null) {
+            mHeaderIcon.setVisibility(View.VISIBLE);
+            mHeaderIcon.setImageIcon(iconCompat.toIcon(mContext));
+        } else {
+            mHeaderIcon.setVisibility(View.GONE);
+        }
+        if (mHeaderIcon.getVisibility() == View.VISIBLE) {
+            final int size = getHeaderIconSize();
+            mHeaderIcon.setLayoutParams(new LinearLayout.LayoutParams(size, size));
+        }
+        // Update title and subtitle
+        mHeaderTitle.setText(getHeaderText());
+        final CharSequence subTitle = getHeaderSubtitle();
+        if (TextUtils.isEmpty(subTitle)) {
+            mHeaderSubtitle.setVisibility(View.GONE);
+            mHeaderTitle.setGravity(Gravity.START | Gravity.CENTER_VERTICAL);
+        } else {
+            mHeaderSubtitle.setVisibility(View.VISIBLE);
+            mHeaderSubtitle.setText(subTitle);
+            mHeaderTitle.setGravity(Gravity.NO_GRAVITY);
+        }
+        if (!mAdapter.isDragging()) {
+            mAdapter.notifyDataSetChanged();
+        }
+        // Add extra padding when device amount is less than 6
+        if (mMediaOutputController.getMediaDevices().size() < 6) {
+            mListBottomPadding.setVisibility(View.VISIBLE);
+        } else {
+            mListBottomPadding.setVisibility(View.GONE);
+        }
+    }
+
+    abstract int getHeaderIconRes();
+
+    abstract IconCompat getHeaderIcon();
+
+    abstract int getHeaderIconSize();
+
+    abstract CharSequence getHeaderText();
+
+    abstract CharSequence getHeaderSubtitle();
+
+    @Override
+    public void onMediaChanged() {
+        mMainThreadHandler.post(() -> refresh());
+    }
+
+    @Override
+    public void onMediaStoppedOrPaused() {
+        if (isShowing()) {
+            dismiss();
+        }
+    }
+
+    @Override
+    public void onRouteChanged() {
+        mMainThreadHandler.post(() -> refresh());
+    }
+
+    @Override
+    public void dismissDialog() {
+        dismiss();
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
new file mode 100644
index 0000000..64d20a27
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -0,0 +1,445 @@
+/*
+ * 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.dialog;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.MediaMetadata;
+import android.media.RoutingSessionInfo;
+import android.media.session.MediaController;
+import android.media.session.MediaSessionManager;
+import android.media.session.PlaybackState;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.core.graphics.drawable.IconCompat;
+
+import com.android.settingslib.RestrictedLockUtilsInternal;
+import com.android.settingslib.Utils;
+import com.android.settingslib.bluetooth.BluetoothUtils;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.media.InfoMediaManager;
+import com.android.settingslib.media.LocalMediaManager;
+import com.android.settingslib.media.MediaDevice;
+import com.android.settingslib.media.MediaOutputSliceConstants;
+import com.android.settingslib.utils.ThreadUtils;
+import com.android.systemui.R;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.statusbar.phone.ShadeController;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import javax.inject.Inject;
+
+/**
+ * Controller for media output dialog
+ */
+public class MediaOutputController implements LocalMediaManager.DeviceCallback{
+
+    private static final String TAG = "MediaOutputController";
+    private static final boolean DEBUG = false;
+
+    private final String mPackageName;
+    private final Context mContext;
+    private final MediaSessionManager mMediaSessionManager;
+    private final ShadeController mShadeController;
+    private final ActivityStarter mActivityStarter;
+    @VisibleForTesting
+    final List<MediaDevice> mMediaDevices = new CopyOnWriteArrayList<>();
+
+    private MediaController mMediaController;
+    @VisibleForTesting
+    Callback mCallback;
+    @VisibleForTesting
+    LocalMediaManager mLocalMediaManager;
+
+    @Inject
+    public MediaOutputController(@NonNull Context context, String packageName,
+            MediaSessionManager mediaSessionManager, LocalBluetoothManager
+            lbm, ShadeController shadeController, ActivityStarter starter) {
+        mContext = context;
+        mPackageName = packageName;
+        mMediaSessionManager = mediaSessionManager;
+        mShadeController = shadeController;
+        mActivityStarter = starter;
+        InfoMediaManager imm = new InfoMediaManager(mContext, packageName, null, lbm);
+        mLocalMediaManager = new LocalMediaManager(mContext, lbm, imm, packageName);
+    }
+
+    void start(@NonNull Callback cb) {
+        mMediaDevices.clear();
+        if (!TextUtils.isEmpty(mPackageName)) {
+            for (MediaController controller : mMediaSessionManager.getActiveSessions(null)) {
+                if (TextUtils.equals(controller.getPackageName(), mPackageName)) {
+                    mMediaController = controller;
+                    mMediaController.unregisterCallback(mCb);
+                    mMediaController.registerCallback(mCb);
+                    break;
+                }
+            }
+        }
+        if (mMediaController == null) {
+            if (DEBUG) {
+                Log.d(TAG, "No media controller for " + mPackageName);
+            }
+        }
+        if (mLocalMediaManager == null) {
+            if (DEBUG) {
+                Log.d(TAG, "No local media manager " + mPackageName);
+            }
+            return;
+        }
+        mCallback = cb;
+        mLocalMediaManager.unregisterCallback(this);
+        mLocalMediaManager.stopScan();
+        mLocalMediaManager.registerCallback(this);
+        mLocalMediaManager.startScan();
+    }
+
+    void stop() {
+        if (mMediaController != null) {
+            mMediaController.unregisterCallback(mCb);
+        }
+        if (mLocalMediaManager != null) {
+            mLocalMediaManager.unregisterCallback(this);
+            mLocalMediaManager.stopScan();
+        }
+        mMediaDevices.clear();
+    }
+
+    @Override
+    public void onDeviceListUpdate(List<MediaDevice> devices) {
+        buildMediaDevices(devices);
+        mCallback.onRouteChanged();
+    }
+
+    @Override
+    public void onSelectedDeviceStateChanged(MediaDevice device,
+            @LocalMediaManager.MediaDeviceState int state) {
+        mCallback.onRouteChanged();
+    }
+
+    @Override
+    public void onDeviceAttributesChanged() {
+        mCallback.onRouteChanged();
+    }
+
+    @Override
+    public void onRequestFailed(int reason) {
+        mCallback.onRouteChanged();
+    }
+
+    CharSequence getHeaderTitle() {
+        if (mMediaController != null) {
+            final MediaMetadata metadata = mMediaController.getMetadata();
+            if (metadata != null) {
+                return metadata.getDescription().getTitle();
+            }
+        }
+        return mContext.getText(R.string.controls_media_title);
+    }
+
+    CharSequence getHeaderSubTitle() {
+        if (mMediaController == null) {
+            return null;
+        }
+        final MediaMetadata metadata = mMediaController.getMetadata();
+        if (metadata == null) {
+            return null;
+        }
+        return metadata.getDescription().getSubtitle();
+    }
+
+    IconCompat getHeaderIcon() {
+        if (mMediaController == null) {
+            return null;
+        }
+        final MediaMetadata metadata = mMediaController.getMetadata();
+        if (metadata != null) {
+            final Bitmap bitmap = metadata.getDescription().getIconBitmap();
+            if (bitmap != null) {
+                final Bitmap roundBitmap = Utils.convertCornerRadiusBitmap(mContext, bitmap,
+                        (float) mContext.getResources().getDimensionPixelSize(
+                                R.dimen.media_output_dialog_icon_corner_radius));
+                return IconCompat.createWithBitmap(roundBitmap);
+            }
+        }
+        if (DEBUG) {
+            Log.d(TAG, "Media meta data does not contain icon information");
+        }
+        return getPackageIcon();
+    }
+
+    IconCompat getDeviceIconCompat(MediaDevice device) {
+        Drawable drawable = device.getIcon();
+        if (drawable == null) {
+            if (DEBUG) {
+                Log.d(TAG, "getDeviceIconCompat() device : " + device.getName()
+                        + ", drawable is null");
+            }
+            // Use default Bluetooth device icon to handle getIcon() is null case.
+            drawable = mContext.getDrawable(com.android.internal.R.drawable.ic_bt_headphones_a2dp);
+        }
+        return BluetoothUtils.createIconWithDrawable(drawable);
+    }
+
+    private IconCompat getPackageIcon() {
+        if (TextUtils.isEmpty(mPackageName)) {
+            return null;
+        }
+        try {
+            final Drawable drawable = mContext.getPackageManager().getApplicationIcon(mPackageName);
+            if (drawable instanceof BitmapDrawable) {
+                return IconCompat.createWithBitmap(((BitmapDrawable) drawable).getBitmap());
+            }
+            final Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
+                    drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
+            final Canvas canvas = new Canvas(bitmap);
+            drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+            drawable.draw(canvas);
+            return IconCompat.createWithBitmap(bitmap);
+        } catch (PackageManager.NameNotFoundException e) {
+            if (DEBUG) {
+                Log.e(TAG, "Package is not found. Unable to get package icon.");
+            }
+        }
+        return null;
+    }
+
+    private void buildMediaDevices(List<MediaDevice> devices) {
+        // For the first time building list, to make sure the top device is the connected device.
+        if (mMediaDevices.isEmpty()) {
+            final MediaDevice connectedMediaDevice = getCurrentConnectedMediaDevice();
+            if (connectedMediaDevice == null) {
+                if (DEBUG) {
+                    Log.d(TAG, "No connected media device.");
+                }
+                mMediaDevices.addAll(devices);
+                return;
+            }
+            for (MediaDevice device : devices) {
+                if (TextUtils.equals(device.getId(), connectedMediaDevice.getId())) {
+                    mMediaDevices.add(0, device);
+                } else {
+                    mMediaDevices.add(device);
+                }
+            }
+            return;
+        }
+        // To keep the same list order
+        final Collection<MediaDevice> targetMediaDevices = new ArrayList<>();
+        for (MediaDevice originalDevice : mMediaDevices) {
+            for (MediaDevice newDevice : devices) {
+                if (TextUtils.equals(originalDevice.getId(), newDevice.getId())) {
+                    targetMediaDevices.add(newDevice);
+                    break;
+                }
+            }
+        }
+        if (targetMediaDevices.size() != devices.size()) {
+            devices.removeAll(targetMediaDevices);
+            targetMediaDevices.addAll(devices);
+        }
+        mMediaDevices.clear();
+        mMediaDevices.addAll(targetMediaDevices);
+    }
+
+    void connectDevice(MediaDevice device) {
+        ThreadUtils.postOnBackgroundThread(() -> {
+            mLocalMediaManager.connectDevice(device);
+        });
+    }
+
+    Collection<MediaDevice> getMediaDevices() {
+        return mMediaDevices;
+    }
+
+    MediaDevice getCurrentConnectedMediaDevice() {
+        return mLocalMediaManager.getCurrentConnectedDevice();
+    }
+
+    private MediaDevice getMediaDeviceById(String id) {
+        return mLocalMediaManager.getMediaDeviceById(new ArrayList<>(mMediaDevices), id);
+    }
+
+    boolean addDeviceToPlayMedia(MediaDevice device) {
+        return mLocalMediaManager.addDeviceToPlayMedia(device);
+    }
+
+    boolean removeDeviceFromPlayMedia(MediaDevice device) {
+        return mLocalMediaManager.removeDeviceFromPlayMedia(device);
+    }
+
+    List<MediaDevice> getSelectableMediaDevice() {
+        return mLocalMediaManager.getSelectableMediaDevice();
+    }
+
+    List<MediaDevice> getSelectedMediaDevice() {
+        return mLocalMediaManager.getSelectedMediaDevice();
+    }
+
+    List<MediaDevice> getDeselectableMediaDevice() {
+        return mLocalMediaManager.getDeselectableMediaDevice();
+    }
+
+    boolean isDeviceIncluded(Collection<MediaDevice> deviceCollection, MediaDevice targetDevice) {
+        for (MediaDevice device : deviceCollection) {
+            if (TextUtils.equals(device.getId(), targetDevice.getId())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    void adjustSessionVolume(String sessionId, int volume) {
+        mLocalMediaManager.adjustSessionVolume(sessionId, volume);
+    }
+
+    void adjustSessionVolume(int volume) {
+        mLocalMediaManager.adjustSessionVolume(volume);
+    }
+
+    int getSessionVolumeMax() {
+        return mLocalMediaManager.getSessionVolumeMax();
+    }
+
+    int getSessionVolume() {
+        return mLocalMediaManager.getSessionVolume();
+    }
+
+    CharSequence getSessionName() {
+        return mLocalMediaManager.getSessionName();
+    }
+
+    void releaseSession() {
+        mLocalMediaManager.releaseSession();
+    }
+
+    List<RoutingSessionInfo> getActiveRemoteMediaDevices() {
+        final List<RoutingSessionInfo> sessionInfos = new ArrayList<>();
+        for (RoutingSessionInfo info : mLocalMediaManager.getActiveMediaSession()) {
+            if (!info.isSystemSession()) {
+                sessionInfos.add(info);
+            }
+        }
+        return sessionInfos;
+    }
+
+    void adjustVolume(MediaDevice device, int volume) {
+        ThreadUtils.postOnBackgroundThread(() -> {
+            device.requestSetVolume(volume);
+        });
+    }
+
+    String getPackageName() {
+        return mPackageName;
+    }
+
+    boolean hasAdjustVolumeUserRestriction() {
+        if (RestrictedLockUtilsInternal.checkIfRestrictionEnforced(
+                mContext, UserManager.DISALLOW_ADJUST_VOLUME, UserHandle.myUserId()) != null) {
+            return true;
+        }
+        final UserManager um = mContext.getSystemService(UserManager.class);
+        return um.hasBaseUserRestriction(UserManager.DISALLOW_ADJUST_VOLUME,
+                UserHandle.of(UserHandle.myUserId()));
+    }
+
+    boolean isTransferring() {
+        for (MediaDevice device : mMediaDevices) {
+            if (device.getState() == LocalMediaManager.MediaDeviceState.STATE_CONNECTING) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    boolean isZeroMode() {
+        if (mMediaDevices.size() == 1) {
+            final MediaDevice device = mMediaDevices.iterator().next();
+            // Add "pair new" only when local output device exists
+            final int type = device.getDeviceType();
+            if (type == MediaDevice.MediaDeviceType.TYPE_PHONE_DEVICE
+                    || type == MediaDevice.MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE
+                    || type == MediaDevice.MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    void launchBluetoothPairing() {
+        mCallback.dismissDialog();
+        final ActivityStarter.OnDismissAction postKeyguardAction = () -> {
+            mContext.sendBroadcast(new Intent()
+                    .setAction(MediaOutputSliceConstants.ACTION_LAUNCH_BLUETOOTH_PAIRING)
+                    .setPackage(MediaOutputSliceConstants.SETTINGS_PACKAGE_NAME));
+            mShadeController.animateCollapsePanels();
+            return true;
+        };
+        mActivityStarter.dismissKeyguardThenExecute(postKeyguardAction, null, true);
+    }
+
+    private final MediaController.Callback mCb = new MediaController.Callback() {
+        @Override
+        public void onMetadataChanged(MediaMetadata metadata) {
+            mCallback.onMediaChanged();
+        }
+
+        @Override
+        public void onPlaybackStateChanged(PlaybackState playbackState) {
+            final int state = playbackState.getState();
+            if (state == PlaybackState.STATE_STOPPED || state == PlaybackState.STATE_PAUSED) {
+                mCallback.onMediaStoppedOrPaused();
+            }
+        }
+    };
+
+    interface Callback {
+        /**
+         * Override to handle the media content updating.
+         */
+        void onMediaChanged();
+
+        /**
+         * Override to handle the media state updating.
+         */
+        void onMediaStoppedOrPaused();
+
+        /**
+         * Override to handle the device updating.
+         */
+        void onRouteChanged();
+
+        /**
+         * Override to dismiss dialog.
+         */
+        void dismissDialog();
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
new file mode 100644
index 0000000..ac9d8ce
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
@@ -0,0 +1,77 @@
+/*
+ * 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.dialog;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.view.View;
+import android.view.WindowManager;
+
+import androidx.core.graphics.drawable.IconCompat;
+
+import com.android.systemui.R;
+import com.android.systemui.dagger.SysUISingleton;
+
+/**
+ * Dialog for media output transferring.
+ */
+@SysUISingleton
+public class MediaOutputDialog extends MediaOutputBaseDialog {
+
+    MediaOutputDialog(Context context, boolean aboveStatusbar, MediaOutputController
+            mediaOutputController) {
+        super(context, mediaOutputController);
+        mAdapter = new MediaOutputAdapter(mMediaOutputController);
+        if (!aboveStatusbar) {
+            getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
+        }
+        show();
+    }
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mGroupItemController.setVisibility(View.GONE);
+        mGroupDivider.setVisibility(View.GONE);
+    }
+
+    @Override
+    int getHeaderIconRes() {
+        return 0;
+    }
+
+    @Override
+    IconCompat getHeaderIcon() {
+        return mMediaOutputController.getHeaderIcon();
+    }
+
+    @Override
+    int getHeaderIconSize() {
+        return mContext.getResources().getDimensionPixelSize(
+                R.dimen.media_output_dialog_header_album_icon_size);
+    }
+
+    @Override
+    CharSequence getHeaderText() {
+        return mMediaOutputController.getHeaderTitle();
+    }
+
+    @Override
+    CharSequence getHeaderSubtitle() {
+        return mMediaOutputController.getHeaderSubTitle();
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
new file mode 100644
index 0000000..bc1dca5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.dialog
+
+import android.content.Context
+import android.media.session.MediaSessionManager
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.statusbar.phone.ShadeController
+import javax.inject.Inject
+
+/**
+ * Factory to create [MediaOutputDialog] objects.
+ */
+class MediaOutputDialogFactory @Inject constructor(
+    private val context: Context,
+    private val mediaSessionManager: MediaSessionManager,
+    private val lbm: LocalBluetoothManager?,
+    private val shadeController: ShadeController,
+    private val starter: ActivityStarter
+) {
+    /** Creates a [MediaOutputDialog] for the given package. */
+    fun create(packageName: String, aboveStatusBar: Boolean) {
+        MediaOutputController(context, packageName, mediaSessionManager, lbm, shadeController,
+                starter).run {
+            MediaOutputDialog(context, aboveStatusBar, this) }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvNotificationPanel.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvNotificationPanel.java
new file mode 100644
index 0000000..7a78c15
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvNotificationPanel.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.tv;
+
+import android.Manifest;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.UserHandle;
+import android.util.Log;
+
+import com.android.systemui.SystemUI;
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.statusbar.CommandQueue;
+
+import javax.inject.Inject;
+
+/**
+ * Offers control methods for the notification panel handler on TV devices.
+ */
+@SysUISingleton
+public class TvNotificationPanel extends SystemUI implements CommandQueue.Callbacks {
+    private static final String TAG = "TvNotificationPanel";
+    private final CommandQueue mCommandQueue;
+    private final String mNotificationHandlerPackage;
+
+    @Inject
+    public TvNotificationPanel(Context context, CommandQueue commandQueue) {
+        super(context);
+        mCommandQueue = commandQueue;
+        mNotificationHandlerPackage = mContext.getResources().getString(
+                com.android.internal.R.string.config_notificationHandlerPackage);
+    }
+
+    @Override
+    public void start() {
+        mCommandQueue.addCallback(this);
+    }
+
+    @Override
+    public void togglePanel() {
+        if (!mNotificationHandlerPackage.isEmpty()) {
+            startNotificationHandlerActivity(
+                    new Intent(NotificationManager.ACTION_TOGGLE_NOTIFICATION_HANDLER_PANEL));
+        }
+    }
+
+    @Override
+    public void animateExpandNotificationsPanel() {
+        if (!mNotificationHandlerPackage.isEmpty()) {
+            startNotificationHandlerActivity(
+                    new Intent(NotificationManager.ACTION_OPEN_NOTIFICATION_HANDLER_PANEL));
+        }
+    }
+
+    @Override
+    public void animateCollapsePanels(int flags, boolean force) {
+        if (!mNotificationHandlerPackage.isEmpty()
+                && (flags & CommandQueue.FLAG_EXCLUDE_NOTIFICATION_PANEL) == 0) {
+            Intent closeNotificationIntent = new Intent(
+                    NotificationManager.ACTION_CLOSE_NOTIFICATION_HANDLER_PANEL);
+            closeNotificationIntent.setPackage(mNotificationHandlerPackage);
+            mContext.sendBroadcastAsUser(closeNotificationIntent, UserHandle.CURRENT);
+        }
+    }
+
+    /**
+     * Starts the activity intent if all of the following are true
+     * <ul>
+     * <li> the notification handler package is a system component </li>
+     * <li> the provided intent is handled by the notification handler package </li>
+     * <li> the notification handler requests the
+     * {@link android.Manifest.permission#STATUS_BAR_SERVICE} permission for the given intent</li>
+     * </ul>
+     *
+     * @param intent The intent for starting the desired activity
+     */
+    private void startNotificationHandlerActivity(Intent intent) {
+        intent.setPackage(mNotificationHandlerPackage);
+        PackageManager pm = mContext.getPackageManager();
+        ResolveInfo ri = pm.resolveActivity(intent, PackageManager.MATCH_SYSTEM_ONLY);
+        if (ri != null && ri.activityInfo != null) {
+            if (ri.activityInfo.permission != null && ri.activityInfo.permission.equals(
+                    Manifest.permission.STATUS_BAR_SERVICE)) {
+                mContext.startActivityAsUser(intent, UserHandle.CURRENT);
+            } else {
+                Log.e(TAG,
+                        "Not launching notification handler activity: Notification handler does "
+                                + "not require the STATUS_BAR_SERVICE permission for intent "
+                                + intent.getAction());
+            }
+        } else {
+            Log.e(TAG,
+                    "Not launching notification handler activity: Could not resolve activityInfo "
+                            + "for intent "
+                            + intent.getAction());
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java
index bcfff60..2795857 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/tv/TvStatusBar.java
@@ -17,13 +17,9 @@
 package com.android.systemui.statusbar.tv;
 
 import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
 import android.os.Bundle;
 import android.os.RemoteException;
 import android.os.ServiceManager;
-import android.os.UserHandle;
 
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.systemui.R;
@@ -49,9 +45,6 @@
 @SysUISingleton
 public class TvStatusBar extends SystemUI implements CommandQueue.Callbacks {
 
-    private static final String ACTION_OPEN_TV_NOTIFICATIONS_PANEL =
-            "com.android.tv.action.OPEN_NOTIFICATIONS_PANEL";
-
     private final CommandQueue mCommandQueue;
     private final Lazy<AssistManager> mAssistManagerLazy;
 
@@ -74,24 +67,11 @@
             // If the system process isn't there we're doomed anyway.
         }
 
-        if  (mContext.getResources().getBoolean(R.bool.audio_recording_disclosure_enabled)) {
+        if (mContext.getResources().getBoolean(R.bool.audio_recording_disclosure_enabled)) {
             // Creating AudioRecordingDisclosureBar and just letting it run
             new AudioRecordingDisclosureBar(mContext);
         }
-    }
 
-    @Override
-    public void animateExpandNotificationsPanel() {
-        startSystemActivity(new Intent(ACTION_OPEN_TV_NOTIFICATIONS_PANEL));
-    }
-
-    private void startSystemActivity(Intent intent) {
-        PackageManager pm = mContext.getPackageManager();
-        ResolveInfo ri = pm.resolveActivity(intent, PackageManager.MATCH_SYSTEM_ONLY);
-        if (ri != null && ri.activityInfo != null) {
-            intent.setPackage(ri.activityInfo.packageName);
-            mContext.startActivityAsUser(intent, UserHandle.CURRENT);
-        }
     }
 
     @Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
index c8566c5..7cebc9f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
@@ -46,6 +46,7 @@
 import android.hardware.biometrics.PromptInfo;
 import android.hardware.face.FaceManager;
 import android.hardware.fingerprint.FingerprintManager;
+import android.hardware.fingerprint.FingerprintSensorProperties;
 import android.os.Bundle;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.testing.AndroidTestingRunner;
@@ -69,6 +70,8 @@
 import java.util.List;
 import java.util.Random;
 
+import javax.inject.Provider;
+
 @RunWith(AndroidTestingRunner.class)
 @RunWithLooper
 @SmallTest
@@ -82,6 +85,14 @@
     private AuthDialog mDialog1;
     @Mock
     private AuthDialog mDialog2;
+    @Mock
+    private CommandQueue mCommandQueue;
+    @Mock
+    private StatusBarStateController mStatusBarStateController;
+    @Mock
+    private FingerprintManager mFingerprintManager;
+    @Mock
+    private UdfpsController mUdfpsController;
 
     private TestableAuthController mAuthController;
 
@@ -104,8 +115,16 @@
         when(mDialog1.isAllowDeviceCredentials()).thenReturn(false);
         when(mDialog2.isAllowDeviceCredentials()).thenReturn(false);
 
-        mAuthController = new TestableAuthController(context, mock(CommandQueue.class),
-                mock(StatusBarStateController.class), new MockInjector());
+        when(mFingerprintManager.isHardwareDetected()).thenReturn(true);
+        FingerprintSensorProperties prop = new FingerprintSensorProperties(
+                1, FingerprintSensorProperties.TYPE_UDFPS, true, 1);
+        List<FingerprintSensorProperties> props = new ArrayList<>();
+        props.add(prop);
+        when(mFingerprintManager.getSensorProperties()).thenReturn(props);
+
+        mAuthController = new TestableAuthController(context, mCommandQueue,
+                mStatusBarStateController, new MockInjector(),
+                () -> mUdfpsController);
 
         mAuthController.start();
     }
@@ -463,6 +482,27 @@
                 eq(null) /* credentialAttestation */);
     }
 
+    @Test
+    public void testOnAodInterrupt() {
+        final int pos = 10;
+        mAuthController.onAodInterrupt(pos, pos);
+        verify(mUdfpsController).onAodInterrupt(eq(pos), eq(pos));
+    }
+
+    @Test
+    public void testOnBiometricAuthenticated_OnCancelAodInterrupt() {
+        showDialog(Authenticators.BIOMETRIC_WEAK, BiometricPrompt.TYPE_FINGERPRINT);
+        mAuthController.onBiometricAuthenticated();
+        verify(mUdfpsController).onCancelAodInterrupt();
+    }
+
+    @Test
+    public void testOnBiometricError_OnCancelAodInterrupt() {
+        showDialog(Authenticators.BIOMETRIC_WEAK, BiometricPrompt.TYPE_FINGERPRINT);
+        mAuthController.onBiometricError(0, 0, 0);
+        verify(mUdfpsController).onCancelAodInterrupt();
+    }
+
     // Helpers
 
     private void showDialog(int authenticators, int biometricModality) {
@@ -504,8 +544,10 @@
         private PromptInfo mLastBiometricPromptInfo;
 
         TestableAuthController(Context context, CommandQueue commandQueue,
-                StatusBarStateController statusBarStateController, Injector injector) {
-            super(context, commandQueue, statusBarStateController, injector);
+                StatusBarStateController statusBarStateController, Injector injector,
+                Provider<UdfpsController> udfpsControllerFactory) {
+            super(context, commandQueue, statusBarStateController, injector,
+                    udfpsControllerFactory);
         }
 
         @Override
@@ -536,7 +578,7 @@
 
         @Override
         FingerprintManager getFingerprintManager(Context context) {
-            return mock(FingerprintManager.class);
+            return mFingerprintManager;
         }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeConfigurationUtil.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeConfigurationUtil.java
index c591c1b..9fd9b47 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeConfigurationUtil.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeConfigurationUtil.java
@@ -59,6 +59,7 @@
         when(config.doubleTapSensorType()).thenReturn(null);
         when(config.tapSensorType()).thenReturn(null);
         when(config.longPressSensorType()).thenReturn(null);
+        when(config.udfpsLongPressSensorType()).thenReturn(null);
 
         when(config.tapGestureEnabled(anyInt())).thenReturn(true);
         when(config.tapSensorAvailable()).thenReturn(true);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
index 1ed5871..3ae02a4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/doze/DozeTriggersTest.java
@@ -37,6 +37,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.statusbar.phone.DozeParameters;
@@ -76,6 +77,8 @@
     private DockManager mDockManager;
     @Mock
     private ProximitySensor.ProximityCheck mProximityCheck;
+    @Mock
+    private AuthController mAuthController;
 
     private DozeTriggers mTriggers;
     private FakeSensorManager mSensors;
@@ -100,7 +103,8 @@
 
         mTriggers = new DozeTriggers(mContext, mHost, mAlarmManager, config, parameters,
                 asyncSensorManager, wakeLock, mDockManager, mProximitySensor,
-                mProximityCheck, mock(DozeLog.class), mBroadcastDispatcher, new FakeSettings());
+                mProximityCheck, mock(DozeLog.class), mBroadcastDispatcher, new FakeSettings(),
+                mAuthController);
         mTriggers.setDozeMachine(mMachine);
         waitForSensorManager();
     }
@@ -186,6 +190,15 @@
         mTriggers.onSensor(DozeLog.REASON_SENSOR_TAP, 100, 100, null);
     }
 
+    @Test
+    public void testOnSensor_Fingerprint() {
+        final int screenX = 100;
+        final int screenY = 100;
+        final int reason = DozeLog.REASON_SENSOR_UDFPS_LONG_PRESS;
+        mTriggers.onSensor(reason, screenX, screenY, null);
+        verify(mAuthController).onAodInterrupt(eq(screenX), eq(screenY));
+    }
+
     private void waitForSensorManager() {
         mExecutor.runAllReady();
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
new file mode 100644
index 0000000..0e376bd
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
@@ -0,0 +1,200 @@
+/*
+ * 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.dialog;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.graphics.drawable.Icon;
+import android.testing.AndroidTestingRunner;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.filters.SmallTest;
+
+import com.android.settingslib.media.LocalMediaManager;
+import com.android.settingslib.media.MediaDevice;
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class MediaOutputAdapterTest extends SysuiTestCase {
+
+    private static final String TEST_DEVICE_NAME_1 = "test_device_name_1";
+    private static final String TEST_DEVICE_NAME_2 = "test_device_name_2";
+    private static final String TEST_DEVICE_ID_1 = "test_device_id_1";
+    private static final String TEST_DEVICE_ID_2 = "test_device_id_2";
+
+    // Mock
+    private MediaOutputController mMediaOutputController = mock(MediaOutputController.class);
+    private MediaDevice mMediaDevice1 = mock(MediaDevice.class);
+    private MediaDevice mMediaDevice2 = mock(MediaDevice.class);
+    private Icon mIcon = mock(Icon.class);
+    private IconCompat mIconCompat = mock(IconCompat.class);
+
+    private MediaOutputAdapter mMediaOutputAdapter;
+    private MediaOutputAdapter.MediaDeviceViewHolder mViewHolder;
+    private List<MediaDevice> mMediaDevices = new ArrayList<>();
+
+    @Before
+    public void setUp() {
+        mMediaOutputAdapter = new MediaOutputAdapter(mMediaOutputController);
+        mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter
+                .onCreateViewHolder(new FrameLayout(mContext), 0);
+
+        when(mMediaOutputController.getMediaDevices()).thenReturn(mMediaDevices);
+        when(mMediaOutputController.hasAdjustVolumeUserRestriction()).thenReturn(false);
+        when(mMediaOutputController.isZeroMode()).thenReturn(false);
+        when(mMediaOutputController.isTransferring()).thenReturn(false);
+        when(mMediaOutputController.getDeviceIconCompat(mMediaDevice1)).thenReturn(mIconCompat);
+        when(mMediaOutputController.getDeviceIconCompat(mMediaDevice2)).thenReturn(mIconCompat);
+        when(mMediaOutputController.getCurrentConnectedMediaDevice()).thenReturn(mMediaDevice1);
+        when(mIconCompat.toIcon(mContext)).thenReturn(mIcon);
+        when(mMediaDevice1.getName()).thenReturn(TEST_DEVICE_NAME_1);
+        when(mMediaDevice1.getId()).thenReturn(TEST_DEVICE_ID_1);
+        when(mMediaDevice2.getName()).thenReturn(TEST_DEVICE_NAME_2);
+        when(mMediaDevice2.getId()).thenReturn(TEST_DEVICE_ID_2);
+        when(mMediaDevice1.getState()).thenReturn(
+                LocalMediaManager.MediaDeviceState.STATE_CONNECTED);
+        when(mMediaDevice2.getState()).thenReturn(
+                LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED);
+        mMediaDevices.add(mMediaDevice1);
+        mMediaDevices.add(mMediaDevice2);
+    }
+
+    @Test
+    public void getItemCount_nonZeroMode_isDeviceSize() {
+        assertThat(mMediaOutputAdapter.getItemCount()).isEqualTo(mMediaDevices.size());
+    }
+
+    @Test
+    public void getItemCount_zeroMode_containExtraOneForPairNew() {
+        when(mMediaOutputController.isZeroMode()).thenReturn(true);
+
+        assertThat(mMediaOutputAdapter.getItemCount()).isEqualTo(mMediaDevices.size() + 1);
+    }
+
+    @Test
+    public void onBindViewHolder_zeroMode_bindPairNew_verifyView() {
+        when(mMediaOutputController.isZeroMode()).thenReturn(true);
+        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 2);
+
+        assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mTitleText.getText()).isEqualTo(mContext.getText(
+                R.string.media_output_dialog_pairing_new));
+    }
+
+    @Test
+    public void onBindViewHolder_bindConnectedDevice_verifyView() {
+        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
+
+        assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mTwoLineTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mViewHolder.mTwoLineTitleText.getText()).isEqualTo(TEST_DEVICE_NAME_1);
+    }
+
+    @Test
+    public void onBindViewHolder_bindDisconnectedBluetoothDevice_verifyView() {
+        when(mMediaDevice2.getDeviceType()).thenReturn(
+                MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE);
+        when(mMediaDevice2.isConnected()).thenReturn(false);
+        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
+
+        assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(
+                mContext.getString(R.string.media_output_dialog_disconnected, TEST_DEVICE_NAME_2));
+    }
+
+    @Test
+    public void onBindViewHolder_bindFailedStateDevice_verifyView() {
+        when(mMediaDevice2.getState()).thenReturn(
+                LocalMediaManager.MediaDeviceState.STATE_CONNECTING_FAILED);
+        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
+
+        assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mViewHolder.mTwoLineTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mViewHolder.mSubTitleText.getText()).isEqualTo(mContext.getText(
+                R.string.media_output_dialog_connect_failed));
+        assertThat(mViewHolder.mTwoLineTitleText.getText()).isEqualTo(TEST_DEVICE_NAME_2);
+    }
+
+    @Test
+    public void onBindViewHolder_inTransferring_bindTransferringDevice_verifyView() {
+        when(mMediaOutputController.isTransferring()).thenReturn(true);
+        when(mMediaDevice1.getState()).thenReturn(
+                LocalMediaManager.MediaDeviceState.STATE_CONNECTING);
+        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
+
+        assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mViewHolder.mTwoLineTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mViewHolder.mTwoLineTitleText.getText()).isEqualTo(TEST_DEVICE_NAME_1);
+    }
+
+    @Test
+    public void onBindViewHolder_inTransferring_bindNonTransferringDevice_verifyView() {
+        when(mMediaOutputController.isTransferring()).thenReturn(true);
+        when(mMediaDevice2.getState()).thenReturn(
+                LocalMediaManager.MediaDeviceState.STATE_CONNECTING);
+        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
+
+        assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mTitleText.getText()).isEqualTo(TEST_DEVICE_NAME_1);
+    }
+
+    @Test
+    public void onItemClick_clickPairNew_verifyLaunchBluetoothPairing() {
+        when(mMediaOutputController.isZeroMode()).thenReturn(true);
+        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 2);
+        mViewHolder.mFrameLayout.performClick();
+
+        verify(mMediaOutputController).launchBluetoothPairing();
+    }
+
+    @Test
+    public void onItemClick_clickDevice_verifyConnectDevice() {
+        assertThat(mMediaDevice2.getState()).isEqualTo(
+                LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED);
+        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
+        mViewHolder.mFrameLayout.performClick();
+
+        verify(mMediaOutputController).connectDevice(mMediaDevice2);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
new file mode 100644
index 0000000..42b21c6
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
@@ -0,0 +1,212 @@
+/*
+ * 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.dialog;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.media.session.MediaSessionManager;
+import android.os.Bundle;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.core.graphics.drawable.IconCompat;
+import androidx.test.filters.SmallTest;
+
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.media.MediaDevice;
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.statusbar.phone.ShadeController;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class MediaOutputBaseDialogTest extends SysuiTestCase {
+
+    private static final String TEST_PACKAGE = "test_package";
+
+    // Mock
+    private MediaOutputBaseAdapter mMediaOutputBaseAdapter = mock(MediaOutputBaseAdapter.class);
+
+    private MediaSessionManager mMediaSessionManager = mock(MediaSessionManager.class);
+    private LocalBluetoothManager mLocalBluetoothManager = mock(LocalBluetoothManager.class);
+    private ShadeController mShadeController = mock(ShadeController.class);
+    private ActivityStarter mStarter = mock(ActivityStarter.class);
+
+    private MediaOutputBaseDialogImpl mMediaOutputBaseDialogImpl;
+    private MediaOutputController mMediaOutputController;
+    private int mHeaderIconRes;
+    private IconCompat mIconCompat;
+    private CharSequence mHeaderTitle;
+    private CharSequence mHeaderSubtitle;
+
+    @Before
+    public void setUp() {
+        mMediaOutputController = new MediaOutputController(mContext, TEST_PACKAGE,
+                mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter);
+        mMediaOutputBaseDialogImpl = new MediaOutputBaseDialogImpl(mContext,
+                mMediaOutputController);
+        mMediaOutputBaseDialogImpl.onCreate(new Bundle());
+    }
+
+    @Test
+    public void refresh_withIconRes_iconIsVisible() {
+        mHeaderIconRes = 1;
+        mMediaOutputBaseDialogImpl.refresh();
+        final ImageView view = mMediaOutputBaseDialogImpl.mDialogView.requireViewById(
+                R.id.header_icon);
+
+        assertThat(view.getVisibility()).isEqualTo(View.VISIBLE);
+    }
+
+    @Test
+    public void refresh_withIconCompat_iconIsVisible() {
+        mIconCompat = mock(IconCompat.class);
+        mMediaOutputBaseDialogImpl.refresh();
+        final ImageView view = mMediaOutputBaseDialogImpl.mDialogView.requireViewById(
+                R.id.header_icon);
+
+        assertThat(view.getVisibility()).isEqualTo(View.VISIBLE);
+    }
+
+    @Test
+    public void refresh_noIcon_iconLayoutNotVisible() {
+        mHeaderIconRes = 0;
+        mIconCompat = null;
+        mMediaOutputBaseDialogImpl.refresh();
+        final ImageView view = mMediaOutputBaseDialogImpl.mDialogView.requireViewById(
+                R.id.header_icon);
+
+        assertThat(view.getVisibility()).isEqualTo(View.GONE);
+    }
+
+    @Test
+    public void refresh_checkTitle() {
+        mHeaderTitle = "test_string";
+
+        mMediaOutputBaseDialogImpl.refresh();
+        final TextView titleView = mMediaOutputBaseDialogImpl.mDialogView.requireViewById(
+                R.id.header_title);
+
+        assertThat(titleView.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(titleView.getText()).isEqualTo(mHeaderTitle);
+    }
+
+    @Test
+    public void refresh_withSubtitle_checkSubtitle() {
+        mHeaderSubtitle = "test_string";
+
+        mMediaOutputBaseDialogImpl.refresh();
+        final TextView subtitleView = mMediaOutputBaseDialogImpl.mDialogView.requireViewById(
+                R.id.header_subtitle);
+
+        assertThat(subtitleView.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(subtitleView.getText()).isEqualTo(mHeaderSubtitle);
+    }
+
+    @Test
+    public void refresh_noSubtitle_checkSubtitle() {
+        mMediaOutputBaseDialogImpl.refresh();
+        final TextView subtitleView = mMediaOutputBaseDialogImpl.mDialogView.requireViewById(
+                R.id.header_subtitle);
+
+        assertThat(subtitleView.getVisibility()).isEqualTo(View.GONE);
+    }
+
+    @Test
+    public void refresh_inDragging_notUpdateAdapter() {
+        when(mMediaOutputBaseAdapter.isDragging()).thenReturn(true);
+        mMediaOutputBaseDialogImpl.refresh();
+
+        verify(mMediaOutputBaseAdapter, never()).notifyDataSetChanged();
+    }
+
+    @Test
+    public void refresh_notInDragging_verifyUpdateAdapter() {
+        when(mMediaOutputBaseAdapter.isDragging()).thenReturn(false);
+        mMediaOutputBaseDialogImpl.refresh();
+
+        verify(mMediaOutputBaseAdapter).notifyDataSetChanged();
+    }
+
+    @Test
+    public void refresh_with6Devices_checkBottomPaddingVisibility() {
+        for (int i = 0; i < 6; i++) {
+            mMediaOutputController.mMediaDevices.add(mock(MediaDevice.class));
+        }
+        mMediaOutputBaseDialogImpl.refresh();
+        final View view = mMediaOutputBaseDialogImpl.mDialogView.requireViewById(
+                R.id.list_bottom_padding);
+
+        assertThat(view.getVisibility()).isEqualTo(View.GONE);
+    }
+
+    @Test
+    public void refresh_with5Devices_checkBottomPaddingVisibility() {
+        for (int i = 0; i < 5; i++) {
+            mMediaOutputController.mMediaDevices.add(mock(MediaDevice.class));
+        }
+        mMediaOutputBaseDialogImpl.refresh();
+        final View view = mMediaOutputBaseDialogImpl.mDialogView.requireViewById(
+                R.id.list_bottom_padding);
+
+        assertThat(view.getVisibility()).isEqualTo(View.VISIBLE);
+    }
+
+    class MediaOutputBaseDialogImpl extends MediaOutputBaseDialog {
+
+        MediaOutputBaseDialogImpl(Context context, MediaOutputController mediaOutputController) {
+            super(context, mediaOutputController);
+
+            mAdapter = mMediaOutputBaseAdapter;
+        }
+
+        int getHeaderIconRes() {
+            return mHeaderIconRes;
+        }
+
+        IconCompat getHeaderIcon() {
+            return mIconCompat;
+        }
+
+        int getHeaderIconSize() {
+            return 10;
+        }
+
+        CharSequence getHeaderText() {
+            return mHeaderTitle;
+        }
+
+        CharSequence getHeaderSubtitle() {
+            return mHeaderSubtitle;
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
new file mode 100644
index 0000000..0dcdecf
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
@@ -0,0 +1,348 @@
+/*
+ * 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.dialog;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.media.MediaDescription;
+import android.media.MediaMetadata;
+import android.media.RoutingSessionInfo;
+import android.media.session.MediaController;
+import android.media.session.MediaSessionManager;
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.media.LocalMediaManager;
+import com.android.settingslib.media.MediaDevice;
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.statusbar.phone.ShadeController;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class MediaOutputControllerTest extends SysuiTestCase {
+
+    private static final String TEST_PACKAGE_NAME = "com.test.package.name";
+    private static final String TEST_DEVICE_1_ID = "test_device_1_id";
+    private static final String TEST_DEVICE_2_ID = "test_device_2_id";
+    private static final String TEST_ARTIST = "test_artist";
+    private static final String TEST_SONG = "test_song";
+    private static final String TEST_SESSION_ID = "test_session_id";
+    private static final String TEST_SESSION_NAME = "test_session_name";
+    // Mock
+    private MediaController mMediaController = mock(MediaController.class);
+    private MediaSessionManager mMediaSessionManager = mock(MediaSessionManager.class);
+    private CachedBluetoothDeviceManager mCachedBluetoothDeviceManager =
+            mock(CachedBluetoothDeviceManager.class);
+    private LocalBluetoothManager mLocalBluetoothManager = mock(LocalBluetoothManager.class);
+    private MediaOutputController.Callback mCb = mock(MediaOutputController.Callback.class);
+    private MediaDevice mMediaDevice1 = mock(MediaDevice.class);
+    private MediaDevice mMediaDevice2 = mock(MediaDevice.class);
+    private MediaMetadata mMediaMetadata = mock(MediaMetadata.class);
+    private RoutingSessionInfo mRemoteSessionInfo = mock(RoutingSessionInfo.class);
+    private ShadeController mShadeController = mock(ShadeController.class);
+    private ActivityStarter mStarter = mock(ActivityStarter.class);
+
+    private Context mSpyContext;
+    private MediaOutputController mMediaOutputController;
+    private LocalMediaManager mLocalMediaManager;
+    private List<MediaController> mMediaControllers = new ArrayList<>();
+    private List<MediaDevice> mMediaDevices = new ArrayList<>();
+    private MediaDescription mMediaDescription;
+    private List<RoutingSessionInfo> mRoutingSessionInfos = new ArrayList<>();
+
+    @Before
+    public void setUp() {
+        mSpyContext = spy(mContext);
+        when(mMediaController.getPackageName()).thenReturn(TEST_PACKAGE_NAME);
+        mMediaControllers.add(mMediaController);
+        when(mMediaSessionManager.getActiveSessions(any())).thenReturn(mMediaControllers);
+        doReturn(mMediaSessionManager).when(mSpyContext).getSystemService(
+                MediaSessionManager.class);
+        when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(
+                mCachedBluetoothDeviceManager);
+        mMediaOutputController = new MediaOutputController(mSpyContext, TEST_PACKAGE_NAME,
+                mMediaSessionManager, mLocalBluetoothManager, mShadeController, mStarter);
+        mLocalMediaManager = spy(mMediaOutputController.mLocalMediaManager);
+        mMediaOutputController.mLocalMediaManager = mLocalMediaManager;
+        MediaDescription.Builder builder = new MediaDescription.Builder();
+        builder.setTitle(TEST_SONG);
+        builder.setSubtitle(TEST_ARTIST);
+        mMediaDescription = builder.build();
+        when(mMediaMetadata.getDescription()).thenReturn(mMediaDescription);
+        when(mMediaDevice1.getId()).thenReturn(TEST_DEVICE_1_ID);
+        when(mMediaDevice2.getId()).thenReturn(TEST_DEVICE_2_ID);
+        mMediaDevices.add(mMediaDevice1);
+        mMediaDevices.add(mMediaDevice2);
+    }
+
+    @Test
+    public void start_verifyLocalMediaManagerInit() {
+        mMediaOutputController.start(mCb);
+
+        verify(mLocalMediaManager).registerCallback(mMediaOutputController);
+        verify(mLocalMediaManager).startScan();
+    }
+
+    @Test
+    public void stop_verifyLocalMediaManagerDeinit() {
+        mMediaOutputController.start(mCb);
+        reset(mLocalMediaManager);
+
+        mMediaOutputController.stop();
+
+        verify(mLocalMediaManager).unregisterCallback(mMediaOutputController);
+        verify(mLocalMediaManager).stopScan();
+    }
+
+    @Test
+    public void start_withPackageName_verifyMediaControllerInit() {
+        mMediaOutputController.start(mCb);
+
+        verify(mMediaController).registerCallback(any());
+    }
+
+    @Test
+    public void start_withoutPackageName_verifyMediaControllerInit() {
+        mMediaOutputController = new MediaOutputController(mSpyContext, null, mMediaSessionManager,
+                mLocalBluetoothManager, mShadeController, mStarter);
+
+        mMediaOutputController.start(mCb);
+
+        verify(mMediaController, never()).registerCallback(any());
+    }
+
+    @Test
+    public void stop_withPackageName_verifyMediaControllerDeinit() {
+        mMediaOutputController.start(mCb);
+        reset(mMediaController);
+
+        mMediaOutputController.stop();
+
+        verify(mMediaController).unregisterCallback(any());
+    }
+
+    @Test
+    public void stop_withoutPackageName_verifyMediaControllerDeinit() {
+        mMediaOutputController = new MediaOutputController(mSpyContext, null, mMediaSessionManager,
+                mLocalBluetoothManager, mShadeController, mStarter);
+        mMediaOutputController.start(mCb);
+
+        mMediaOutputController.stop();
+
+        verify(mMediaController, never()).unregisterCallback(any());
+    }
+
+    @Test
+    public void onDeviceListUpdate_verifyDeviceListCallback() {
+        mMediaOutputController.start(mCb);
+        reset(mCb);
+
+        mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+        final List<MediaDevice> devices = new ArrayList<>(mMediaOutputController.getMediaDevices());
+
+        assertThat(devices.containsAll(mMediaDevices)).isTrue();
+        assertThat(devices.size()).isEqualTo(mMediaDevices.size());
+        verify(mCb).onRouteChanged();
+    }
+
+    @Test
+    public void onSelectedDeviceStateChanged_verifyCallback() {
+        mMediaOutputController.start(mCb);
+        reset(mCb);
+
+        mMediaOutputController.onSelectedDeviceStateChanged(mMediaDevice1,
+                LocalMediaManager.MediaDeviceState.STATE_CONNECTED);
+
+        verify(mCb).onRouteChanged();
+    }
+
+    @Test
+    public void onDeviceAttributesChanged_verifyCallback() {
+        mMediaOutputController.start(mCb);
+        reset(mCb);
+
+        mMediaOutputController.onDeviceAttributesChanged();
+
+        verify(mCb).onRouteChanged();
+    }
+
+    @Test
+    public void onRequestFailed_verifyCallback() {
+        mMediaOutputController.start(mCb);
+        reset(mCb);
+
+        mMediaOutputController.onRequestFailed(0 /* reason */);
+
+        verify(mCb).onRouteChanged();
+    }
+
+    @Test
+    public void getHeaderTitle_withoutMetadata_returnDefaultString() {
+        when(mMediaController.getMetadata()).thenReturn(null);
+
+        mMediaOutputController.start(mCb);
+
+        assertThat(mMediaOutputController.getHeaderTitle()).isEqualTo(
+                mContext.getText(R.string.controls_media_title));
+    }
+
+    @Test
+    public void getHeaderTitle_withMetadata_returnSongName() {
+        when(mMediaController.getMetadata()).thenReturn(mMediaMetadata);
+
+        mMediaOutputController.start(mCb);
+
+        assertThat(mMediaOutputController.getHeaderTitle()).isEqualTo(TEST_SONG);
+    }
+
+    @Test
+    public void getHeaderSubTitle_withoutMetadata_returnNull() {
+        when(mMediaController.getMetadata()).thenReturn(null);
+
+        mMediaOutputController.start(mCb);
+
+        assertThat(mMediaOutputController.getHeaderSubTitle()).isNull();
+    }
+
+    @Test
+    public void getHeaderSubTitle_withMetadata_returnArtistName() {
+        when(mMediaController.getMetadata()).thenReturn(mMediaMetadata);
+
+        mMediaOutputController.start(mCb);
+
+        assertThat(mMediaOutputController.getHeaderSubTitle()).isEqualTo(TEST_ARTIST);
+    }
+
+    @Test
+    public void connectDevice_verifyConnect() {
+        mMediaOutputController.connectDevice(mMediaDevice1);
+
+        // Wait for background thread execution
+        try {
+            Thread.sleep(100);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+
+        verify(mLocalMediaManager).connectDevice(mMediaDevice1);
+    }
+
+    @Test
+    public void getActiveRemoteMediaDevice_isSystemSession_returnSession() {
+        when(mRemoteSessionInfo.getId()).thenReturn(TEST_SESSION_ID);
+        when(mRemoteSessionInfo.getName()).thenReturn(TEST_SESSION_NAME);
+        when(mRemoteSessionInfo.getVolumeMax()).thenReturn(100);
+        when(mRemoteSessionInfo.getVolume()).thenReturn(10);
+        when(mRemoteSessionInfo.isSystemSession()).thenReturn(false);
+        mRoutingSessionInfos.add(mRemoteSessionInfo);
+        when(mLocalMediaManager.getActiveMediaSession()).thenReturn(mRoutingSessionInfos);
+
+        assertThat(mMediaOutputController.getActiveRemoteMediaDevices()).containsExactly(
+                mRemoteSessionInfo);
+    }
+
+    @Test
+    public void getActiveRemoteMediaDevice_notSystemSession_returnEmpty() {
+        when(mRemoteSessionInfo.getId()).thenReturn(TEST_SESSION_ID);
+        when(mRemoteSessionInfo.getName()).thenReturn(TEST_SESSION_NAME);
+        when(mRemoteSessionInfo.getVolumeMax()).thenReturn(100);
+        when(mRemoteSessionInfo.getVolume()).thenReturn(10);
+        when(mRemoteSessionInfo.isSystemSession()).thenReturn(true);
+        mRoutingSessionInfos.add(mRemoteSessionInfo);
+        when(mLocalMediaManager.getActiveMediaSession()).thenReturn(mRoutingSessionInfos);
+
+        assertThat(mMediaOutputController.getActiveRemoteMediaDevices()).isEmpty();
+    }
+
+    @Test
+    public void isZeroMode_onlyFromPhoneOutput_returnTrue() {
+        // Multiple available devices
+        assertThat(mMediaOutputController.isZeroMode()).isFalse();
+        when(mMediaDevice1.getDeviceType()).thenReturn(
+                MediaDevice.MediaDeviceType.TYPE_PHONE_DEVICE);
+        mMediaDevices.clear();
+        mMediaDevices.add(mMediaDevice1);
+        mMediaOutputController.start(mCb);
+        mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+
+        assertThat(mMediaOutputController.isZeroMode()).isTrue();
+
+        when(mMediaDevice1.getDeviceType()).thenReturn(
+                MediaDevice.MediaDeviceType.TYPE_3POINT5_MM_AUDIO_DEVICE);
+
+        assertThat(mMediaOutputController.isZeroMode()).isTrue();
+
+        when(mMediaDevice1.getDeviceType()).thenReturn(
+                MediaDevice.MediaDeviceType.TYPE_USB_C_AUDIO_DEVICE);
+
+        assertThat(mMediaOutputController.isZeroMode()).isTrue();
+    }
+
+    @Test
+    public void isZeroMode_notFromPhoneOutput_returnFalse() {
+        when(mMediaDevice1.getDeviceType()).thenReturn(
+                MediaDevice.MediaDeviceType.TYPE_UNKNOWN);
+        mMediaDevices.clear();
+        mMediaDevices.add(mMediaDevice1);
+        mMediaOutputController.start(mCb);
+        mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+
+        assertThat(mMediaOutputController.isZeroMode()).isFalse();
+
+        when(mMediaDevice1.getDeviceType()).thenReturn(
+                MediaDevice.MediaDeviceType.TYPE_FAST_PAIR_BLUETOOTH_DEVICE);
+
+        assertThat(mMediaOutputController.isZeroMode()).isFalse();
+
+        when(mMediaDevice1.getDeviceType()).thenReturn(
+                MediaDevice.MediaDeviceType.TYPE_BLUETOOTH_DEVICE);
+
+        assertThat(mMediaOutputController.isZeroMode()).isFalse();
+
+        when(mMediaDevice1.getDeviceType()).thenReturn(
+                MediaDevice.MediaDeviceType.TYPE_CAST_DEVICE);
+
+        assertThat(mMediaOutputController.isZeroMode()).isFalse();
+
+        when(mMediaDevice1.getDeviceType()).thenReturn(
+                MediaDevice.MediaDeviceType.TYPE_CAST_GROUP_DEVICE);
+
+        assertThat(mMediaOutputController.isZeroMode()).isFalse();
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
index 37ccac0..23c0930 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/DozeServiceHostTest.java
@@ -179,7 +179,8 @@
         HashSet<Integer> reasonsThatDontPulse = new HashSet<>(
                 Arrays.asList(DozeLog.REASON_SENSOR_PICKUP,
                         DozeLog.REASON_SENSOR_DOUBLE_TAP,
-                        DozeLog.REASON_SENSOR_TAP));
+                        DozeLog.REASON_SENSOR_TAP,
+                        DozeLog.REASON_SENSOR_UDFPS_LONG_PRESS));
 
         doAnswer(invocation -> {
             DozeHost.PulseCallback callback = invocation.getArgument(0);
diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java
index fede1d2..48055b5 100644
--- a/services/core/java/com/android/server/am/ActivityManagerConstants.java
+++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java
@@ -26,6 +26,7 @@
 import android.net.Uri;
 import android.os.Build;
 import android.os.Handler;
+import android.os.Message;
 import android.provider.DeviceConfig;
 import android.provider.DeviceConfig.OnPropertiesChangedListener;
 import android.provider.DeviceConfig.Properties;
@@ -124,6 +125,7 @@
     private static final long DEFAULT_TOP_TO_FGS_GRACE_DURATION = 15 * 1000;
     private static final int DEFAULT_PENDINGINTENT_WARNING_THRESHOLD = 2000;
     private static final int DEFAULT_MIN_CRASH_INTERVAL = 2 * 60 * 1000;
+    private static final int DEFAULT_MAX_PHANTOM_PROCESSES = 32;
 
 
     // Flag stored in the DeviceConfig API.
@@ -133,6 +135,11 @@
     private static final String KEY_MAX_CACHED_PROCESSES = "max_cached_processes";
 
     /**
+     * Maximum number of cached processes.
+     */
+    private static final String KEY_MAX_PHANTOM_PROCESSES = "max_phantom_processes";
+
+    /**
      * Default value for mFlagBackgroundActivityStartsEnabled if not explicitly set in
      * Settings.Global. This allows it to be set experimentally unless it has been
      * enabled/disabled in developer options. Defaults to false.
@@ -364,6 +371,11 @@
      */
     public final ArraySet<ComponentName> KEEP_WARMING_SERVICES = new ArraySet<ComponentName>();
 
+    /**
+     * Maximum number of phantom processes.
+     */
+    public int MAX_PHANTOM_PROCESSES = DEFAULT_MAX_PHANTOM_PROCESSES;
+
     private List<String> mDefaultImperceptibleKillExemptPackages;
     private List<Integer> mDefaultImperceptibleKillExemptProcStates;
 
@@ -481,6 +493,9 @@
                             case KEY_BINDER_HEAVY_HITTER_AUTO_SAMPLER_THRESHOLD:
                                 updateBinderHeavyHitterWatcher();
                                 break;
+                            case KEY_MAX_PHANTOM_PROCESSES:
+                                updateMaxPhantomProcesses();
+                                break;
                             default:
                                 break;
                         }
@@ -599,6 +614,8 @@
                 // with defaults.
                 Slog.e("ActivityManagerConstants", "Bad activity manager config settings", e);
             }
+            final long currentPowerCheckInterval = POWER_CHECK_INTERVAL;
+
             BACKGROUND_SETTLE_TIME = mParser.getLong(KEY_BACKGROUND_SETTLE_TIME,
                     DEFAULT_BACKGROUND_SETTLE_TIME);
             FGSERVICE_MIN_SHOWN_TIME = mParser.getLong(KEY_FGSERVICE_MIN_SHOWN_TIME,
@@ -664,6 +681,13 @@
             PENDINGINTENT_WARNING_THRESHOLD = mParser.getInt(KEY_PENDINGINTENT_WARNING_THRESHOLD,
                     DEFAULT_PENDINGINTENT_WARNING_THRESHOLD);
 
+            if (POWER_CHECK_INTERVAL != currentPowerCheckInterval) {
+                mService.mHandler.removeMessages(
+                        ActivityManagerService.CHECK_EXCESSIVE_POWER_USE_MSG);
+                final Message msg = mService.mHandler.obtainMessage(
+                        ActivityManagerService.CHECK_EXCESSIVE_POWER_USE_MSG);
+                mService.mHandler.sendMessageDelayed(msg, POWER_CHECK_INTERVAL);
+            }
             // For new flags that are intended for server-side experiments, please use the new
             // DeviceConfig package.
         }
@@ -811,6 +835,16 @@
         mService.scheduleUpdateBinderHeavyHitterWatcherConfig();
     }
 
+    private void updateMaxPhantomProcesses() {
+        final int oldVal = MAX_PHANTOM_PROCESSES;
+        MAX_PHANTOM_PROCESSES = DeviceConfig.getInt(
+                DeviceConfig.NAMESPACE_ACTIVITY_MANAGER, KEY_MAX_PHANTOM_PROCESSES,
+                DEFAULT_MAX_PHANTOM_PROCESSES);
+        if (oldVal > MAX_PHANTOM_PROCESSES) {
+            mService.mHandler.post(mService.mPhantomProcessList::trimPhantomProcessesIfNecessary);
+        }
+    }
+
     void dump(PrintWriter pw) {
         pw.println("ACTIVITY MANAGER SETTINGS (dumpsys activity settings) "
                 + Settings.Global.ACTIVITY_MANAGER_CONSTANTS + ":");
@@ -897,6 +931,8 @@
         pw.println(BINDER_HEAVY_HITTER_AUTO_SAMPLER_BATCHSIZE);
         pw.print("  "); pw.print(KEY_BINDER_HEAVY_HITTER_AUTO_SAMPLER_THRESHOLD); pw.print("=");
         pw.println(BINDER_HEAVY_HITTER_AUTO_SAMPLER_THRESHOLD);
+        pw.print("  "); pw.print(KEY_MAX_PHANTOM_PROCESSES); pw.print("=");
+        pw.println(MAX_PHANTOM_PROCESSES);
 
         pw.println();
         if (mOverrideMaxCachedProcesses >= 0) {
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index b55d555..b1b4018 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -635,6 +635,12 @@
     final ProcessList mProcessList;
 
     /**
+     * The list of phantom processes.
+     * @see PhantomProcessRecord
+     */
+    final PhantomProcessList mPhantomProcessList;
+
+    /**
      * Tracking long-term execution of processes to look for abuse and other
      * bad app behavior.
      */
@@ -1996,6 +2002,7 @@
         mProcessList = injector.getProcessList(this);
         mProcessList.init(this, activeUids, mPlatformCompat);
         mAppProfiler = new AppProfiler(this, BackgroundThread.getHandler().getLooper(), null);
+        mPhantomProcessList = new PhantomProcessList(this);
         mOomAdjuster = hasHandlerThread
                 ? new OomAdjuster(this, mProcessList, activeUids, handlerThread) : null;
 
@@ -2053,6 +2060,7 @@
         mProcessList.init(this, activeUids, mPlatformCompat);
         mAppProfiler = new AppProfiler(this, BackgroundThread.getHandler().getLooper(),
                 new LowMemDetector(this));
+        mPhantomProcessList = new PhantomProcessList(this);
         mOomAdjuster = new OomAdjuster(this, mProcessList, activeUids);
 
         // Broadcast policy parameters
@@ -9209,6 +9217,10 @@
             }
         }
 
+        if (dumpAll) {
+            mPhantomProcessList.dump(pw, "  ");
+        }
+
         if (mImportantProcesses.size() > 0) {
             synchronized (mPidsSelfLocked) {
                 boolean printed = false;
@@ -14832,44 +14844,24 @@
             int i = mProcessList.mLruProcesses.size();
             while (i > 0) {
                 i--;
-                ProcessRecord app = mProcessList.mLruProcesses.get(i);
+                final ProcessRecord app = mProcessList.mLruProcesses.get(i);
                 if (app.setProcState >= ActivityManager.PROCESS_STATE_HOME) {
-                    if (app.lastCpuTime <= 0) {
-                        continue;
+                    int cpuLimit;
+                    long checkDur = curUptime - app.getWhenUnimportant();
+                    if (checkDur <= mConstants.POWER_CHECK_INTERVAL) {
+                        cpuLimit = mConstants.POWER_CHECK_MAX_CPU_1;
+                    } else if (checkDur <= (mConstants.POWER_CHECK_INTERVAL * 2)
+                            || app.setProcState <= ActivityManager.PROCESS_STATE_HOME) {
+                        cpuLimit = mConstants.POWER_CHECK_MAX_CPU_2;
+                    } else if (checkDur <= (mConstants.POWER_CHECK_INTERVAL * 3)) {
+                        cpuLimit = mConstants.POWER_CHECK_MAX_CPU_3;
+                    } else {
+                        cpuLimit = mConstants.POWER_CHECK_MAX_CPU_4;
                     }
-                    long cputimeUsed = app.curCpuTime - app.lastCpuTime;
-                    if (DEBUG_POWER) {
-                        StringBuilder sb = new StringBuilder(128);
-                        sb.append("CPU for ");
-                        app.toShortString(sb);
-                        sb.append(": over ");
-                        TimeUtils.formatDuration(uptimeSince, sb);
-                        sb.append(" used ");
-                        TimeUtils.formatDuration(cputimeUsed, sb);
-                        sb.append(" (");
-                        sb.append((cputimeUsed * 100) / uptimeSince);
-                        sb.append("%)");
-                        Slog.i(TAG_POWER, sb.toString());
-                    }
-                    // If the process has used too much CPU over the last duration, the
-                    // user probably doesn't want this, so kill!
-                    if (doCpuKills && uptimeSince > 0) {
-                        // What is the limit for this process?
-                        int cpuLimit;
-                        long checkDur = curUptime - app.getWhenUnimportant();
-                        if (checkDur <= mConstants.POWER_CHECK_INTERVAL) {
-                            cpuLimit = mConstants.POWER_CHECK_MAX_CPU_1;
-                        } else if (checkDur <= (mConstants.POWER_CHECK_INTERVAL * 2)
-                                || app.setProcState <= ActivityManager.PROCESS_STATE_HOME) {
-                            cpuLimit = mConstants.POWER_CHECK_MAX_CPU_2;
-                        } else if (checkDur <= (mConstants.POWER_CHECK_INTERVAL * 3)) {
-                            cpuLimit = mConstants.POWER_CHECK_MAX_CPU_3;
-                        } else {
-                            cpuLimit = mConstants.POWER_CHECK_MAX_CPU_4;
-                        }
-                        if (((cputimeUsed * 100) / uptimeSince) >= cpuLimit) {
-                            mBatteryStatsService.reportExcessiveCpu(app.info.uid, app.processName,
-                                        uptimeSince, cputimeUsed);
+                    if (app.lastCpuTime > 0) {
+                        final long cputimeUsed = app.curCpuTime - app.lastCpuTime;
+                        if (checkExcessivePowerUsageLocked(uptimeSince, doCpuKills, cputimeUsed,
+                                app.processName, app.toShortString(), cpuLimit, app)) {
                             app.kill("excessive cpu " + cputimeUsed + " during " + uptimeSince
                                     + " dur=" + checkDur + " limit=" + cpuLimit,
                                     ApplicationExitInfo.REASON_EXCESSIVE_RESOURCE_USAGE,
@@ -14878,23 +14870,73 @@
                             synchronized (mProcessStats.mLock) {
                                 app.baseProcessTracker.reportExcessiveCpu(app.pkgList.mPkgList);
                             }
-                            for (int ipkg = app.pkgList.size() - 1; ipkg >= 0; ipkg--) {
-                                ProcessStats.ProcessStateHolder holder = app.pkgList.valueAt(ipkg);
-                                FrameworkStatsLog.write(
-                                        FrameworkStatsLog.EXCESSIVE_CPU_USAGE_REPORTED,
-                                        app.info.uid,
-                                        holder.state.getName(),
-                                        holder.state.getPackage(),
-                                        holder.appVersion);
-                            }
                         }
                     }
+
                     app.lastCpuTime = app.curCpuTime;
+
+                    // Also check the phantom processes if there is any
+                    final long chkDur = checkDur;
+                    final int cpuLmt = cpuLimit;
+                    final boolean doKill = doCpuKills;
+                    mPhantomProcessList.forEachPhantomProcessOfApp(app, r -> {
+                        if (r.mLastCputime > 0) {
+                            final long cputimeUsed = r.mCurrentCputime - r.mLastCputime;
+                            if (checkExcessivePowerUsageLocked(uptimeSince, doKill, cputimeUsed,
+                                    app.processName, r.toString(), cpuLimit, app)) {
+                                mPhantomProcessList.killPhantomProcessGroupLocked(app, r,
+                                        ApplicationExitInfo.REASON_EXCESSIVE_RESOURCE_USAGE,
+                                        ApplicationExitInfo.SUBREASON_EXCESSIVE_CPU,
+                                        "excessive cpu " + cputimeUsed + " during "
+                                        + uptimeSince + " dur=" + chkDur + " limit=" + cpuLmt);
+                                return false;
+                            }
+                        }
+                        r.mLastCputime = r.mCurrentCputime;
+                        return true;
+                    });
                 }
             }
         }
     }
 
+    private boolean checkExcessivePowerUsageLocked(final long uptimeSince, boolean doCpuKills,
+            final long cputimeUsed, final String processName, final String description,
+            final int cpuLimit, final ProcessRecord app) {
+        if (DEBUG_POWER) {
+            StringBuilder sb = new StringBuilder(128);
+            sb.append("CPU for ");
+            sb.append(description);
+            sb.append(": over ");
+            TimeUtils.formatDuration(uptimeSince, sb);
+            sb.append(" used ");
+            TimeUtils.formatDuration(cputimeUsed, sb);
+            sb.append(" (");
+            sb.append((cputimeUsed * 100.0) / uptimeSince);
+            sb.append("%)");
+            Slog.i(TAG_POWER, sb.toString());
+        }
+        // If the process has used too much CPU over the last duration, the
+        // user probably doesn't want this, so kill!
+        if (doCpuKills && uptimeSince > 0) {
+            if (((cputimeUsed * 100) / uptimeSince) >= cpuLimit) {
+                mBatteryStatsService.reportExcessiveCpu(app.info.uid, app.processName,
+                        uptimeSince, cputimeUsed);
+                for (int ipkg = app.pkgList.size() - 1; ipkg >= 0; ipkg--) {
+                    ProcessStats.ProcessStateHolder holder = app.pkgList.valueAt(ipkg);
+                    FrameworkStatsLog.write(
+                            FrameworkStatsLog.EXCESSIVE_CPU_USAGE_REPORTED,
+                            app.info.uid,
+                            processName,
+                            holder.state.getPackage(),
+                            holder.appVersion);
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+
     final void setProcessTrackerStateLocked(ProcessRecord proc, int memFactor, long now) {
         synchronized (mProcessStats.mLock) {
             if (proc.thread != null && proc.baseProcessTracker != null) {
diff --git a/services/core/java/com/android/server/am/AppProfiler.java b/services/core/java/com/android/server/am/AppProfiler.java
index 0b5d585..31ffb35 100644
--- a/services/core/java/com/android/server/am/AppProfiler.java
+++ b/services/core/java/com/android/server/am/AppProfiler.java
@@ -1257,6 +1257,10 @@
                 }
             }
 
+            if (haveNewCpuStats) {
+                mService.mPhantomProcessList.updateProcessCpuStatesLocked(mProcessCpuTracker);
+            }
+
             final BatteryStatsImpl bstats = mService.mBatteryStatsService.getActiveStatistics();
             synchronized (bstats) {
                 if (haveNewCpuStats) {
diff --git a/services/core/java/com/android/server/am/PhantomProcessList.java b/services/core/java/com/android/server/am/PhantomProcessList.java
new file mode 100644
index 0000000..e2fcf08
--- /dev/null
+++ b/services/core/java/com/android/server/am/PhantomProcessList.java
@@ -0,0 +1,395 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.am;
+
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR;
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
+
+import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_PROCESSES;
+import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
+import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
+
+import android.app.ApplicationExitInfo.Reason;
+import android.app.ApplicationExitInfo.SubReason;
+import android.os.Handler;
+import android.os.Process;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.os.ProcessCpuTracker;
+
+import libcore.io.IoUtils;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.function.Function;
+
+/**
+ * Activity manager code dealing with phantom processes.
+ */
+public final class PhantomProcessList {
+    static final String TAG = TAG_WITH_CLASS_NAME ? "PhantomProcessList" : TAG_AM;
+
+    final Object mLock = new Object();
+
+    /**
+     * All of the phantom process record we track, key is the pid of the process.
+     */
+    @GuardedBy("mLock")
+    final SparseArray<PhantomProcessRecord> mPhantomProcesses = new SparseArray<>();
+
+    /**
+     * The mapping between app processes and their phantom processess, outer key is the pid of
+     * the app process, while the inner key is the pid of the phantom process.
+     */
+    @GuardedBy("mLock")
+    final SparseArray<SparseArray<PhantomProcessRecord>> mAppPhantomProcessMap =
+            new SparseArray<>();
+
+    /**
+     * The mapping of the pidfd to PhantomProcessRecord.
+     */
+    @GuardedBy("mLock")
+    final SparseArray<PhantomProcessRecord> mPhantomProcessesPidFds = new SparseArray<>();
+
+    /**
+     * The list of phantom processes tha's being signaled to be killed but still undead yet.
+     */
+    @GuardedBy("mLock")
+    final SparseArray<PhantomProcessRecord> mZombiePhantomProcesses = new SparseArray<>();
+
+    @GuardedBy("mLock")
+    private final ArrayList<PhantomProcessRecord> mTempPhantomProcesses = new ArrayList<>();
+
+    @GuardedBy("mLock")
+    private boolean mTrimPhantomProcessScheduled = false;
+
+    @GuardedBy("mLock")
+    int mUpdateSeq;
+
+    private final ActivityManagerService mService;
+    private final Handler mKillHandler;
+
+    PhantomProcessList(final ActivityManagerService service) {
+        mService = service;
+        mKillHandler = service.mProcessList.sKillHandler;
+    }
+
+    /**
+     * Get the existing phantom process record, or create if it's not existing yet;
+     * however, before creating it, we'll check if this is really a phantom process
+     * and we'll return null if it's not.
+     */
+    @GuardedBy("mLock")
+    PhantomProcessRecord getOrCreatePhantomProcessIfNeededLocked(final String processName,
+            final int uid, final int pid) {
+        // First check if it's actually an app process we know
+        if (isAppProcess(pid)) {
+            return null;
+        }
+
+        // Have we already been aware of this?
+        final int index = mPhantomProcesses.indexOfKey(pid);
+        if (index >= 0) {
+            final PhantomProcessRecord proc = mPhantomProcesses.valueAt(index);
+            if (proc.equals(processName, uid, pid)) {
+                return proc;
+            }
+            // Somehow our record doesn't match, remove it anyway
+            Slog.w(TAG, "Stale " + proc + ", removing");
+            mPhantomProcesses.removeAt(index);
+        } else {
+            // Is this one of the zombie processes we've known?
+            final int idx = mZombiePhantomProcesses.indexOfKey(pid);
+            if (idx >= 0) {
+                final PhantomProcessRecord proc = mZombiePhantomProcesses.valueAt(idx);
+                if (proc.equals(processName, uid, pid)) {
+                    return proc;
+                }
+                // Our zombie process information is outdated, let's remove this one, it shoud
+                // have been gone.
+                mZombiePhantomProcesses.removeAt(idx);
+            }
+        }
+
+        int ppid = getParentPid(pid);
+
+        // Walk through its parents and see if it could be traced back to an app process.
+        while (ppid > 1) {
+            if (isAppProcess(ppid)) {
+                // It's a phantom process, bookkeep it
+                try {
+                    final PhantomProcessRecord proc = new PhantomProcessRecord(
+                            processName, uid, pid, ppid, mService,
+                            this::onPhantomProcessKilledLocked);
+                    proc.mUpdateSeq = mUpdateSeq;
+                    mPhantomProcesses.put(pid, proc);
+                    SparseArray<PhantomProcessRecord> array = mAppPhantomProcessMap.get(ppid);
+                    if (array == null) {
+                        array = new SparseArray<>();
+                        mAppPhantomProcessMap.put(ppid, array);
+                    }
+                    array.put(pid, proc);
+                    if (proc.mPidFd != null) {
+                        mKillHandler.getLooper().getQueue().addOnFileDescriptorEventListener(
+                                proc.mPidFd, EVENT_INPUT | EVENT_ERROR,
+                                this::onPhantomProcessFdEvent);
+                        mPhantomProcessesPidFds.put(proc.mPidFd.getInt$(), proc);
+                    }
+                    scheduleTrimPhantomProcessesLocked();
+                    return proc;
+                } catch (IllegalStateException e) {
+                    return null;
+                }
+            }
+
+            ppid = getParentPid(ppid);
+        }
+        return null;
+    }
+
+    private static int getParentPid(int pid) {
+        try {
+            return Process.getParentPid(pid);
+        } catch (Exception e) {
+        }
+        return -1;
+    }
+
+    private boolean isAppProcess(int pid) {
+        synchronized (mService.mPidsSelfLocked) {
+            return mService.mPidsSelfLocked.get(pid) != null;
+        }
+    }
+
+    private int onPhantomProcessFdEvent(FileDescriptor fd, int events) {
+        synchronized (mLock) {
+            final PhantomProcessRecord proc = mPhantomProcessesPidFds.get(fd.getInt$());
+            if ((events & EVENT_INPUT) != 0) {
+                proc.onProcDied(true);
+            } else {
+                // EVENT_ERROR, kill the process
+                proc.killLocked("Process error", true);
+            }
+        }
+        return 0;
+    }
+
+    @GuardedBy("mLock")
+    private void onPhantomProcessKilledLocked(final PhantomProcessRecord proc) {
+        if (proc.mPidFd != null && proc.mPidFd.valid()) {
+            mKillHandler.getLooper().getQueue()
+                    .removeOnFileDescriptorEventListener(proc.mPidFd);
+            mPhantomProcessesPidFds.remove(proc.mPidFd.getInt$());
+            IoUtils.closeQuietly(proc.mPidFd);
+        }
+        mPhantomProcesses.remove(proc.mPid);
+        final int index = mAppPhantomProcessMap.indexOfKey(proc.mPpid);
+        if (index < 0) {
+            return;
+        }
+        SparseArray<PhantomProcessRecord> array = mAppPhantomProcessMap.valueAt(index);
+        array.remove(proc.mPid);
+        if (array.size() == 0) {
+            mAppPhantomProcessMap.removeAt(index);
+        }
+        if (proc.mZombie) {
+            // If it's not really dead, bookkeep it
+            mZombiePhantomProcesses.put(proc.mPid, proc);
+        } else {
+            // In case of race condition, let's try to remove it from zombie list
+            mZombiePhantomProcesses.remove(proc.mPid);
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void scheduleTrimPhantomProcessesLocked() {
+        if (!mTrimPhantomProcessScheduled) {
+            mTrimPhantomProcessScheduled = true;
+            mService.mHandler.post(this::trimPhantomProcessesIfNecessary);
+        }
+    }
+
+    /**
+     * Clamp the number of phantom processes to
+     * {@link ActivityManagerConstants#MAX_PHANTOM_PROCESSE}, kills those surpluses in the
+     * order of the oom adjs of their parent process.
+     */
+    void trimPhantomProcessesIfNecessary() {
+        synchronized (mLock) {
+            mTrimPhantomProcessScheduled = false;
+            if (mService.mConstants.MAX_PHANTOM_PROCESSES < mPhantomProcesses.size()) {
+                for (int i = mPhantomProcesses.size() - 1; i >= 0; i--) {
+                    mTempPhantomProcesses.add(mPhantomProcesses.valueAt(i));
+                }
+                synchronized (mService.mPidsSelfLocked) {
+                    Collections.sort(mTempPhantomProcesses, (a, b) -> {
+                        final ProcessRecord ra = mService.mPidsSelfLocked.get(a.mPpid);
+                        if (ra == null) {
+                            // parent is gone, this process should have been killed too
+                            return 1;
+                        }
+                        final ProcessRecord rb = mService.mPidsSelfLocked.get(b.mPpid);
+                        if (rb == null) {
+                            // parent is gone, this process should have been killed too
+                            return -1;
+                        }
+                        if (ra.curAdj != rb.curAdj) {
+                            return ra.curAdj - rb.curAdj;
+                        }
+                        if (a.mKnownSince != b.mKnownSince) {
+                            // In case of identical oom adj, younger one first
+                            return a.mKnownSince < b.mKnownSince ? 1 : -1;
+                        }
+                        return 0;
+                    });
+                }
+                for (int i = mTempPhantomProcesses.size() - 1;
+                        i >= mService.mConstants.MAX_PHANTOM_PROCESSES; i--) {
+                    final PhantomProcessRecord proc = mTempPhantomProcesses.get(i);
+                    proc.killLocked("Trimming phantom processes", true);
+                }
+                mTempPhantomProcesses.clear();
+            }
+        }
+    }
+
+    /**
+     * Remove all entries with outdated seq num.
+     */
+    @GuardedBy("mLock")
+    void pruneStaleProcessesLocked() {
+        for (int i = mPhantomProcesses.size() - 1; i >= 0; i--) {
+            final PhantomProcessRecord proc = mPhantomProcesses.valueAt(i);
+            if (proc.mUpdateSeq < mUpdateSeq) {
+                if (DEBUG_PROCESSES) {
+                    Slog.v(TAG, "Pruning " + proc + " as it should have been dead.");
+                }
+                proc.killLocked("Stale process", true);
+            }
+        }
+        for (int i = mZombiePhantomProcesses.size() - 1; i >= 0; i--) {
+            final PhantomProcessRecord proc = mZombiePhantomProcesses.valueAt(i);
+            if (proc.mUpdateSeq < mUpdateSeq) {
+                if (DEBUG_PROCESSES) {
+                    Slog.v(TAG, "Pruning " + proc + " as it should have been dead.");
+                }
+            }
+        }
+    }
+
+    /**
+     * Kill the given phantom process, all its siblings (if any) and their parent process
+     */
+    @GuardedBy("mService")
+    void killPhantomProcessGroupLocked(ProcessRecord app, PhantomProcessRecord proc,
+            @Reason int reasonCode, @SubReason int subReason, String msg) {
+        synchronized (mLock) {
+            int index = mAppPhantomProcessMap.indexOfKey(proc.mPpid);
+            if (index >= 0) {
+                final SparseArray<PhantomProcessRecord> array =
+                        mAppPhantomProcessMap.valueAt(index);
+                for (int i = array.size() - 1; i >= 0; i--) {
+                    final PhantomProcessRecord r = array.valueAt(i);
+                    if (r == proc) {
+                        r.killLocked(msg, true);
+                    } else {
+                        r.killLocked("Caused by siling process: " + msg, false);
+                    }
+                }
+            }
+        }
+        // Lastly, kill the parent process too
+        app.kill("Caused by child process: " + msg, reasonCode, subReason, true);
+    }
+
+    /**
+     * Iterate all phantom process belonging to the given app, and invokve callback
+     * for each of them.
+     */
+    void forEachPhantomProcessOfApp(final ProcessRecord app,
+            final Function<PhantomProcessRecord, Boolean> callback) {
+        synchronized (mLock) {
+            int index = mAppPhantomProcessMap.indexOfKey(app.pid);
+            if (index >= 0) {
+                final SparseArray<PhantomProcessRecord> array =
+                        mAppPhantomProcessMap.valueAt(index);
+                for (int i = array.size() - 1; i >= 0; i--) {
+                    final PhantomProcessRecord r = array.valueAt(i);
+                    if (!callback.apply(r)) {
+                        break;
+                    }
+                }
+            }
+        }
+    }
+
+    @GuardedBy("tracker")
+    void updateProcessCpuStatesLocked(ProcessCpuTracker tracker) {
+        synchronized (mLock) {
+            // refresh the phantom process list with the latest cpu stats results.
+            mUpdateSeq++;
+            for (int i = tracker.countStats() - 1; i >= 0; i--) {
+                final ProcessCpuTracker.Stats st = tracker.getStats(i);
+                final PhantomProcessRecord r =
+                        getOrCreatePhantomProcessIfNeededLocked(st.name, st.uid, st.pid);
+                if (r != null) {
+                    r.mUpdateSeq = mUpdateSeq;
+                    r.mCurrentCputime += st.rel_utime + st.rel_stime;
+                    if (r.mLastCputime == 0) {
+                        r.mLastCputime = r.mCurrentCputime;
+                    }
+                    r.updateAdjLocked();
+                }
+            }
+            // remove the stale ones
+            pruneStaleProcessesLocked();
+        }
+    }
+
+    void dump(PrintWriter pw, String prefix) {
+        synchronized (mLock) {
+            dumpPhantomeProcessLocked(pw, prefix, "All Active App Child Processes:",
+                    mPhantomProcesses);
+            dumpPhantomeProcessLocked(pw, prefix, "All Zombie App Child Processes:",
+                    mZombiePhantomProcesses);
+        }
+    }
+
+    void dumpPhantomeProcessLocked(PrintWriter pw, String prefix, String headline,
+            SparseArray<PhantomProcessRecord> list) {
+        final int size = list.size();
+        if (size == 0) {
+            return;
+        }
+        pw.println();
+        pw.print(prefix);
+        pw.println(headline);
+        for (int i = 0; i < size; i++) {
+            final PhantomProcessRecord proc = list.valueAt(i);
+            pw.print(prefix);
+            pw.print("  proc #");
+            pw.print(i);
+            pw.print(": ");
+            pw.println(proc.toString());
+            proc.dump(pw, prefix + "    ");
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/am/PhantomProcessRecord.java b/services/core/java/com/android/server/am/PhantomProcessRecord.java
new file mode 100644
index 0000000..0156ee5
--- /dev/null
+++ b/services/core/java/com/android/server/am/PhantomProcessRecord.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.am;
+
+import static android.os.Process.PROC_NEWLINE_TERM;
+import static android.os.Process.PROC_OUT_LONG;
+
+import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
+import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
+
+import android.os.Handler;
+import android.os.Process;
+import android.os.StrictMode;
+import android.os.SystemClock;
+import android.os.Trace;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.EventLog;
+import android.util.Slog;
+import android.util.TimeUtils;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.function.Consumer;
+
+/**
+ * The "phantom" app processes, which are forked by app processes so we are not aware of
+ * them until we walk through the process list in /proc.
+ */
+public final class PhantomProcessRecord {
+    static final String TAG = TAG_WITH_CLASS_NAME ? "PhantomProcessRecord" : TAG_AM;
+
+    static final long[] LONG_OUT = new long[1];
+    static final int[] LONG_FORMAT = new int[] {PROC_NEWLINE_TERM | PROC_OUT_LONG};
+
+    final String mProcessName;   // name of the process
+    final int mUid;              // uid of the process
+    final int mPid;              // The id of the process
+    final int mPpid;             // Ancestor (managed app process) pid of the process
+    final long mKnownSince;      // The timestamp when we're aware of the process
+    final FileDescriptor mPidFd; // The fd to monitor the termination of this process
+
+    long mLastCputime;           // How long proc has run CPU at last check
+    long mCurrentCputime;        // How long proc has run CPU most recently
+    int mUpdateSeq;              // Seq no, indicating the last check on this process
+    int mAdj;                    // The last known oom adj score
+    boolean mKilled;             // Whether it has been killed by us or not
+    boolean mZombie;             // Whether it was signaled to be killed but timed out
+    String mStringName;          // Caching of the toString() result
+
+    final ActivityManagerService mService;
+    final Object mLock;
+    final Consumer<PhantomProcessRecord> mOnKillListener;
+    final Handler mKillHandler;
+
+    PhantomProcessRecord(final String processName, final int uid, final int pid,
+            final int ppid, final ActivityManagerService service,
+            final Consumer<PhantomProcessRecord> onKillListener) throws IllegalStateException {
+        mProcessName = processName;
+        mUid = uid;
+        mPid = pid;
+        mPpid = ppid;
+        mKilled = false;
+        mAdj = ProcessList.NATIVE_ADJ;
+        mKnownSince = SystemClock.elapsedRealtime();
+        mService = service;
+        mLock = service.mPhantomProcessList.mLock;
+        mOnKillListener = onKillListener;
+        mKillHandler = service.mProcessList.sKillHandler;
+        if (Process.supportsPidFd()) {
+            StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
+            try {
+                mPidFd = Process.openPidFd(pid, 0);
+                if (mPidFd == null) {
+                    throw new IllegalStateException();
+                }
+            } catch (IOException e) {
+                // Maybe a race condition, the process is gone.
+                Slog.w(TAG, "Unable to open process " + pid + ", it might be gone");
+                IllegalStateException ex = new IllegalStateException();
+                ex.initCause(e);
+                throw ex;
+            } finally {
+                StrictMode.setThreadPolicy(oldPolicy);
+            }
+        } else {
+            mPidFd = null;
+        }
+    }
+
+    @GuardedBy("mLock")
+    void killLocked(String reason, boolean noisy) {
+        if (!mKilled) {
+            Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "kill");
+            if (noisy || mUid == mService.mCurOomAdjUid) {
+                mService.reportUidInfoMessageLocked(TAG,
+                        "Killing " + toString() + ": " + reason, mUid);
+            }
+            if (mPid > 0) {
+                EventLog.writeEvent(EventLogTags.AM_KILL, UserHandle.getUserId(mUid),
+                        mPid, mProcessName, mAdj, reason);
+                if (!Process.supportsPidFd()) {
+                    onProcDied(false);
+                } else {
+                    // We'll notify the listener when we're notified it's dead.
+                    // Meanwhile, we'd also need handle the case of zombie processes.
+                    mKillHandler.postDelayed(mProcKillTimer, this,
+                            ProcessList.PROC_KILL_TIMEOUT);
+                }
+                Process.killProcessQuiet(mPid);
+                ProcessList.killProcessGroup(mUid, mPid);
+            }
+            mKilled = true;
+            Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
+        }
+    }
+
+    private Runnable mProcKillTimer = new Runnable() {
+        @Override
+        public void run() {
+            synchronized (mLock) {
+                // The process is maybe in either D or Z state.
+                Slog.w(TAG, "Process " + toString() + " is still alive after "
+                        + ProcessList.PROC_KILL_TIMEOUT + "ms");
+                // Force a cleanup as we can't keep the fd open forever
+                mZombie = true;
+                onProcDied(false);
+                // But still bookkeep it, so it won't be added as a new one if it's spotted again.
+            }
+        }
+    };
+
+    @GuardedBy("mLock")
+    void updateAdjLocked() {
+        if (Process.readProcFile("/proc/" + mPid + "/oom_score_adj",
+                LONG_FORMAT, null, LONG_OUT, null)) {
+            mAdj = (int) LONG_OUT[0];
+        }
+    }
+
+    @GuardedBy("mLock")
+    void onProcDied(boolean reallyDead) {
+        if (reallyDead) {
+            Slog.i(TAG, "Process " + toString() + " died");
+        }
+        mKillHandler.removeCallbacks(mProcKillTimer, this);
+        if (mOnKillListener != null) {
+            mOnKillListener.accept(this);
+        }
+    }
+
+    @Override
+    public String toString() {
+        if (mStringName != null) {
+            return mStringName;
+        }
+        StringBuilder sb = new StringBuilder(128);
+        sb.append("PhantomProcessRecord {");
+        sb.append(Integer.toHexString(System.identityHashCode(this)));
+        sb.append(' ');
+        sb.append(mPid);
+        sb.append(':');
+        sb.append(mPpid);
+        sb.append(':');
+        sb.append(mProcessName);
+        sb.append('/');
+        if (mUid < Process.FIRST_APPLICATION_UID) {
+            sb.append(mUid);
+        } else {
+            sb.append('u');
+            sb.append(UserHandle.getUserId(mUid));
+            int appId = UserHandle.getAppId(mUid);
+            if (appId >= Process.FIRST_APPLICATION_UID) {
+                sb.append('a');
+                sb.append(appId - Process.FIRST_APPLICATION_UID);
+            } else {
+                sb.append('s');
+                sb.append(appId);
+            }
+            if (appId >= Process.FIRST_ISOLATED_UID && appId <= Process.LAST_ISOLATED_UID) {
+                sb.append('i');
+                sb.append(appId - Process.FIRST_ISOLATED_UID);
+            }
+        }
+        sb.append('}');
+        return mStringName = sb.toString();
+    }
+
+    void dump(PrintWriter pw, String prefix) {
+        final long now = SystemClock.elapsedRealtime();
+        pw.print(prefix);
+        pw.print("user #");
+        pw.print(UserHandle.getUserId(mUid));
+        pw.print(" uid=");
+        pw.print(mUid);
+        pw.print(" pid=");
+        pw.print(mPid);
+        pw.print(" ppid=");
+        pw.print(mPpid);
+        pw.print(" knownSince=");
+        TimeUtils.formatDuration(mKnownSince, now, pw);
+        pw.print(" killed=");
+        pw.println(mKilled);
+        pw.print(prefix);
+        pw.print("lastCpuTime=");
+        pw.print(mLastCputime);
+        if (mLastCputime > 0) {
+            pw.print(" timeUsed=");
+            TimeUtils.formatDuration(mCurrentCputime - mLastCputime, pw);
+        }
+        pw.print(" oom adj=");
+        pw.print(mAdj);
+        pw.print(" seq=");
+        pw.println(mUpdateSeq);
+    }
+
+    boolean equals(final String processName, final int uid, final int pid) {
+        return mUid == uid && mPid == pid && TextUtils.equals(mProcessName, processName);
+    }
+}
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index ced2f0f..5e65563 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -329,7 +329,7 @@
     /**
      * How long between a process kill and we actually receive its death recipient
      */
-    private static final int PROC_KILL_TIMEOUT = 2000; // 2 seconds;
+    static final int PROC_KILL_TIMEOUT = 2000; // 2 seconds;
 
     /**
      * Native heap allocations will now have a non-zero tag in the most significant byte.
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index d01a30f..35b1449 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -281,7 +281,8 @@
     static final int LONG_PRESS_HOME_NOTHING = 0;
     static final int LONG_PRESS_HOME_ALL_APPS = 1;
     static final int LONG_PRESS_HOME_ASSIST = 2;
-    static final int LAST_LONG_PRESS_HOME_BEHAVIOR = LONG_PRESS_HOME_ASSIST;
+    static final int LONG_PRESS_HOME_NOTIFICATION_PANEL = 3;
+    static final int LAST_LONG_PRESS_HOME_BEHAVIOR = LONG_PRESS_HOME_NOTIFICATION_PANEL;
 
     // must match: config_doubleTapOnHomeBehavior in config.xml
     static final int DOUBLE_TAP_HOME_NOTHING = 0;
@@ -1694,8 +1695,18 @@
                 case LONG_PRESS_HOME_ASSIST:
                     launchAssistAction(null, deviceId);
                     break;
+                case LONG_PRESS_HOME_NOTIFICATION_PANEL:
+                    IStatusBarService statusBarService = getStatusBarService();
+                    if (statusBarService != null) {
+                        try {
+                            statusBarService.togglePanel();
+                        } catch (RemoteException e) {
+                            // do nothing.
+                        }
+                    }
+                    break;
                 default:
-                    Log.w(TAG, "Undefined home long press behavior: "
+                    Log.w(TAG, "Undefined long press on home behavior: "
                             + mLongPressOnHomeBehavior);
                     break;
             }
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 9172897..63f363e 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -425,8 +425,7 @@
     int maxRecents;
 
     /** Only used for persistable tasks, otherwise 0. The last time this task was moved. Used for
-     * determining the order when restoring. Sign indicates whether last task movement was to front
-     * (positive) or back (negative). Absolute value indicates time. */
+     *  determining the order when restoring. */
     long mLastTimeMoved;
 
     /** If original intent did not allow relinquishing task identity, save that information */
@@ -952,6 +951,11 @@
         removeImmediately();
         if (isLeafTask()) {
             mAtmService.getTaskChangeNotificationController().notifyTaskRemoved(mTaskId);
+
+            final TaskDisplayArea taskDisplayArea = getDisplayArea();
+            if (taskDisplayArea != null) {
+                taskDisplayArea.onLeafTaskRemoved(mTaskId);
+            }
         }
     }
 
@@ -1525,15 +1529,14 @@
         mStackSupervisor.updateTopResumedActivityIfNeeded();
     }
 
-    void updateTaskMovement(boolean toFront) {
+    void updateTaskMovement(boolean toTop, int position) {
+        EventLogTags.writeWmTaskMoved(mTaskId, toTop ? 1 : 0, position);
+        final TaskDisplayArea taskDisplayArea = getDisplayArea();
+        if (taskDisplayArea != null && isLeafTask()) {
+            taskDisplayArea.onLeafTaskMoved(this, toTop);
+        }
         if (isPersistable) {
             mLastTimeMoved = System.currentTimeMillis();
-            // Sign is used to keep tasks sorted when persisted. Tasks sent to the bottom most
-            // recently will be most negative, tasks sent to the bottom before that will be less
-            // negative. Similarly for recent tasks moved to the top which will be most positive.
-            if (!toFront) {
-                mLastTimeMoved *= -1;
-            }
         }
         mRootWindowContainer.invalidateTaskLayers();
     }
@@ -3180,6 +3183,7 @@
 
     @Override
     void positionChildAt(int position, WindowContainer child, boolean includingParents) {
+        final boolean toTop = position >= (mChildren.size() - 1);
         position = getAdjustedChildPosition(child, position);
         super.positionChildAt(position, child, includingParents);
 
@@ -3187,10 +3191,9 @@
         if (DEBUG_TASK_MOVEMENT) Slog.d(TAG_WM, "positionChildAt: child=" + child
                 + " position=" + position + " parent=" + this);
 
-        final int toTop = position >= (mChildren.size() - 1) ? 1 : 0;
         final Task task = child.asTask();
         if (task != null) {
-            EventLogTags.writeWmTaskMoved(task.mTaskId, toTop, position);
+            task.updateTaskMovement(toTop, position);
         }
     }
 
@@ -6877,9 +6880,6 @@
             if (!deferResume) {
                 mRootWindowContainer.resumeFocusedStacksTopActivities();
             }
-            EventLogTags.writeWmTaskToFront(tr.mUserId, tr.mTaskId);
-            mAtmService.getTaskChangeNotificationController()
-                    .notifyTaskMovedToFront(tr.getTaskInfo());
         } finally {
             mDisplayContent.continueUpdateImeTarget();
         }
@@ -7265,7 +7265,6 @@
             Slog.i(TAG_WM, "positionChildAt: positioning task=" + task + " at " + position);
         }
         positionChildAt(position, task, includingParents);
-        task.updateTaskMovement(toTop);
         getDisplayContent().layoutAndAssignWindowLayersIfNeeded();
 
 
@@ -7402,7 +7401,6 @@
         }
 
         positionChildAt(POSITION_TOP, child, true /* includingParents */);
-        child.updateTaskMovement(true);
 
         final DisplayContent displayContent = getDisplayContent();
         displayContent.layoutAndAssignWindowLayersIfNeeded();
@@ -7415,7 +7413,6 @@
         final Task nextFocusableStack = getDisplayArea().getNextFocusableStack(
                 child.getRootTask(), true /* ignoreCurrent */);
         positionChildAtBottom(child, nextFocusableStack == null /* includingParents */);
-        child.updateTaskMovement(true);
     }
 
     @VisibleForTesting
@@ -7440,12 +7437,6 @@
         }
 
         final boolean isTop = getTopChild() == child;
-
-        final Task task = child.asTask();
-        if (task != null) {
-            task.updateTaskMovement(isTop);
-        }
-
         if (isTop) {
             final DisplayContent displayContent = getDisplayContent();
             displayContent.layoutAndAssignWindowLayersIfNeeded();
diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index 2b32e40..890f56e 100644
--- a/services/core/java/com/android/server/wm/TaskDisplayArea.java
+++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java
@@ -16,6 +16,7 @@
 
 package com.android.server.wm;
 
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
@@ -152,6 +153,12 @@
      */
     private boolean mRemoved;
 
+
+    /**
+     * The id of a leaf task that most recently being moved to front.
+     */
+    private int mLastLeafTaskToFrontId;
+
     TaskDisplayArea(DisplayContent displayContent, WindowManagerService service, String name,
             int displayAreaFeature) {
         super(service, Type.ANY, name, displayAreaFeature);
@@ -382,7 +389,7 @@
                     this /* child */, true /* includingParents */);
         }
 
-        child.updateTaskMovement(moveToTop);
+        child.updateTaskMovement(moveToTop, targetPosition);
 
         mDisplayContent.layoutAndAssignWindowLayersIfNeeded();
 
@@ -405,6 +412,30 @@
         }
     }
 
+    void onLeafTaskRemoved(int taskId) {
+        if (mLastLeafTaskToFrontId == taskId) {
+            mLastLeafTaskToFrontId = INVALID_TASK_ID;
+        }
+    }
+
+    void onLeafTaskMoved(Task t, boolean toTop) {
+        if (!toTop) {
+            if (t.mTaskId == mLastLeafTaskToFrontId) {
+                mLastLeafTaskToFrontId = INVALID_TASK_ID;
+            }
+            return;
+        }
+        if (t.mTaskId == mLastLeafTaskToFrontId || t.topRunningActivityLocked() == null) {
+            return;
+        }
+
+        mLastLeafTaskToFrontId = t.mTaskId;
+        EventLogTags.writeWmTaskToFront(t.mUserId, t.mTaskId);
+        // Notifying only when a leak task moved to front. Or the listeners would be notified
+        // couple times from the leaf task all the way up to the root task.
+        mAtmService.getTaskChangeNotificationController().notifyTaskMovedToFront(t.getTaskInfo());
+    }
+
     @Override
     boolean forAllTaskDisplayAreas(Function<TaskDisplayArea, Boolean> callback,
             boolean traverseTopToBottom) {
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/AppChildProcessTest.java b/services/tests/mockingservicestests/src/com/android/server/am/AppChildProcessTest.java
new file mode 100644
index 0000000..04e8b63
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/am/AppChildProcessTest.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.am;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManagerInternal;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+import android.platform.test.annotations.Presubmit;
+import android.util.ArraySet;
+
+import com.android.dx.mockito.inline.extended.StaticMockitoSession;
+import com.android.server.LocalServices;
+import com.android.server.ServiceThread;
+import com.android.server.am.ActivityManagerService.Injector;
+import com.android.server.appop.AppOpsService;
+import com.android.server.wm.ActivityTaskManagerService;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.quality.Strictness;
+
+import java.io.File;
+
+@Presubmit
+public class AppChildProcessTest {
+    private static final String TAG = AppChildProcessTest.class.getSimpleName();
+
+    @Rule public ServiceThreadRule mServiceThreadRule = new ServiceThreadRule();
+    @Mock private AppOpsService mAppOpsService;
+    @Mock private PackageManagerInternal mPackageManagerInt;
+    private StaticMockitoSession mMockitoSession;
+
+    private Context mContext = getInstrumentation().getTargetContext();
+    private TestInjector mInjector;
+    private ActivityManagerService mAms;
+    private ProcessList mProcessList;
+    private PhantomProcessList mPhantomProcessList;
+    private Handler mHandler;
+    private HandlerThread mHandlerThread;
+
+    @BeforeClass
+    public static void setUpOnce() {
+        System.setProperty("dexmaker.share_classloader", "true");
+    }
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mMockitoSession = mockitoSession()
+            .spyStatic(Process.class)
+            .strictness(Strictness.LENIENT)
+            .startMocking();
+
+        mHandlerThread = new HandlerThread(TAG);
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+        final ProcessList pList = new ProcessList();
+        mProcessList = spy(pList);
+
+        mInjector = new TestInjector(mContext);
+        mAms = new ActivityManagerService(mInjector, mServiceThreadRule.getThread());
+        mAms.mActivityTaskManager = new ActivityTaskManagerService(mContext);
+        mAms.mActivityTaskManager.initialize(null, null, mContext.getMainLooper());
+        mAms.mAtmInternal = spy(mAms.mActivityTaskManager.getAtmInternal());
+        mAms.mPackageManagerInt = mPackageManagerInt;
+        pList.mService = mAms;
+        mPhantomProcessList = mAms.mPhantomProcessList;
+        doReturn(new ComponentName("", "")).when(mPackageManagerInt).getSystemUiServiceComponent();
+        doReturn(false).when(() -> Process.supportsPidFd());
+        // Remove stale instance of PackageManagerInternal if there is any
+        LocalServices.removeServiceForTest(PackageManagerInternal.class);
+        LocalServices.addService(PackageManagerInternal.class, mPackageManagerInt);
+    }
+
+    @After
+    public void tearDown() {
+        LocalServices.removeServiceForTest(PackageManagerInternal.class);
+        mMockitoSession.finishMocking();
+        mHandlerThread.quit();
+    }
+
+    @Test
+    public void testManageAppChildProcesses() throws Exception {
+        final int initPid = 1;
+        final int rootUid = 0;
+        final int zygote64Pid = 100;
+        final int zygote32Pid = 101;
+        final int app1Pid = 200;
+        final int app2Pid = 201;
+        final int app1Uid = 10000;
+        final int app2Uid = 10001;
+        final int child1Pid = 300;
+        final int child2Pid = 301;
+        final int nativePid = 400;
+        final String zygote64ProcessName = "zygote64";
+        final String zygote32ProcessName = "zygote32";
+        final String app1ProcessName = "test1";
+        final String app2ProcessName = "test2";
+        final String child1ProcessName = "test1_child1";
+        final String child2ProcessName = "test1_child1_child2";
+        final String nativeProcessName = "test_native";
+
+        makeParent(zygote64Pid, initPid);
+        makeParent(zygote32Pid, initPid);
+
+        makeAppProcess(app1Pid, app1Uid, app1ProcessName, app1ProcessName);
+        makeParent(app1Pid, zygote64Pid);
+        makeAppProcess(app2Pid, app2Uid, app2ProcessName, app2ProcessName);
+        makeParent(app2Pid, zygote64Pid);
+
+        assertEquals(0, mPhantomProcessList.mPhantomProcesses.size());
+
+        // Verify zygote itself isn't a phantom process
+        assertEquals(null, mPhantomProcessList.getOrCreatePhantomProcessIfNeededLocked(
+                zygote64ProcessName, rootUid, zygote64Pid));
+        assertEquals(null, mPhantomProcessList.getOrCreatePhantomProcessIfNeededLocked(
+                zygote32ProcessName, rootUid, zygote32Pid));
+        // Verify none of the app isn't a phantom process
+        assertEquals(null, mPhantomProcessList.getOrCreatePhantomProcessIfNeededLocked(
+                app1ProcessName, app1Uid, app1Pid));
+        assertEquals(null, mPhantomProcessList.getOrCreatePhantomProcessIfNeededLocked(
+                app2ProcessName, app2Uid, app2Pid));
+
+        // "Fork" an app child process
+        makeParent(child1Pid, app1Pid);
+        PhantomProcessRecord pr = mPhantomProcessList
+                .getOrCreatePhantomProcessIfNeededLocked(child1ProcessName, app1Uid, child1Pid);
+        assertTrue(pr != null);
+        assertEquals(1, mPhantomProcessList.mPhantomProcesses.size());
+        assertEquals(pr, mPhantomProcessList.mPhantomProcesses.valueAt(0));
+        verifyPhantomProcessRecord(pr, child1ProcessName, app1Uid, child1Pid);
+
+        // Create another native process from init
+        makeParent(nativePid, initPid);
+        assertEquals(null, mPhantomProcessList.getOrCreatePhantomProcessIfNeededLocked(
+                nativeProcessName, rootUid, nativePid));
+        assertEquals(1, mPhantomProcessList.mPhantomProcesses.size());
+        assertEquals(pr, mPhantomProcessList.mPhantomProcesses.valueAt(0));
+
+        // "Fork" another app child process
+        makeParent(child2Pid, child1Pid);
+        PhantomProcessRecord pr2 = mPhantomProcessList
+                .getOrCreatePhantomProcessIfNeededLocked(child2ProcessName, app1Uid, child2Pid);
+        assertTrue(pr2 != null);
+        assertEquals(2, mPhantomProcessList.mPhantomProcesses.size());
+        verifyPhantomProcessRecord(pr2, child2ProcessName, app1Uid, child2Pid);
+
+        ArraySet<PhantomProcessRecord> set = new ArraySet<>();
+        set.add(pr);
+        set.add(pr2);
+        for (int i = mPhantomProcessList.mPhantomProcesses.size() - 1; i >= 0; i--) {
+            set.remove(mPhantomProcessList.mPhantomProcesses.valueAt(i));
+        }
+        assertEquals(0, set.size());
+    }
+
+    private void verifyPhantomProcessRecord(PhantomProcessRecord pr,
+            String processName, int uid, int pid) {
+        assertEquals(processName, pr.mProcessName);
+        assertEquals(uid, pr.mUid);
+        assertEquals(pid, pr.mPid);
+    }
+
+    private void makeAppProcess(int pid, int uid, String packageName, String processName) {
+        ApplicationInfo ai = new ApplicationInfo();
+        ai.packageName = packageName;
+        ProcessRecord app = new ProcessRecord(mAms, ai, processName, uid);
+        app.pid = pid;
+        mAms.mPidsSelfLocked.doAddInternal(app);
+    }
+
+    private void makeParent(int pid, int ppid) {
+        doReturn(ppid).when(() -> Process.getParentPid(eq(pid)));
+    }
+
+    private class TestInjector extends Injector {
+        TestInjector(Context context) {
+            super(context);
+        }
+
+        @Override
+        public AppOpsService getAppOpsService(File file, Handler handler) {
+            return mAppOpsService;
+        }
+
+        @Override
+        public Handler getUiHandler(ActivityManagerService service) {
+            return mHandler;
+        }
+
+        @Override
+        public ProcessList getProcessList(ActivityManagerService service) {
+            return mProcessList;
+        }
+    }
+
+    static class ServiceThreadRule implements TestRule {
+        private ServiceThread mThread;
+
+        ServiceThread getThread() {
+            return mThread;
+        }
+
+        @Override
+        public Statement apply(Statement base, Description description) {
+            return new Statement() {
+                @Override
+                public void evaluate() throws Throwable {
+                    mThread = new ServiceThread("TestServiceThread",
+                            Process.THREAD_PRIORITY_DEFAULT, true /* allowIo */);
+                    mThread.start();
+                    try {
+                        base.evaluate();
+                    } finally {
+                        mThread.getThreadHandler().runWithScissors(mThread::quit, 0 /* timeout */);
+                    }
+                }
+            };
+        }
+    }
+
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskStackChangedListenerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskStackChangedListenerTest.java
index ca3f815..fa2c942 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskStackChangedListenerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskStackChangedListenerTest.java
@@ -219,19 +219,13 @@
         activity.setDetachedFromWindowLatch(onDetachedFromWindowLatch);
         final int id = activity.getTaskId();
 
-        // Test for onTaskCreated.
-        waitForCallback(taskCreatedLaunchLatch);
+        // Test for onTaskCreated and onTaskMovedToFront
+        waitForCallback(taskMovedToFrontLatch);
+        assertEquals(0, taskCreatedLaunchLatch.getCount());
         assertEquals(id, params[0]);
         ComponentName componentName = (ComponentName) params[1];
         assertEquals(ActivityTaskChangeCallbacks.class.getName(), componentName.getClassName());
 
-        // Test for onTaskMovedToFront.
-        assertEquals(1, taskMovedToFrontLatch.getCount());
-        mService.moveTaskToFront(null, getInstrumentation().getContext().getPackageName(), id, 0,
-                null);
-        waitForCallback(taskMovedToFrontLatch);
-        assertEquals(activity.getTaskId(), params[0]);
-
         // Test for onTaskRemovalStarted.
         assertEquals(1, taskRemovalStartedLatch.getCount());
         assertEquals(1, taskRemovedLatch.getCount());