Merge "Only show error if screenshot process dies within timeout period" into rvc-dev
diff --git a/apex/media/framework/java/android/media/MediaParser.java b/apex/media/framework/java/android/media/MediaParser.java
index 070b13b..7cbb98e 100644
--- a/apex/media/framework/java/android/media/MediaParser.java
+++ b/apex/media/framework/java/android/media/MediaParser.java
@@ -219,7 +219,8 @@
          * duration is unknown.
          */
         public long getDurationMicros() {
-            return mExoPlayerSeekMap.getDurationUs();
+            long durationUs = mExoPlayerSeekMap.getDurationUs();
+            return durationUs != C.TIME_UNSET ? durationUs : UNKNOWN_DURATION;
         }
 
         /**
diff --git a/apex/statsd/framework/java/android/util/StatsEvent.java b/apex/statsd/framework/java/android/util/StatsEvent.java
index 8bd36a5..8be5c63 100644
--- a/apex/statsd/framework/java/android/util/StatsEvent.java
+++ b/apex/statsd/framework/java/android/util/StatsEvent.java
@@ -26,6 +26,8 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 
+import java.util.Arrays;
+
 /**
  * StatsEvent builds and stores the buffer sent over the statsd socket.
  * This class defines and encapsulates the socket protocol.
@@ -224,7 +226,9 @@
 
     // Max payload size is 4 bytes less as 4 bytes are reserved for statsEventTag.
     // See android_util_StatsLog.cpp.
-    private static final int MAX_PAYLOAD_SIZE = LOGGER_ENTRY_MAX_PAYLOAD - 4;
+    private static final int MAX_PUSH_PAYLOAD_SIZE = LOGGER_ENTRY_MAX_PAYLOAD - 4;
+
+    private static final int MAX_PULL_PAYLOAD_SIZE = 50 * 1024; // 50 KB
 
     private final int mAtomId;
     private final byte[] mPayload;
@@ -619,6 +623,7 @@
         @NonNull
         public Builder usePooledBuffer() {
             mUsePooledBuffer = true;
+            mBuffer.setMaxSize(MAX_PUSH_PAYLOAD_SIZE, mPos);
             return this;
         }
 
@@ -694,8 +699,9 @@
         @GuardedBy("sLock")
         private static Buffer sPool;
 
-        private final byte[] mBytes = new byte[MAX_PAYLOAD_SIZE];
+        private byte[] mBytes = new byte[MAX_PUSH_PAYLOAD_SIZE];
         private boolean mOverflow = false;
+        private int mMaxSize = MAX_PULL_PAYLOAD_SIZE;
 
         @NonNull
         private static Buffer obtain() {
@@ -717,15 +723,26 @@
         }
 
         private void release() {
-            synchronized (sLock) {
-                if (null == sPool) {
-                    sPool = this;
+            // Recycle this Buffer if its size is MAX_PUSH_PAYLOAD_SIZE or under.
+            if (mBytes.length <= MAX_PUSH_PAYLOAD_SIZE) {
+                synchronized (sLock) {
+                    if (null == sPool) {
+                        sPool = this;
+                    }
                 }
             }
         }
 
         private void reset() {
             mOverflow = false;
+            mMaxSize = MAX_PULL_PAYLOAD_SIZE;
+        }
+
+        private void setMaxSize(final int maxSize, final int numBytesWritten) {
+            mMaxSize = maxSize;
+            if (numBytesWritten > maxSize) {
+                mOverflow = true;
+            }
         }
 
         private boolean hasOverflowed() {
@@ -740,11 +757,28 @@
          * @return true if space is available, false otherwise.
          **/
         private boolean hasEnoughSpace(final int index, final int numBytes) {
-            final boolean result = index + numBytes < MAX_PAYLOAD_SIZE;
-            if (!result) {
+            final int totalBytesNeeded = index + numBytes;
+
+            if (totalBytesNeeded > mMaxSize) {
                 mOverflow = true;
+                return false;
             }
-            return result;
+
+            // Expand buffer if needed.
+            if (mBytes.length < mMaxSize && totalBytesNeeded > mBytes.length) {
+                int newSize = mBytes.length;
+                do {
+                    newSize *= 2;
+                } while (newSize <= totalBytesNeeded);
+
+                if (newSize > mMaxSize) {
+                    newSize = mMaxSize;
+                }
+
+                mBytes = Arrays.copyOf(mBytes, newSize);
+            }
+
+            return true;
         }
 
         /**
diff --git a/apex/statsd/framework/test/src/android/util/StatsEventTest.java b/apex/statsd/framework/test/src/android/util/StatsEventTest.java
index 7b51155..8d26369 100644
--- a/apex/statsd/framework/test/src/android/util/StatsEventTest.java
+++ b/apex/statsd/framework/test/src/android/util/StatsEventTest.java
@@ -33,6 +33,7 @@
 
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
+import java.util.Random;
 
 /**
  * Internal tests for {@link StatsEvent}.
@@ -644,6 +645,165 @@
         statsEvent.release();
     }
 
+    @Test
+    public void testLargePulledEvent() {
+        final int expectedAtomId = 10_020;
+        byte[] field1 = new byte[10 * 1024];
+        new Random().nextBytes(field1);
+
+        final long minTimestamp = SystemClock.elapsedRealtimeNanos();
+        final StatsEvent statsEvent =
+                StatsEvent.newBuilder().setAtomId(expectedAtomId).writeByteArray(field1).build();
+        final long maxTimestamp = SystemClock.elapsedRealtimeNanos();
+
+        assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId);
+
+        final ByteBuffer buffer =
+                ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN);
+
+        assertWithMessage("Root element in buffer is not TYPE_OBJECT")
+                .that(buffer.get())
+                .isEqualTo(StatsEvent.TYPE_OBJECT);
+
+        assertWithMessage("Incorrect number of elements in root object")
+                .that(buffer.get())
+                .isEqualTo(3);
+
+        assertWithMessage("First element is not timestamp")
+                .that(buffer.get())
+                .isEqualTo(StatsEvent.TYPE_LONG);
+
+        assertWithMessage("Incorrect timestamp")
+                .that(buffer.getLong())
+                .isIn(Range.closed(minTimestamp, maxTimestamp));
+
+        assertWithMessage("Second element is not atom id")
+                .that(buffer.get())
+                .isEqualTo(StatsEvent.TYPE_INT);
+
+        assertWithMessage("Incorrect atom id").that(buffer.getInt()).isEqualTo(expectedAtomId);
+
+        assertWithMessage("Third element is not byte array")
+                .that(buffer.get())
+                .isEqualTo(StatsEvent.TYPE_BYTE_ARRAY);
+
+        final byte[] field1Actual = getByteArrayFromByteBuffer(buffer);
+        assertWithMessage("Incorrect field 1").that(field1Actual).isEqualTo(field1);
+
+        assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position());
+
+        statsEvent.release();
+    }
+
+    @Test
+    public void testPulledEventOverflow() {
+        final int expectedAtomId = 10_020;
+        byte[] field1 = new byte[50 * 1024];
+        new Random().nextBytes(field1);
+
+        final long minTimestamp = SystemClock.elapsedRealtimeNanos();
+        final StatsEvent statsEvent =
+                StatsEvent.newBuilder().setAtomId(expectedAtomId).writeByteArray(field1).build();
+        final long maxTimestamp = SystemClock.elapsedRealtimeNanos();
+
+        assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId);
+
+        final ByteBuffer buffer =
+                ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN);
+
+        assertWithMessage("Root element in buffer is not TYPE_OBJECT")
+                .that(buffer.get())
+                .isEqualTo(StatsEvent.TYPE_OBJECT);
+
+        assertWithMessage("Incorrect number of elements in root object")
+                .that(buffer.get())
+                .isEqualTo(3);
+
+        assertWithMessage("First element is not timestamp")
+                .that(buffer.get())
+                .isEqualTo(StatsEvent.TYPE_LONG);
+
+        assertWithMessage("Incorrect timestamp")
+                .that(buffer.getLong())
+                .isIn(Range.closed(minTimestamp, maxTimestamp));
+
+        assertWithMessage("Second element is not atom id")
+                .that(buffer.get())
+                .isEqualTo(StatsEvent.TYPE_INT);
+
+        assertWithMessage("Incorrect atom id").that(buffer.getInt()).isEqualTo(expectedAtomId);
+
+        assertWithMessage("Third element is not errors type")
+                .that(buffer.get())
+                .isEqualTo(StatsEvent.TYPE_ERRORS);
+
+        final int errorMask = buffer.getInt();
+
+        assertWithMessage("ERROR_OVERFLOW should be the only error in the error mask")
+                .that(errorMask)
+                .isEqualTo(StatsEvent.ERROR_OVERFLOW);
+
+        assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position());
+
+        statsEvent.release();
+    }
+
+    @Test
+    public void testPushedEventOverflow() {
+        final int expectedAtomId = 10_020;
+        byte[] field1 = new byte[10 * 1024];
+        new Random().nextBytes(field1);
+
+        final long minTimestamp = SystemClock.elapsedRealtimeNanos();
+        final StatsEvent statsEvent = StatsEvent.newBuilder()
+                                              .setAtomId(expectedAtomId)
+                                              .writeByteArray(field1)
+                                              .usePooledBuffer()
+                                              .build();
+        final long maxTimestamp = SystemClock.elapsedRealtimeNanos();
+
+        assertThat(statsEvent.getAtomId()).isEqualTo(expectedAtomId);
+
+        final ByteBuffer buffer =
+                ByteBuffer.wrap(statsEvent.getBytes()).order(ByteOrder.LITTLE_ENDIAN);
+
+        assertWithMessage("Root element in buffer is not TYPE_OBJECT")
+                .that(buffer.get())
+                .isEqualTo(StatsEvent.TYPE_OBJECT);
+
+        assertWithMessage("Incorrect number of elements in root object")
+                .that(buffer.get())
+                .isEqualTo(3);
+
+        assertWithMessage("First element is not timestamp")
+                .that(buffer.get())
+                .isEqualTo(StatsEvent.TYPE_LONG);
+
+        assertWithMessage("Incorrect timestamp")
+                .that(buffer.getLong())
+                .isIn(Range.closed(minTimestamp, maxTimestamp));
+
+        assertWithMessage("Second element is not atom id")
+                .that(buffer.get())
+                .isEqualTo(StatsEvent.TYPE_INT);
+
+        assertWithMessage("Incorrect atom id").that(buffer.getInt()).isEqualTo(expectedAtomId);
+
+        assertWithMessage("Third element is not errors type")
+                .that(buffer.get())
+                .isEqualTo(StatsEvent.TYPE_ERRORS);
+
+        final int errorMask = buffer.getInt();
+
+        assertWithMessage("ERROR_OVERFLOW should be the only error in the error mask")
+                .that(errorMask)
+                .isEqualTo(StatsEvent.ERROR_OVERFLOW);
+
+        assertThat(statsEvent.getNumBytes()).isEqualTo(buffer.position());
+
+        statsEvent.release();
+    }
+
     private static byte[] getByteArrayFromByteBuffer(final ByteBuffer buffer) {
         final int numBytes = buffer.getInt();
         byte[] bytes = new byte[numBytes];
diff --git a/api/test-current.txt b/api/test-current.txt
index ed4c9b1..66b5015 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -1298,6 +1298,130 @@
 
 }
 
+package android.hardware.hdmi {
+
+  public final class HdmiControlManager {
+    method @Nullable public android.hardware.hdmi.HdmiSwitchClient getSwitchClient();
+    method @RequiresPermission("android.permission.HDMI_CEC") public void setStandbyMode(boolean);
+    field public static final String ACTION_OSD_MESSAGE = "android.hardware.hdmi.action.OSD_MESSAGE";
+    field public static final int AVR_VOLUME_MUTED = 101; // 0x65
+    field public static final int CLEAR_TIMER_STATUS_CEC_DISABLE = 162; // 0xa2
+    field public static final int CLEAR_TIMER_STATUS_CHECK_RECORDER_CONNECTION = 160; // 0xa0
+    field public static final int CLEAR_TIMER_STATUS_FAIL_TO_CLEAR_SELECTED_SOURCE = 161; // 0xa1
+    field public static final int CLEAR_TIMER_STATUS_TIMER_CLEARED = 128; // 0x80
+    field public static final int CLEAR_TIMER_STATUS_TIMER_NOT_CLEARED_NO_INFO_AVAILABLE = 2; // 0x2
+    field public static final int CLEAR_TIMER_STATUS_TIMER_NOT_CLEARED_NO_MATCHING = 1; // 0x1
+    field public static final int CLEAR_TIMER_STATUS_TIMER_NOT_CLEARED_RECORDING = 0; // 0x0
+    field public static final int CONTROL_STATE_CHANGED_REASON_SETTING = 1; // 0x1
+    field public static final int CONTROL_STATE_CHANGED_REASON_STANDBY = 3; // 0x3
+    field public static final int CONTROL_STATE_CHANGED_REASON_START = 0; // 0x0
+    field public static final int CONTROL_STATE_CHANGED_REASON_WAKEUP = 2; // 0x2
+    field public static final int DEVICE_EVENT_ADD_DEVICE = 1; // 0x1
+    field public static final int DEVICE_EVENT_REMOVE_DEVICE = 2; // 0x2
+    field public static final int DEVICE_EVENT_UPDATE_DEVICE = 3; // 0x3
+    field public static final String EXTRA_MESSAGE_EXTRA_PARAM1 = "android.hardware.hdmi.extra.MESSAGE_EXTRA_PARAM1";
+    field public static final String EXTRA_MESSAGE_ID = "android.hardware.hdmi.extra.MESSAGE_ID";
+    field public static final int ONE_TOUCH_RECORD_ALREADY_RECORDING = 18; // 0x12
+    field public static final int ONE_TOUCH_RECORD_CEC_DISABLED = 51; // 0x33
+    field public static final int ONE_TOUCH_RECORD_CHECK_RECORDER_CONNECTION = 49; // 0x31
+    field public static final int ONE_TOUCH_RECORD_DISALLOW_TO_COPY = 13; // 0xd
+    field public static final int ONE_TOUCH_RECORD_DISALLOW_TO_FUTHER_COPIES = 14; // 0xe
+    field public static final int ONE_TOUCH_RECORD_FAIL_TO_RECORD_DISPLAYED_SCREEN = 50; // 0x32
+    field public static final int ONE_TOUCH_RECORD_INVALID_EXTERNAL_PHYSICAL_ADDRESS = 10; // 0xa
+    field public static final int ONE_TOUCH_RECORD_INVALID_EXTERNAL_PLUG_NUMBER = 9; // 0x9
+    field public static final int ONE_TOUCH_RECORD_MEDIA_PROBLEM = 21; // 0x15
+    field public static final int ONE_TOUCH_RECORD_MEDIA_PROTECTED = 19; // 0x13
+    field public static final int ONE_TOUCH_RECORD_NOT_ENOUGH_SPACE = 22; // 0x16
+    field public static final int ONE_TOUCH_RECORD_NO_MEDIA = 16; // 0x10
+    field public static final int ONE_TOUCH_RECORD_NO_OR_INSUFFICIENT_CA_ENTITLEMENTS = 12; // 0xc
+    field public static final int ONE_TOUCH_RECORD_NO_SOURCE_SIGNAL = 20; // 0x14
+    field public static final int ONE_TOUCH_RECORD_OTHER_REASON = 31; // 0x1f
+    field public static final int ONE_TOUCH_RECORD_PARENT_LOCK_ON = 23; // 0x17
+    field public static final int ONE_TOUCH_RECORD_PLAYING = 17; // 0x11
+    field public static final int ONE_TOUCH_RECORD_PREVIOUS_RECORDING_IN_PROGRESS = 48; // 0x30
+    field public static final int ONE_TOUCH_RECORD_RECORDING_ALREADY_TERMINATED = 27; // 0x1b
+    field public static final int ONE_TOUCH_RECORD_RECORDING_ANALOGUE_SERVICE = 3; // 0x3
+    field public static final int ONE_TOUCH_RECORD_RECORDING_CURRENTLY_SELECTED_SOURCE = 1; // 0x1
+    field public static final int ONE_TOUCH_RECORD_RECORDING_DIGITAL_SERVICE = 2; // 0x2
+    field public static final int ONE_TOUCH_RECORD_RECORDING_EXTERNAL_INPUT = 4; // 0x4
+    field public static final int ONE_TOUCH_RECORD_RECORDING_TERMINATED_NORMALLY = 26; // 0x1a
+    field public static final int ONE_TOUCH_RECORD_UNABLE_ANALOGUE_SERVICE = 6; // 0x6
+    field public static final int ONE_TOUCH_RECORD_UNABLE_DIGITAL_SERVICE = 5; // 0x5
+    field public static final int ONE_TOUCH_RECORD_UNABLE_SELECTED_SERVICE = 7; // 0x7
+    field public static final int ONE_TOUCH_RECORD_UNSUPPORTED_CA = 11; // 0xb
+    field public static final int OSD_MESSAGE_ARC_CONNECTED_INVALID_PORT = 1; // 0x1
+    field public static final int OSD_MESSAGE_AVR_VOLUME_CHANGED = 2; // 0x2
+    field public static final int POWER_STATUS_ON = 0; // 0x0
+    field public static final int POWER_STATUS_STANDBY = 1; // 0x1
+    field public static final int POWER_STATUS_TRANSIENT_TO_ON = 2; // 0x2
+    field public static final int POWER_STATUS_TRANSIENT_TO_STANDBY = 3; // 0x3
+    field public static final int POWER_STATUS_UNKNOWN = -1; // 0xffffffff
+    field @Deprecated public static final int RESULT_ALREADY_IN_PROGRESS = 4; // 0x4
+    field public static final int RESULT_COMMUNICATION_FAILED = 7; // 0x7
+    field public static final int RESULT_EXCEPTION = 5; // 0x5
+    field public static final int RESULT_INCORRECT_MODE = 6; // 0x6
+    field public static final int RESULT_SOURCE_NOT_AVAILABLE = 2; // 0x2
+    field public static final int RESULT_SUCCESS = 0; // 0x0
+    field public static final int RESULT_TARGET_NOT_AVAILABLE = 3; // 0x3
+    field public static final int RESULT_TIMEOUT = 1; // 0x1
+    field public static final int TIMER_RECORDING_RESULT_EXTRA_CEC_DISABLED = 3; // 0x3
+    field public static final int TIMER_RECORDING_RESULT_EXTRA_CHECK_RECORDER_CONNECTION = 1; // 0x1
+    field public static final int TIMER_RECORDING_RESULT_EXTRA_FAIL_TO_RECORD_SELECTED_SOURCE = 2; // 0x2
+    field public static final int TIMER_RECORDING_RESULT_EXTRA_NO_ERROR = 0; // 0x0
+    field public static final int TIMER_RECORDING_TYPE_ANALOGUE = 2; // 0x2
+    field public static final int TIMER_RECORDING_TYPE_DIGITAL = 1; // 0x1
+    field public static final int TIMER_RECORDING_TYPE_EXTERNAL = 3; // 0x3
+    field public static final int TIMER_STATUS_MEDIA_INFO_NOT_PRESENT = 2; // 0x2
+    field public static final int TIMER_STATUS_MEDIA_INFO_PRESENT_NOT_PROTECTED = 0; // 0x0
+    field public static final int TIMER_STATUS_MEDIA_INFO_PRESENT_PROTECTED = 1; // 0x1
+    field public static final int TIMER_STATUS_NOT_PROGRAMMED_CA_NOT_SUPPORTED = 6; // 0x6
+    field public static final int TIMER_STATUS_NOT_PROGRAMMED_CLOCK_FAILURE = 10; // 0xa
+    field public static final int TIMER_STATUS_NOT_PROGRAMMED_DATE_OUT_OF_RANGE = 2; // 0x2
+    field public static final int TIMER_STATUS_NOT_PROGRAMMED_DUPLICATED = 14; // 0xe
+    field public static final int TIMER_STATUS_NOT_PROGRAMMED_INVALID_EXTERNAL_PHYSICAL_NUMBER = 5; // 0x5
+    field public static final int TIMER_STATUS_NOT_PROGRAMMED_INVALID_EXTERNAL_PLUG_NUMBER = 4; // 0x4
+    field public static final int TIMER_STATUS_NOT_PROGRAMMED_INVALID_SEQUENCE = 3; // 0x3
+    field public static final int TIMER_STATUS_NOT_PROGRAMMED_NO_CA_ENTITLEMENTS = 7; // 0x7
+    field public static final int TIMER_STATUS_NOT_PROGRAMMED_NO_FREE_TIME = 1; // 0x1
+    field public static final int TIMER_STATUS_NOT_PROGRAMMED_PARENTAL_LOCK_ON = 9; // 0x9
+    field public static final int TIMER_STATUS_NOT_PROGRAMMED_UNSUPPORTED_RESOLUTION = 8; // 0x8
+    field public static final int TIMER_STATUS_PROGRAMMED_INFO_ENOUGH_SPACE = 8; // 0x8
+    field public static final int TIMER_STATUS_PROGRAMMED_INFO_MIGHT_NOT_ENOUGH_SPACE = 11; // 0xb
+    field public static final int TIMER_STATUS_PROGRAMMED_INFO_NOT_ENOUGH_SPACE = 9; // 0x9
+    field public static final int TIMER_STATUS_PROGRAMMED_INFO_NO_MEDIA_INFO = 10; // 0xa
+  }
+
+  public final class HdmiControlServiceWrapper {
+    ctor public HdmiControlServiceWrapper();
+    method @NonNull public android.hardware.hdmi.HdmiControlManager createHdmiControlManager();
+    method @BinderThread public void setDeviceTypes(@NonNull int[]);
+    method @BinderThread public void setPortInfo(@NonNull java.util.List<android.hardware.hdmi.HdmiPortInfo>);
+    field public static final int DEVICE_PURE_CEC_SWITCH = 6; // 0x6
+  }
+
+  public final class HdmiPortInfo implements android.os.Parcelable {
+    ctor public HdmiPortInfo(int, int, int, boolean, boolean, boolean);
+    method public int describeContents();
+    method public int getAddress();
+    method public int getId();
+    method public int getType();
+    method public boolean isArcSupported();
+    method public boolean isCecSupported();
+    method public boolean isMhlSupported();
+    field @NonNull public static final android.os.Parcelable.Creator<android.hardware.hdmi.HdmiPortInfo> CREATOR;
+    field public static final int PORT_INPUT = 0; // 0x0
+    field public static final int PORT_OUTPUT = 1; // 0x1
+  }
+
+  public class HdmiSwitchClient {
+    method public int getDeviceType();
+    method @NonNull public java.util.List<android.hardware.hdmi.HdmiPortInfo> getPortInfo();
+    method public void sendKeyEvent(int, boolean);
+    method public void sendVendorCommand(int, byte[], boolean);
+  }
+
+}
+
 package android.hardware.lights {
 
   public final class Light implements android.os.Parcelable {
diff --git a/cmds/idmap2/Android.bp b/cmds/idmap2/Android.bp
index ef5c4ce..fb58305 100644
--- a/cmds/idmap2/Android.bp
+++ b/cmds/idmap2/Android.bp
@@ -168,13 +168,13 @@
     ],
     host_supported: true,
     srcs: [
+        "idmap2/CommandUtils.cpp",
         "idmap2/Create.cpp",
         "idmap2/CreateMultiple.cpp",
         "idmap2/Dump.cpp",
         "idmap2/Lookup.cpp",
         "idmap2/Main.cpp",
         "idmap2/Scan.cpp",
-        "idmap2/Verify.cpp",
     ],
     target: {
         android: {
diff --git a/cmds/idmap2/idmap2/Verify.cpp b/cmds/idmap2/idmap2/CommandUtils.cpp
similarity index 70%
rename from cmds/idmap2/idmap2/Verify.cpp
rename to cmds/idmap2/idmap2/CommandUtils.cpp
index 9cb67b3..e058cd6e 100644
--- a/cmds/idmap2/idmap2/Verify.cpp
+++ b/cmds/idmap2/idmap2/CommandUtils.cpp
@@ -19,30 +19,19 @@
 #include <string>
 #include <vector>
 
-#include "idmap2/CommandLineOptions.h"
 #include "idmap2/Idmap.h"
 #include "idmap2/Result.h"
 #include "idmap2/SysTrace.h"
 
-using android::idmap2::CommandLineOptions;
 using android::idmap2::Error;
 using android::idmap2::IdmapHeader;
 using android::idmap2::Result;
 using android::idmap2::Unit;
 
-Result<Unit> Verify(const std::vector<std::string>& args) {
-  SYSTRACE << "Verify " << args;
-  std::string idmap_path;
-
-  const CommandLineOptions opts =
-      CommandLineOptions("idmap2 verify")
-          .MandatoryOption("--idmap-path", "input: path to idmap file to verify", &idmap_path);
-
-  const auto opts_ok = opts.Parse(args);
-  if (!opts_ok) {
-    return opts_ok.GetError();
-  }
-
+Result<Unit> Verify(const std::string& idmap_path, const std::string& target_path,
+                    const std::string& overlay_path, uint32_t fulfilled_policies,
+                    bool enforce_overlayable) {
+  SYSTRACE << "Verify " << idmap_path;
   std::ifstream fin(idmap_path);
   const std::unique_ptr<const IdmapHeader> header = IdmapHeader::FromBinaryStream(fin);
   fin.close();
@@ -50,7 +39,8 @@
     return Error("failed to parse idmap header");
   }
 
-  const auto header_ok = header->IsUpToDate();
+  const auto header_ok = header->IsUpToDate(target_path.c_str(), overlay_path.c_str(),
+                                            fulfilled_policies, enforce_overlayable);
   if (!header_ok) {
     return Error(header_ok.GetError(), "idmap not up to date");
   }
diff --git a/cmds/idmap2/idmap2/CommandUtils.h b/cmds/idmap2/idmap2/CommandUtils.h
new file mode 100644
index 0000000..99605de
--- /dev/null
+++ b/cmds/idmap2/idmap2/CommandUtils.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef IDMAP2_IDMAP2_COMMAND_UTILS_H_
+#define IDMAP2_IDMAP2_COMMAND_UTILS_H_
+
+#include "idmap2/Result.h"
+
+android::idmap2::Result<android::idmap2::Unit> Verify(const std::string& idmap_path,
+                                                      const std::string& target_path,
+                                                      const std::string& overlay_path,
+                                                      uint32_t fulfilled_policies,
+                                                      bool enforce_overlayable);
+
+#endif  // IDMAP2_IDMAP2_COMMAND_UTILS_H_
diff --git a/cmds/idmap2/idmap2/Commands.h b/cmds/idmap2/idmap2/Commands.h
index e626738..69eea8d 100644
--- a/cmds/idmap2/idmap2/Commands.h
+++ b/cmds/idmap2/idmap2/Commands.h
@@ -27,6 +27,5 @@
 android::idmap2::Result<android::idmap2::Unit> Dump(const std::vector<std::string>& args);
 android::idmap2::Result<android::idmap2::Unit> Lookup(const std::vector<std::string>& args);
 android::idmap2::Result<android::idmap2::Unit> Scan(const std::vector<std::string>& args);
-android::idmap2::Result<android::idmap2::Unit> Verify(const std::vector<std::string>& args);
 
 #endif  // IDMAP2_IDMAP2_COMMANDS_H_
diff --git a/cmds/idmap2/idmap2/CreateMultiple.cpp b/cmds/idmap2/idmap2/CreateMultiple.cpp
index 4b70acc..abdfaf4 100644
--- a/cmds/idmap2/idmap2/CreateMultiple.cpp
+++ b/cmds/idmap2/idmap2/CreateMultiple.cpp
@@ -26,6 +26,7 @@
 #include "android-base/stringprintf.h"
 #include "idmap2/BinaryStreamVisitor.h"
 #include "idmap2/CommandLineOptions.h"
+#include "idmap2/CommandUtils.h"
 #include "idmap2/FileUtils.h"
 #include "idmap2/Idmap.h"
 #include "idmap2/Policies.h"
@@ -103,7 +104,8 @@
       continue;
     }
 
-    if (!Verify(std::vector<std::string>({"--idmap-path", idmap_path}))) {
+    if (!Verify(idmap_path, target_apk_path, overlay_apk_path, fulfilled_policies,
+                !ignore_overlayable)) {
       const std::unique_ptr<const ApkAssets> overlay_apk = ApkAssets::Load(overlay_apk_path);
       if (!overlay_apk) {
         LOG(WARNING) << "failed to load apk " << overlay_apk_path.c_str();
diff --git a/cmds/idmap2/idmap2/Main.cpp b/cmds/idmap2/idmap2/Main.cpp
index a07e793..fb093f0 100644
--- a/cmds/idmap2/idmap2/Main.cpp
+++ b/cmds/idmap2/idmap2/Main.cpp
@@ -53,9 +53,8 @@
 int main(int argc, char** argv) {
   SYSTRACE << "main";
   const NameToFunctionMap commands = {
-      {"create", Create}, {"create-multiple", CreateMultiple},
-      {"dump", Dump},     {"lookup", Lookup},
-      {"scan", Scan},     {"verify", Verify},
+      {"create", Create}, {"create-multiple", CreateMultiple}, {"dump", Dump}, {"lookup", Lookup},
+      {"scan", Scan},
   };
   if (argc <= 1) {
     PrintUsage(commands, std::cerr);
diff --git a/cmds/idmap2/idmap2/Scan.cpp b/cmds/idmap2/idmap2/Scan.cpp
index da04532..3625045 100644
--- a/cmds/idmap2/idmap2/Scan.cpp
+++ b/cmds/idmap2/idmap2/Scan.cpp
@@ -27,8 +27,11 @@
 #include "Commands.h"
 #include "android-base/properties.h"
 #include "idmap2/CommandLineOptions.h"
+#include "idmap2/CommandUtils.h"
 #include "idmap2/FileUtils.h"
 #include "idmap2/Idmap.h"
+#include "idmap2/Policies.h"
+#include "idmap2/PolicyUtils.h"
 #include "idmap2/ResourceUtils.h"
 #include "idmap2/Result.h"
 #include "idmap2/SysTrace.h"
@@ -48,6 +51,7 @@
 using android::idmap2::utils::ExtractOverlayManifestInfo;
 using android::idmap2::utils::FindFiles;
 using android::idmap2::utils::OverlayManifestInfo;
+using android::idmap2::utils::PoliciesToBitmaskResult;
 
 using PolicyBitmask = android::ResTable_overlayable_policy_header::PolicyBitmask;
 
@@ -215,7 +219,15 @@
 
   std::stringstream stream;
   for (const auto& overlay : interesting_apks) {
-    if (!Verify(std::vector<std::string>({"--idmap-path", overlay.idmap_path}))) {
+    const auto policy_bitmask = PoliciesToBitmaskResult(overlay.policies);
+    if (!policy_bitmask) {
+      LOG(WARNING) << "failed to create idmap for overlay apk path \"" << overlay.apk_path
+                   << "\": " << policy_bitmask.GetErrorMessage();
+      continue;
+    }
+
+    if (!Verify(overlay.idmap_path, target_apk_path, overlay.apk_path, *policy_bitmask,
+                !overlay.ignore_overlayable)) {
       std::vector<std::string> create_args = {"--target-apk-path",  target_apk_path,
                                               "--overlay-apk-path", overlay.apk_path,
                                               "--idmap-path",       overlay.idmap_path};
diff --git a/cmds/idmap2/idmap2d/Idmap2Service.cpp b/cmds/idmap2/idmap2d/Idmap2Service.cpp
index a93184f..908d966 100644
--- a/cmds/idmap2/idmap2d/Idmap2Service.cpp
+++ b/cmds/idmap2/idmap2d/Idmap2Service.cpp
@@ -33,16 +33,19 @@
 #include "idmap2/BinaryStreamVisitor.h"
 #include "idmap2/FileUtils.h"
 #include "idmap2/Idmap.h"
+#include "idmap2/Result.h"
 #include "idmap2/SysTrace.h"
 #include "idmap2/ZipFile.h"
 #include "utils/String8.h"
 
 using android::IPCThreadState;
+using android::base::StringPrintf;
 using android::binder::Status;
 using android::idmap2::BinaryStreamVisitor;
 using android::idmap2::GetPackageCrc;
 using android::idmap2::Idmap;
 using android::idmap2::IdmapHeader;
+using android::idmap2::ZipFile;
 using android::idmap2::utils::kIdmapCacheDir;
 using android::idmap2::utils::kIdmapFilePermissionMask;
 using android::idmap2::utils::UidHasWriteAccessToPath;
@@ -66,6 +69,21 @@
   return static_cast<PolicyBitmask>(arg);
 }
 
+Status GetCrc(const std::string& apk_path, uint32_t* out_crc) {
+  const auto overlay_zip = ZipFile::Open(apk_path);
+  if (!overlay_zip) {
+    return error(StringPrintf("failed to open apk %s", apk_path.c_str()));
+  }
+
+  const auto crc = GetPackageCrc(*overlay_zip);
+  if (!crc) {
+    return error(crc.GetErrorMessage());
+  }
+
+  *out_crc = *crc;
+  return ok();
+}
+
 }  // namespace
 
 namespace android::os {
@@ -98,10 +116,9 @@
 }
 
 Status Idmap2Service::verifyIdmap(const std::string& target_apk_path,
-                                  const std::string& overlay_apk_path,
-                                  int32_t fulfilled_policies ATTRIBUTE_UNUSED,
-                                  bool enforce_overlayable ATTRIBUTE_UNUSED,
-                                  int32_t user_id ATTRIBUTE_UNUSED, bool* _aidl_return) {
+                                  const std::string& overlay_apk_path, int32_t fulfilled_policies,
+                                  bool enforce_overlayable, int32_t user_id ATTRIBUTE_UNUSED,
+                                  bool* _aidl_return) {
   SYSTRACE << "Idmap2Service::verifyIdmap " << overlay_apk_path;
   assert(_aidl_return);
   const std::string idmap_path = Idmap::CanonicalIdmapPathFor(kIdmapCacheDir, overlay_apk_path);
@@ -113,34 +130,38 @@
     return error("failed to parse idmap header");
   }
 
-  if (strcmp(header->GetTargetPath().data(), target_apk_path.data()) != 0) {
-    *_aidl_return = false;
-    return ok();
-  }
-
-  if (target_apk_path != kFrameworkPath) {
-    *_aidl_return = (bool) header->IsUpToDate();
+  uint32_t target_crc;
+  if (target_apk_path == kFrameworkPath && android_crc_) {
+    target_crc = *android_crc_;
   } else {
-    if (!android_crc_) {
-      // Loading the framework zip can take several milliseconds. Cache the crc of the framework
-      // resource APK to reduce repeated work during boot.
-      const auto target_zip = idmap2::ZipFile::Open(target_apk_path);
-      if (!target_zip) {
-        return error(base::StringPrintf("failed to open target %s", target_apk_path.c_str()));
-      }
-
-      const auto target_crc = GetPackageCrc(*target_zip);
-      if (!target_crc) {
-        return error(target_crc.GetErrorMessage());
-      }
-
-      android_crc_ = *target_crc;
+    auto target_crc_status = GetCrc(target_apk_path, &target_crc);
+    if (!target_crc_status.isOk()) {
+      *_aidl_return = false;
+      return target_crc_status;
     }
 
-    *_aidl_return = (bool) header->IsUpToDate(android_crc_.value());
+    // Loading the framework zip can take several milliseconds. Cache the crc of the framework
+    // resource APK to reduce repeated work during boot.
+    if (target_apk_path == kFrameworkPath) {
+      android_crc_ = target_crc;
+    }
   }
 
-  // TODO(b/119328308): Check that the set of fulfilled policies of the overlay has not changed
+  uint32_t overlay_crc;
+  auto overlay_crc_status = GetCrc(overlay_apk_path, &overlay_crc);
+  if (!overlay_crc_status.isOk()) {
+    *_aidl_return = false;
+    return overlay_crc_status;
+  }
+
+  auto up_to_date =
+      header->IsUpToDate(target_apk_path.c_str(), overlay_apk_path.c_str(), target_crc, overlay_crc,
+                         fulfilled_policies, enforce_overlayable);
+  if (!up_to_date) {
+    *_aidl_return = false;
+    return error(up_to_date.GetErrorMessage());
+  }
+
   return ok();
 }
 
diff --git a/cmds/idmap2/include/idmap2/Idmap.h b/cmds/idmap2/include/idmap2/Idmap.h
index 77a7b30..8f25b8d 100644
--- a/cmds/idmap2/include/idmap2/Idmap.h
+++ b/cmds/idmap2/include/idmap2/Idmap.h
@@ -117,6 +117,14 @@
     return overlay_crc_;
   }
 
+  inline uint32_t GetFulfilledPolicies() const {
+    return fulfilled_policies_;
+  }
+
+  bool GetEnforceOverlayable() const {
+    return enforce_overlayable_;
+  }
+
   inline StringPiece GetTargetPath() const {
     return StringPiece(target_path_);
   }
@@ -132,8 +140,11 @@
   // Invariant: anytime the idmap data encoding is changed, the idmap version
   // field *must* be incremented. Because of this, we know that if the idmap
   // header is up-to-date the entire file is up-to-date.
-  Result<Unit> IsUpToDate() const;
-  Result<Unit> IsUpToDate(uint32_t target_crc_) const;
+  Result<Unit> IsUpToDate(const char* target_path, const char* overlay_path,
+                          uint32_t fulfilled_policies, bool enforce_overlayable) const;
+  Result<Unit> IsUpToDate(const char* target_path, const char* overlay_path, uint32_t target_crc,
+                          uint32_t overlay_crc, uint32_t fulfilled_policies,
+                          bool enforce_overlayable) const;
 
   void accept(Visitor* v) const;
 
@@ -145,6 +156,8 @@
   uint32_t version_;
   uint32_t target_crc_;
   uint32_t overlay_crc_;
+  uint32_t fulfilled_policies_;
+  bool enforce_overlayable_;
   char target_path_[kIdmapStringLength];
   char overlay_path_[kIdmapStringLength];
   std::string debug_info_;
diff --git a/cmds/idmap2/libidmap2/BinaryStreamVisitor.cpp b/cmds/idmap2/libidmap2/BinaryStreamVisitor.cpp
index 362dcb3..255212a 100644
--- a/cmds/idmap2/libidmap2/BinaryStreamVisitor.cpp
+++ b/cmds/idmap2/libidmap2/BinaryStreamVisitor.cpp
@@ -66,6 +66,8 @@
   Write32(header.GetVersion());
   Write32(header.GetTargetCrc());
   Write32(header.GetOverlayCrc());
+  Write32(header.GetFulfilledPolicies());
+  Write8(static_cast<uint8_t>(header.GetEnforceOverlayable()));
   WriteString256(header.GetTargetPath());
   WriteString256(header.GetOverlayPath());
   WriteString(header.GetDebugInfo());
diff --git a/cmds/idmap2/libidmap2/Idmap.cpp b/cmds/idmap2/libidmap2/Idmap.cpp
index 706b842..0bea217 100644
--- a/cmds/idmap2/libidmap2/Idmap.cpp
+++ b/cmds/idmap2/libidmap2/Idmap.cpp
@@ -112,14 +112,18 @@
 
 std::unique_ptr<const IdmapHeader> IdmapHeader::FromBinaryStream(std::istream& stream) {
   std::unique_ptr<IdmapHeader> idmap_header(new IdmapHeader());
-
+  uint8_t enforce_overlayable;
   if (!Read32(stream, &idmap_header->magic_) || !Read32(stream, &idmap_header->version_) ||
       !Read32(stream, &idmap_header->target_crc_) || !Read32(stream, &idmap_header->overlay_crc_) ||
+      !Read32(stream, &idmap_header->fulfilled_policies_) ||
+      !Read8(stream, &enforce_overlayable) ||
       !ReadString256(stream, idmap_header->target_path_) ||
       !ReadString256(stream, idmap_header->overlay_path_)) {
     return nullptr;
   }
 
+  idmap_header->enforce_overlayable_ = static_cast<bool>(enforce_overlayable);
+
   auto debug_str = ReadString(stream);
   if (!debug_str) {
     return nullptr;
@@ -129,21 +133,35 @@
   return std::move(idmap_header);
 }
 
-Result<Unit> IdmapHeader::IsUpToDate() const {
-  const std::unique_ptr<const ZipFile> target_zip = ZipFile::Open(target_path_);
+Result<Unit> IdmapHeader::IsUpToDate(const char* target_path, const char* overlay_path,
+                                     uint32_t fulfilled_policies, bool enforce_overlayable) const {
+  const std::unique_ptr<const ZipFile> target_zip = ZipFile::Open(target_path);
   if (!target_zip) {
-    return Error("failed to open target %s", GetTargetPath().to_string().c_str());
+    return Error("failed to open target %s", target_path);
   }
 
-  Result<uint32_t> target_crc = GetPackageCrc(*target_zip);
+  const Result<uint32_t> target_crc = GetPackageCrc(*target_zip);
   if (!target_crc) {
     return Error("failed to get target crc");
   }
 
-  return IsUpToDate(*target_crc);
+  const std::unique_ptr<const ZipFile> overlay_zip = ZipFile::Open(overlay_path);
+  if (!overlay_zip) {
+    return Error("failed to overlay target %s", overlay_path);
+  }
+
+  const Result<uint32_t> overlay_crc = GetPackageCrc(*overlay_zip);
+  if (!overlay_crc) {
+    return Error("failed to get overlay crc");
+  }
+
+  return IsUpToDate(target_path, overlay_path, *target_crc, *overlay_crc, fulfilled_policies,
+                    enforce_overlayable);
 }
 
-Result<Unit> IdmapHeader::IsUpToDate(uint32_t target_crc) const {
+Result<Unit> IdmapHeader::IsUpToDate(const char* target_path, const char* overlay_path,
+                                     uint32_t target_crc, uint32_t overlay_crc,
+                                     uint32_t fulfilled_policies, bool enforce_overlayable) const {
   if (magic_ != kIdmapMagic) {
     return Error("bad magic: actual 0x%08x, expected 0x%08x", magic_, kIdmapMagic);
   }
@@ -157,19 +175,30 @@
                  target_crc);
   }
 
-  const std::unique_ptr<const ZipFile> overlay_zip = ZipFile::Open(overlay_path_);
-  if (!overlay_zip) {
-    return Error("failed to open overlay %s", GetOverlayPath().to_string().c_str());
-  }
-
-  Result<uint32_t> overlay_crc = GetPackageCrc(*overlay_zip);
-  if (!overlay_crc) {
-    return Error("failed to get overlay crc");
-  }
-
-  if (overlay_crc_ != *overlay_crc) {
+  if (overlay_crc_ != overlay_crc) {
     return Error("bad overlay crc: idmap version 0x%08x, file system version 0x%08x", overlay_crc_,
-                 *overlay_crc);
+                 overlay_crc);
+  }
+
+  if (fulfilled_policies_ != fulfilled_policies) {
+    return Error("bad fulfilled policies: idmap version 0x%08x, file system version 0x%08x",
+                 fulfilled_policies, fulfilled_policies_);
+  }
+
+  if (enforce_overlayable != enforce_overlayable_) {
+    return Error("bad enforce overlayable: idmap version %s, file system version %s",
+                 enforce_overlayable ? "true" : "false",
+                 enforce_overlayable_ ? "true" : "false");
+  }
+
+  if (strcmp(target_path, target_path_) != 0) {
+    return Error("bad target path: idmap version %s, file system version %s", target_path,
+                 target_path_);
+  }
+
+  if (strcmp(overlay_path, overlay_path_) != 0) {
+    return Error("bad overlay path: idmap version %s, file system version %s", overlay_path,
+                 overlay_path_);
   }
 
   return Unit{};
@@ -320,6 +349,9 @@
   }
   header->overlay_crc_ = *crc;
 
+  header->fulfilled_policies_ = fulfilled_policies;
+  header->enforce_overlayable_ = enforce_overlayable;
+
   if (target_apk_path.size() > sizeof(header->target_path_)) {
     return Error("target apk path \"%s\" longer than maximum size %zu", target_apk_path.c_str(),
                  sizeof(header->target_path_));
diff --git a/cmds/idmap2/libidmap2/RawPrintVisitor.cpp b/cmds/idmap2/libidmap2/RawPrintVisitor.cpp
index 751c60c..3f62a2a 100644
--- a/cmds/idmap2/libidmap2/RawPrintVisitor.cpp
+++ b/cmds/idmap2/libidmap2/RawPrintVisitor.cpp
@@ -23,10 +23,12 @@
 #include "android-base/macros.h"
 #include "android-base/stringprintf.h"
 #include "androidfw/ApkAssets.h"
+#include "idmap2/PolicyUtils.h"
 #include "idmap2/ResourceUtils.h"
 #include "idmap2/Result.h"
 
 using android::ApkAssets;
+using android::idmap2::policy::PoliciesToDebugString;
 
 namespace {
 
@@ -39,9 +41,6 @@
 
 namespace android::idmap2 {
 
-// verbatim copy fomr PrettyPrintVisitor.cpp, move to common utils
-#define RESID(pkg, type, entry) (((pkg) << 24) | ((type) << 16) | (entry))
-
 void RawPrintVisitor::visit(const Idmap& idmap ATTRIBUTE_UNUSED) {
 }
 
@@ -50,6 +49,9 @@
   print(header.GetVersion(), "version");
   print(header.GetTargetCrc(), "target crc");
   print(header.GetOverlayCrc(), "overlay crc");
+  print(header.GetFulfilledPolicies(), "fulfilled policies: %s",
+        PoliciesToDebugString(header.GetFulfilledPolicies()).c_str());
+  print(static_cast<uint8_t>(header.GetEnforceOverlayable()), "enforce overlayable");
   print(header.GetTargetPath().to_string(), kIdmapStringLength, "target path");
   print(header.GetOverlayPath().to_string(), kIdmapStringLength, "overlay path");
   print("...", StringSizeWhenEncoded(header.GetDebugInfo()), "debug info");
diff --git a/cmds/idmap2/libidmap2/ResourceMapping.cpp b/cmds/idmap2/libidmap2/ResourceMapping.cpp
index 44acbca..34589a1 100644
--- a/cmds/idmap2/libidmap2/ResourceMapping.cpp
+++ b/cmds/idmap2/libidmap2/ResourceMapping.cpp
@@ -61,8 +61,7 @@
                               const ResourceId& target_resource) {
   static constexpr const PolicyBitmask sDefaultPolicies =
       PolicyFlags::ODM_PARTITION | PolicyFlags::OEM_PARTITION | PolicyFlags::SYSTEM_PARTITION |
-      PolicyFlags::VENDOR_PARTITION | PolicyFlags::PRODUCT_PARTITION | PolicyFlags::SIGNATURE |
-      PolicyFlags::ACTOR_SIGNATURE;
+      PolicyFlags::VENDOR_PARTITION | PolicyFlags::PRODUCT_PARTITION | PolicyFlags::SIGNATURE;
 
   // If the resource does not have an overlayable definition, allow the resource to be overlaid if
   // the overlay is preinstalled or signed with the same signature as the target.
@@ -292,13 +291,6 @@
                                                        const PolicyBitmask& fulfilled_policies,
                                                        bool enforce_overlayable,
                                                        LogInfo& log_info) {
-  if (enforce_overlayable) {
-    log_info.Info(LogMessage() << "fulfilled_policies="
-                               << ConcatPolicies(BitmaskToPolicies(fulfilled_policies))
-                               << " enforce_overlayable="
-                               << (enforce_overlayable ? "true" : "false"));
-  }
-
   AssetManager2 target_asset_manager;
   if (!target_asset_manager.SetApkAssets({&target_apk_assets}, true /* invalidate_caches */,
                                          false /* filter_incompatible_configs*/)) {
diff --git a/cmds/idmap2/libidmap2_policies/include/idmap2/Policies.h b/cmds/idmap2/libidmap2_policies/include/idmap2/Policies.h
index 4973b76..5bd353a 100644
--- a/cmds/idmap2/libidmap2_policies/include/idmap2/Policies.h
+++ b/cmds/idmap2/libidmap2_policies/include/idmap2/Policies.h
@@ -21,9 +21,12 @@
 #include <string>
 #include <vector>
 
+#include "android-base/stringprintf.h"
 #include "androidfw/ResourceTypes.h"
 #include "androidfw/StringPiece.h"
 
+using android::base::StringPrintf;
+
 using PolicyBitmask = android::ResTable_overlayable_policy_header::PolicyBitmask;
 using PolicyFlags = android::ResTable_overlayable_policy_header::PolicyFlags;
 
@@ -48,6 +51,29 @@
     {kPolicySystem, PolicyFlags::SYSTEM_PARTITION},
     {kPolicyVendor, PolicyFlags::VENDOR_PARTITION},
 };
+
+inline static std::string PoliciesToDebugString(PolicyBitmask policies) {
+  std::string str;
+  uint32_t remaining = policies;
+  for (auto const& policy : kPolicyStringToFlag) {
+    if ((policies & policy.second) != policy.second) {
+      continue;
+    }
+    if (!str.empty()) {
+      str.append("|");
+    }
+    str.append(policy.first.data());
+    remaining &= ~policy.second;
+  }
+  if (remaining != 0) {
+    if (!str.empty()) {
+      str.append("|");
+    }
+    str.append(StringPrintf("0x%08x", remaining));
+  }
+  return !str.empty() ? str : "none";
+}
+
 }  // namespace android::idmap2::policy
 
 #endif  // IDMAP2_INCLUDE_IDMAP2_POLICIES_H_
diff --git a/cmds/idmap2/tests/BinaryStreamVisitorTests.cpp b/cmds/idmap2/tests/BinaryStreamVisitorTests.cpp
index db4778c..5fea7bc 100644
--- a/cmds/idmap2/tests/BinaryStreamVisitorTests.cpp
+++ b/cmds/idmap2/tests/BinaryStreamVisitorTests.cpp
@@ -48,6 +48,11 @@
   ASSERT_TRUE(result2);
   const auto idmap2 = std::move(*result2);
 
+  ASSERT_EQ(idmap1->GetHeader()->GetFulfilledPolicies(),
+            idmap2->GetHeader()->GetFulfilledPolicies());
+  ASSERT_EQ(idmap1->GetHeader()->GetEnforceOverlayable(),
+            idmap2->GetHeader()->GetEnforceOverlayable());
+  ASSERT_EQ(idmap1->GetHeader()->GetTargetPath(), idmap2->GetHeader()->GetTargetPath());
   ASSERT_EQ(idmap1->GetHeader()->GetTargetCrc(), idmap2->GetHeader()->GetTargetCrc());
   ASSERT_EQ(idmap1->GetHeader()->GetTargetPath(), idmap2->GetHeader()->GetTargetPath());
   ASSERT_EQ(idmap1->GetData().size(), 1U);
diff --git a/cmds/idmap2/tests/IdmapTests.cpp b/cmds/idmap2/tests/IdmapTests.cpp
index 87da36c..6fab5e0 100644
--- a/cmds/idmap2/tests/IdmapTests.cpp
+++ b/cmds/idmap2/tests/IdmapTests.cpp
@@ -62,9 +62,11 @@
   std::unique_ptr<const IdmapHeader> header = IdmapHeader::FromBinaryStream(stream);
   ASSERT_THAT(header, NotNull());
   ASSERT_EQ(header->GetMagic(), 0x504d4449U);
-  ASSERT_EQ(header->GetVersion(), 0x03U);
+  ASSERT_EQ(header->GetVersion(), 0x04U);
   ASSERT_EQ(header->GetTargetCrc(), 0x1234U);
   ASSERT_EQ(header->GetOverlayCrc(), 0x5678U);
+  ASSERT_EQ(header->GetFulfilledPolicies(), 0x11);
+  ASSERT_EQ(header->GetEnforceOverlayable(), true);
   ASSERT_EQ(header->GetTargetPath().to_string(), "targetX.apk");
   ASSERT_EQ(header->GetOverlayPath().to_string(), "overlayX.apk");
   ASSERT_EQ(header->GetDebugInfo(), "debug");
@@ -73,7 +75,7 @@
 TEST(IdmapTests, FailToCreateIdmapHeaderFromBinaryStreamIfPathTooLong) {
   std::string raw(reinterpret_cast<const char*>(idmap_raw_data), idmap_raw_data_len);
   // overwrite the target path string, including the terminating null, with '.'
-  for (size_t i = 0x10; i < 0x110; i++) {
+  for (size_t i = 0x15; i < 0x115; i++) {
     raw[i] = '.';
   }
   std::istringstream stream(raw);
@@ -82,7 +84,7 @@
 }
 
 TEST(IdmapTests, CreateIdmapDataHeaderFromBinaryStream) {
-  const size_t offset = 0x21c;
+  const size_t offset = 0x221;
   std::string raw(reinterpret_cast<const char*>(idmap_raw_data + offset),
                   idmap_raw_data_len - offset);
   std::istringstream stream(raw);
@@ -94,7 +96,7 @@
 }
 
 TEST(IdmapTests, CreateIdmapDataFromBinaryStream) {
-  const size_t offset = 0x21c;
+  const size_t offset = 0x221;
   std::string raw(reinterpret_cast<const char*>(idmap_raw_data + offset),
                   idmap_raw_data_len - offset);
   std::istringstream stream(raw);
@@ -128,9 +130,11 @@
 
   ASSERT_THAT(idmap->GetHeader(), NotNull());
   ASSERT_EQ(idmap->GetHeader()->GetMagic(), 0x504d4449U);
-  ASSERT_EQ(idmap->GetHeader()->GetVersion(), 0x03U);
+  ASSERT_EQ(idmap->GetHeader()->GetVersion(), 0x04U);
   ASSERT_EQ(idmap->GetHeader()->GetTargetCrc(), 0x1234U);
   ASSERT_EQ(idmap->GetHeader()->GetOverlayCrc(), 0x5678U);
+  ASSERT_EQ(idmap->GetHeader()->GetFulfilledPolicies(), 0x11);
+  ASSERT_EQ(idmap->GetHeader()->GetEnforceOverlayable(), true);
   ASSERT_EQ(idmap->GetHeader()->GetTargetPath().to_string(), "targetX.apk");
   ASSERT_EQ(idmap->GetHeader()->GetOverlayPath().to_string(), "overlayX.apk");
 
@@ -180,9 +184,11 @@
 
   ASSERT_THAT(idmap->GetHeader(), NotNull());
   ASSERT_EQ(idmap->GetHeader()->GetMagic(), 0x504d4449U);
-  ASSERT_EQ(idmap->GetHeader()->GetVersion(), 0x03U);
+  ASSERT_EQ(idmap->GetHeader()->GetVersion(), 0x04U);
   ASSERT_EQ(idmap->GetHeader()->GetTargetCrc(), android::idmap2::TestConstants::TARGET_CRC);
   ASSERT_EQ(idmap->GetHeader()->GetOverlayCrc(), android::idmap2::TestConstants::OVERLAY_CRC);
+  ASSERT_EQ(idmap->GetHeader()->GetFulfilledPolicies(), PolicyFlags::PUBLIC);
+  ASSERT_EQ(idmap->GetHeader()->GetEnforceOverlayable(), true);
   ASSERT_EQ(idmap->GetHeader()->GetTargetPath().to_string(), target_apk_path);
   ASSERT_EQ(idmap->GetHeader()->GetOverlayPath(), overlay_apk_path);
 }
@@ -389,7 +395,8 @@
 
   std::unique_ptr<const IdmapHeader> header = IdmapHeader::FromBinaryStream(stream);
   ASSERT_THAT(header, NotNull());
-  ASSERT_TRUE(header->IsUpToDate());
+  ASSERT_TRUE(header->IsUpToDate(target_apk_path.c_str(), overlay_apk_path.c_str(),
+                                 PolicyFlags::PUBLIC, /* enforce_overlayable */ true));
 
   // magic: bytes (0x0, 0x03)
   std::string bad_magic_string(stream.str());
@@ -402,7 +409,8 @@
       IdmapHeader::FromBinaryStream(bad_magic_stream);
   ASSERT_THAT(bad_magic_header, NotNull());
   ASSERT_NE(header->GetMagic(), bad_magic_header->GetMagic());
-  ASSERT_FALSE(bad_magic_header->IsUpToDate());
+  ASSERT_FALSE(bad_magic_header->IsUpToDate(target_apk_path.c_str(), overlay_apk_path.c_str(),
+                                            PolicyFlags::PUBLIC, /* enforce_overlayable */ true));
 
   // version: bytes (0x4, 0x07)
   std::string bad_version_string(stream.str());
@@ -415,7 +423,8 @@
       IdmapHeader::FromBinaryStream(bad_version_stream);
   ASSERT_THAT(bad_version_header, NotNull());
   ASSERT_NE(header->GetVersion(), bad_version_header->GetVersion());
-  ASSERT_FALSE(bad_version_header->IsUpToDate());
+  ASSERT_FALSE(bad_magic_header->IsUpToDate(target_apk_path.c_str(), overlay_apk_path.c_str(),
+                                            PolicyFlags::PUBLIC, /* enforce_overlayable */ true));
 
   // target crc: bytes (0x8, 0xb)
   std::string bad_target_crc_string(stream.str());
@@ -428,7 +437,8 @@
       IdmapHeader::FromBinaryStream(bad_target_crc_stream);
   ASSERT_THAT(bad_target_crc_header, NotNull());
   ASSERT_NE(header->GetTargetCrc(), bad_target_crc_header->GetTargetCrc());
-  ASSERT_FALSE(bad_target_crc_header->IsUpToDate());
+  ASSERT_FALSE(bad_magic_header->IsUpToDate(target_apk_path.c_str(), overlay_apk_path.c_str(),
+                                            PolicyFlags::PUBLIC, /* enforce_overlayable */ true));
 
   // overlay crc: bytes (0xc, 0xf)
   std::string bad_overlay_crc_string(stream.str());
@@ -441,27 +451,55 @@
       IdmapHeader::FromBinaryStream(bad_overlay_crc_stream);
   ASSERT_THAT(bad_overlay_crc_header, NotNull());
   ASSERT_NE(header->GetOverlayCrc(), bad_overlay_crc_header->GetOverlayCrc());
-  ASSERT_FALSE(bad_overlay_crc_header->IsUpToDate());
+  ASSERT_FALSE(bad_magic_header->IsUpToDate(target_apk_path.c_str(), overlay_apk_path.c_str(),
+                                            PolicyFlags::PUBLIC, /* enforce_overlayable */ true));
 
-  // target path: bytes (0x10, 0x10f)
+  // fulfilled policy: bytes (0x10, 0x13)
+  std::string bad_policy_string(stream.str());
+  bad_policy_string[0x10] = '.';
+  bad_policy_string[0x11] = '.';
+  bad_policy_string[0x12] = '.';
+  bad_policy_string[0x13] = '.';
+  std::stringstream bad_policy_stream(bad_policy_string);
+  std::unique_ptr<const IdmapHeader> bad_policy_header =
+      IdmapHeader::FromBinaryStream(bad_policy_stream);
+  ASSERT_THAT(bad_policy_header, NotNull());
+  ASSERT_NE(header->GetFulfilledPolicies(), bad_policy_header->GetFulfilledPolicies());
+  ASSERT_FALSE(bad_policy_header->IsUpToDate(target_apk_path.c_str(), overlay_apk_path.c_str(),
+                                             PolicyFlags::PUBLIC, /* enforce_overlayable */ true));
+
+  // enforce overlayable: bytes (0x14)
+  std::string bad_enforce_string(stream.str());
+  bad_enforce_string[0x14] = '\0';
+  std::stringstream bad_enforce_stream(bad_enforce_string);
+  std::unique_ptr<const IdmapHeader> bad_enforce_header =
+      IdmapHeader::FromBinaryStream(bad_enforce_stream);
+  ASSERT_THAT(bad_enforce_header, NotNull());
+  ASSERT_NE(header->GetEnforceOverlayable(), bad_enforce_header->GetEnforceOverlayable());
+  ASSERT_FALSE(bad_enforce_header->IsUpToDate(target_apk_path.c_str(), overlay_apk_path.c_str(),
+                                              PolicyFlags::PUBLIC, /* enforce_overlayable */ true));
+
+  // target path: bytes (0x15, 0x114)
   std::string bad_target_path_string(stream.str());
-  bad_target_path_string[0x10] = '\0';
+  bad_target_path_string[0x15] = '\0';
   std::stringstream bad_target_path_stream(bad_target_path_string);
   std::unique_ptr<const IdmapHeader> bad_target_path_header =
       IdmapHeader::FromBinaryStream(bad_target_path_stream);
   ASSERT_THAT(bad_target_path_header, NotNull());
   ASSERT_NE(header->GetTargetPath(), bad_target_path_header->GetTargetPath());
-  ASSERT_FALSE(bad_target_path_header->IsUpToDate());
+  ASSERT_FALSE(bad_magic_header->IsUpToDate(target_apk_path.c_str(), overlay_apk_path.c_str(),
+                                            PolicyFlags::PUBLIC, /* enforce_overlayable */ true));
 
-  // overlay path: bytes (0x110, 0x20f)
+  // overlay path: bytes (0x115, 0x214)
   std::string bad_overlay_path_string(stream.str());
-  bad_overlay_path_string[0x110] = '\0';
+  bad_overlay_path_string[0x115] = '\0';
   std::stringstream bad_overlay_path_stream(bad_overlay_path_string);
   std::unique_ptr<const IdmapHeader> bad_overlay_path_header =
       IdmapHeader::FromBinaryStream(bad_overlay_path_stream);
   ASSERT_THAT(bad_overlay_path_header, NotNull());
   ASSERT_NE(header->GetOverlayPath(), bad_overlay_path_header->GetOverlayPath());
-  ASSERT_FALSE(bad_overlay_path_header->IsUpToDate());
+  ASSERT_FALSE(bad_magic_header->IsUpToDate(target_apk_path.c_str(), overlay_apk_path.c_str(),
+                                            PolicyFlags::PUBLIC, /* enforce_overlayable */ true));
 }
 
 class TestVisitor : public Visitor {
diff --git a/cmds/idmap2/tests/RawPrintVisitorTests.cpp b/cmds/idmap2/tests/RawPrintVisitorTests.cpp
index 5c5c81e..b268d5a 100644
--- a/cmds/idmap2/tests/RawPrintVisitorTests.cpp
+++ b/cmds/idmap2/tests/RawPrintVisitorTests.cpp
@@ -43,6 +43,8 @@
         << str << "--------";                                     \
   } while (0)
 
+#define ADDRESS "[0-9a-f]{8}: "
+
 TEST(RawPrintVisitorTests, CreateRawPrintVisitor) {
   fclose(stderr);  // silence expected warnings
 
@@ -62,15 +64,16 @@
   RawPrintVisitor visitor(stream);
   (*idmap)->accept(&visitor);
 
-#define ADDRESS "[0-9a-f]{8}: "
   ASSERT_CONTAINS_REGEX(ADDRESS "504d4449  magic\n", stream.str());
-  ASSERT_CONTAINS_REGEX(ADDRESS "00000003  version\n", stream.str());
+  ASSERT_CONTAINS_REGEX(ADDRESS "00000004  version\n", stream.str());
   ASSERT_CONTAINS_REGEX(
       StringPrintf(ADDRESS "%s  target crc\n", android::idmap2::TestConstants::TARGET_CRC_STRING),
       stream.str());
   ASSERT_CONTAINS_REGEX(
       StringPrintf(ADDRESS "%s  overlay crc\n", android::idmap2::TestConstants::OVERLAY_CRC_STRING),
       stream.str());
+  ASSERT_CONTAINS_REGEX(ADDRESS "00000001  fulfilled policies: public\n", stream.str());
+  ASSERT_CONTAINS_REGEX(ADDRESS "      01  enforce overlayable\n", stream.str());
   ASSERT_CONTAINS_REGEX(ADDRESS "      7f  target package id\n", stream.str());
   ASSERT_CONTAINS_REGEX(ADDRESS "      7f  overlay package id\n", stream.str());
   ASSERT_CONTAINS_REGEX(ADDRESS "00000004  target entry count\n", stream.str());
@@ -83,7 +86,6 @@
   ASSERT_CONTAINS_REGEX(ADDRESS "7f010000  value: integer/int1\n", stream.str());
   ASSERT_CONTAINS_REGEX(ADDRESS "7f010000  overlay id: integer/int1\n", stream.str());
   ASSERT_CONTAINS_REGEX(ADDRESS "7f010000  target id: integer/int1\n", stream.str());
-#undef ADDRESS
 }
 
 TEST(RawPrintVisitorTests, CreateRawPrintVisitorWithoutAccessToApks) {
@@ -99,22 +101,23 @@
   RawPrintVisitor visitor(stream);
   (*idmap)->accept(&visitor);
 
-  ASSERT_NE(stream.str().find("00000000: 504d4449  magic\n"), std::string::npos);
-  ASSERT_NE(stream.str().find("00000004: 00000003  version\n"), std::string::npos);
-  ASSERT_NE(stream.str().find("00000008: 00001234  target crc\n"), std::string::npos);
-  ASSERT_NE(stream.str().find("0000000c: 00005678  overlay crc\n"), std::string::npos);
-  ASSERT_NE(stream.str().find("0000021c:       7f  target package id\n"), std::string::npos);
-  ASSERT_NE(stream.str().find("0000021d:       7f  overlay package id\n"), std::string::npos);
-  ASSERT_NE(stream.str().find("0000021e: 00000003  target entry count\n"), std::string::npos);
-  ASSERT_NE(stream.str().find("00000222: 00000003  overlay entry count\n"), std::string::npos);
-  ASSERT_NE(stream.str().find("00000226: 00000000  string pool index offset\n"), std::string::npos);
-  ASSERT_NE(stream.str().find("0000022a: 00000000  string pool byte length\n"), std::string::npos);
-  ASSERT_NE(stream.str().find("0000022e: 7f020000  target id\n"), std::string::npos);
-  ASSERT_NE(stream.str().find("00000232:       01  type: reference\n"), std::string::npos);
-  ASSERT_NE(stream.str().find("00000233: 7f020000  value\n"), std::string::npos);
-
-  ASSERT_NE(stream.str().find("00000249: 7f020000  overlay id\n"), std::string::npos);
-  ASSERT_NE(stream.str().find("0000024d: 7f020000  target id\n"), std::string::npos);
+  ASSERT_CONTAINS_REGEX(ADDRESS "504d4449  magic\n", stream.str());
+  ASSERT_CONTAINS_REGEX(ADDRESS "00000004  version\n", stream.str());
+  ASSERT_CONTAINS_REGEX(ADDRESS "00001234  target crc\n", stream.str());
+  ASSERT_CONTAINS_REGEX(ADDRESS "00005678  overlay crc\n", stream.str());
+  ASSERT_CONTAINS_REGEX(ADDRESS "00000011  fulfilled policies: public|signature\n", stream.str());
+  ASSERT_CONTAINS_REGEX(ADDRESS "      01  enforce overlayable\n", stream.str());
+  ASSERT_CONTAINS_REGEX(ADDRESS "      7f  target package id\n", stream.str());
+  ASSERT_CONTAINS_REGEX(ADDRESS "      7f  overlay package id\n", stream.str());
+  ASSERT_CONTAINS_REGEX(ADDRESS "00000003  target entry count\n", stream.str());
+  ASSERT_CONTAINS_REGEX(ADDRESS "00000003  overlay entry count\n", stream.str());
+  ASSERT_CONTAINS_REGEX(ADDRESS "00000000  string pool index offset\n", stream.str());
+  ASSERT_CONTAINS_REGEX(ADDRESS "00000000  string pool byte length\n", stream.str());
+  ASSERT_CONTAINS_REGEX(ADDRESS "7f020000  target id\n", stream.str());
+  ASSERT_CONTAINS_REGEX(ADDRESS "      01  type: reference\n", stream.str());
+  ASSERT_CONTAINS_REGEX(ADDRESS "7f020000  value\n", stream.str());
+  ASSERT_CONTAINS_REGEX(ADDRESS "7f020000  overlay id\n", stream.str());
+  ASSERT_CONTAINS_REGEX(ADDRESS "7f020000  target id\n", stream.str());
 }
 
 }  // namespace android::idmap2
diff --git a/cmds/idmap2/tests/ResourceMappingTests.cpp b/cmds/idmap2/tests/ResourceMappingTests.cpp
index 5754eaf..de039f4 100644
--- a/cmds/idmap2/tests/ResourceMappingTests.cpp
+++ b/cmds/idmap2/tests/ResourceMappingTests.cpp
@@ -287,26 +287,66 @@
                               R::overlay::string::str4, false /* rewrite */));
 }
 
-
-// Overlays that are pre-installed or are signed with the same signature as the target/actor can
+// Overlays that are neither pre-installed nor signed with the same signature as the target cannot
 // overlay packages that have not defined overlayable resources.
-TEST(ResourceMappingTests, ResourcesFromApkAssetsDefaultPolicies) {
-  constexpr PolicyBitmask kDefaultPolicies =
-      PolicyFlags::SIGNATURE | PolicyFlags::ACTOR_SIGNATURE | PolicyFlags::PRODUCT_PARTITION |
-      PolicyFlags::SYSTEM_PARTITION | PolicyFlags::VENDOR_PARTITION | PolicyFlags::ODM_PARTITION |
-      PolicyFlags::OEM_PARTITION;
+TEST(ResourceMappingTests, ResourcesFromApkAssetsDefaultPoliciesPublicFail) {
+  auto resources = TestGetResourceMapping("/target/target-no-overlayable.apk",
+                                          "/overlay/overlay-no-name.apk", PolicyFlags::PUBLIC,
+                                          /* enforce_overlayable */ true);
 
-  for (PolicyBitmask policy = 1U << (sizeof(PolicyBitmask) * 8 - 1); policy > 0;
-       policy = policy >> 1U) {
+  ASSERT_TRUE(resources) << resources.GetErrorMessage();
+  ASSERT_EQ(resources->GetTargetToOverlayMap().size(), 0U);
+}
+
+// Overlays that are pre-installed or are signed with the same signature as the target can overlay
+// packages that have not defined overlayable resources.
+TEST(ResourceMappingTests, ResourcesFromApkAssetsDefaultPolicies) {
+  auto CheckEntries = [&](const PolicyBitmask& fulfilled_policies) -> void {
     auto resources = TestGetResourceMapping("/target/target-no-overlayable.apk",
                                             "/system-overlay-invalid/system-overlay-invalid.apk",
-                                            policy, /* enforce_overlayable */ true);
-    ASSERT_TRUE(resources) << resources.GetErrorMessage();
+                                            fulfilled_policies,
+                                            /* enforce_overlayable */ true);
 
-    const size_t expected_overlaid = (policy & kDefaultPolicies) != 0 ? 10U : 0U;
-    ASSERT_EQ(expected_overlaid, resources->GetTargetToOverlayMap().size())
-        << "Incorrect number of resources overlaid through policy " << policy;
-  }
+    ASSERT_TRUE(resources) << resources.GetErrorMessage();
+    auto& res = *resources;
+    ASSERT_EQ(resources->GetTargetToOverlayMap().size(), 10U);
+    ASSERT_RESULT(MappingExists(res, R::target::string::not_overlayable, Res_value::TYPE_REFERENCE,
+                                R::system_overlay_invalid::string::not_overlayable,
+                                false /* rewrite */));
+    ASSERT_RESULT(MappingExists(res, R::target::string::other, Res_value::TYPE_REFERENCE,
+                                R::system_overlay_invalid::string::other, false /* rewrite */));
+    ASSERT_RESULT(MappingExists(res, R::target::string::policy_actor, Res_value::TYPE_REFERENCE,
+                                R::system_overlay_invalid::string::policy_actor,
+                                false /* rewrite */));
+    ASSERT_RESULT(MappingExists(res, R::target::string::policy_odm, Res_value::TYPE_REFERENCE,
+                                R::system_overlay_invalid::string::policy_odm,
+                                false /* rewrite */));
+    ASSERT_RESULT(MappingExists(res, R::target::string::policy_oem, Res_value::TYPE_REFERENCE,
+                                R::system_overlay_invalid::string::policy_oem,
+                                false /* rewrite */));
+    ASSERT_RESULT(MappingExists(res, R::target::string::policy_product, Res_value::TYPE_REFERENCE,
+                                R::system_overlay_invalid::string::policy_product,
+                                false /* rewrite */));
+    ASSERT_RESULT(MappingExists(res, R::target::string::policy_public, Res_value::TYPE_REFERENCE,
+                                R::system_overlay_invalid::string::policy_public,
+                                false /* rewrite */));
+    ASSERT_RESULT(MappingExists(res, R::target::string::policy_signature, Res_value::TYPE_REFERENCE,
+                                R::system_overlay_invalid::string::policy_signature,
+                                false /* rewrite */));
+    ASSERT_RESULT(MappingExists(res, R::target::string::policy_system, Res_value::TYPE_REFERENCE,
+                                R::system_overlay_invalid::string::policy_system,
+                                false /* rewrite */));
+    ASSERT_RESULT(MappingExists(
+        res, R::target::string::policy_system_vendor, Res_value::TYPE_REFERENCE,
+        R::system_overlay_invalid::string::policy_system_vendor, false /* rewrite */));
+  };
+
+  CheckEntries(PolicyFlags::SIGNATURE);
+  CheckEntries(PolicyFlags::PRODUCT_PARTITION);
+  CheckEntries(PolicyFlags::SYSTEM_PARTITION);
+  CheckEntries(PolicyFlags::VENDOR_PARTITION);
+  CheckEntries(PolicyFlags::ODM_PARTITION);
+  CheckEntries(PolicyFlags::OEM_PARTITION);
 }
 
 }  // namespace android::idmap2
diff --git a/cmds/idmap2/tests/TestHelpers.h b/cmds/idmap2/tests/TestHelpers.h
index e899589..b599dcb 100644
--- a/cmds/idmap2/tests/TestHelpers.h
+++ b/cmds/idmap2/tests/TestHelpers.h
@@ -30,7 +30,7 @@
     0x49, 0x44, 0x4d, 0x50,
 
     // 0x4: version
-    0x03, 0x00, 0x00, 0x00,
+    0x04, 0x00, 0x00, 0x00,
 
     // 0x8: target crc
     0x34, 0x12, 0x00, 0x00,
@@ -38,7 +38,13 @@
     // 0xc: overlay crc
     0x78, 0x56, 0x00, 0x00,
 
-    // 0x10: target path "targetX.apk"
+    // 0x10: fulfilled policies
+    0x11, 0x00, 0x00, 0x00,
+
+    // 0x14: enforce overlayable
+    0x01,
+
+    // 0x15: target path "targetX.apk"
     0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x58, 0x2e, 0x61, 0x70, 0x6b, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
@@ -56,7 +62,7 @@
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 
-    // 0x110: overlay path "overlayX.apk"
+    // 0x115: overlay path "overlayX.apk"
     0x6f, 0x76, 0x65, 0x72, 0x6c, 0x61, 0x79, 0x58, 0x2e, 0x61, 0x70, 0x6b, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
@@ -74,7 +80,7 @@
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 
-    // 0x210: debug string
+    // 0x215: debug string
     // string length, including terminating null
     0x08, 0x00, 0x00, 0x00,
 
@@ -82,63 +88,63 @@
     0x64, 0x65, 0x62, 0x75, 0x67, 0x00, 0x00, 0x00,
 
     // DATA HEADER
-    // 0x21c: target_package_id
+    // 0x221: target_package_id
     0x7f,
 
-    // 0x21d: overlay_package_id
+    // 0x222: overlay_package_id
     0x7f,
 
-    // 0x21e: target_entry_count
+    // 0x223: target_entry_count
     0x03, 0x00, 0x00, 0x00,
 
-    // 0x222: overlay_entry_count
+    // 0x227: overlay_entry_count
     0x03, 0x00, 0x00, 0x00,
 
-    // 0x226: string_pool_offset
+    // 0x22b: string_pool_offset
     0x00, 0x00, 0x00, 0x00,
 
-    // 0x22a: string_pool_byte_length
+    // 0x22f: string_pool_byte_length
     0x00, 0x00, 0x00, 0x00,
 
     // TARGET ENTRIES
-    // 0x22e: 0x7f020000
-    0x00, 0x00, 0x02, 0x7f,
-
-    // 0x232: TYPE_REFERENCE
-    0x01,
-
     // 0x233: 0x7f020000
     0x00, 0x00, 0x02, 0x7f,
 
-    // 0x237: 0x7f030000
-    0x00, 0x00, 0x03, 0x7f,
-
-    // 0x23b: TYPE_REFERENCE
+    // 0x237: TYPE_REFERENCE
     0x01,
 
+    // 0x238: 0x7f020000
+    0x00, 0x00, 0x02, 0x7f,
+
     // 0x23c: 0x7f030000
     0x00, 0x00, 0x03, 0x7f,
 
-    // 0x240: 0x7f030002
-    0x02, 0x00, 0x03, 0x7f,
-
-    // 0x244: TYPE_REFERENCE
+    // 0x240: TYPE_REFERENCE
     0x01,
 
-    // 0x245: 0x7f030001
+    // 0x241: 0x7f030000
+    0x00, 0x00, 0x03, 0x7f,
+
+    // 0x245: 0x7f030002
+    0x02, 0x00, 0x03, 0x7f,
+
+    // 0x249: TYPE_REFERENCE
+    0x01,
+
+    // 0x24a: 0x7f030001
     0x01, 0x00, 0x03, 0x7f,
 
     // OVERLAY ENTRIES
-    // 0x249: 0x7f020000 -> 0x7f020000
+    // 0x24e: 0x7f020000 -> 0x7f020000
     0x00, 0x00, 0x02, 0x7f, 0x00, 0x00, 0x02, 0x7f,
 
-    // 0x251: 0x7f030000 -> 0x7f030000
+    // 0x256: 0x7f030000 -> 0x7f030000
     0x00, 0x00, 0x03, 0x7f, 0x00, 0x00, 0x03, 0x7f,
 
-    // 0x259: 0x7f030001 -> 0x7f030002
+    // 0x25e: 0x7f030001 -> 0x7f030002
     0x01, 0x00, 0x03, 0x7f, 0x02, 0x00, 0x03, 0x7f};
 
-const unsigned int idmap_raw_data_len = 0x261;
+const unsigned int idmap_raw_data_len = 0x266;
 
 std::string GetTestDataPath();
 
diff --git a/cmds/incidentd/src/Section.cpp b/cmds/incidentd/src/Section.cpp
index 61e5eb0..33e7649 100644
--- a/cmds/incidentd/src/Section.cpp
+++ b/cmds/incidentd/src/Section.cpp
@@ -477,14 +477,15 @@
 
     // Run dumping thread
     const uint64_t start = Nanotime();
-    std::thread worker([&]() {
+    std::thread worker([write_fd = std::move(dumpPipe.writeFd()), service = std::move(service),
+                        this]() mutable {
         // Don't crash the service if writing to a closed pipe (may happen if dumping times out)
         signal(SIGPIPE, sigpipe_handler);
-        status_t err = service->dump(dumpPipe.writeFd().get(), mArgs);
+        status_t err = service->dump(write_fd.get(), this->mArgs);
         if (err != OK) {
             ALOGW("[%s] dump thread failed. Error: %s", this->name.string(), strerror(-err));
         }
-        dumpPipe.writeFd().reset();
+        write_fd.reset();
     });
 
     // Collect dump content
diff --git a/cmds/statsd/src/atoms.proto b/cmds/statsd/src/atoms.proto
index 9abae52..81d059e 100644
--- a/cmds/statsd/src/atoms.proto
+++ b/cmds/statsd/src/atoms.proto
@@ -446,6 +446,9 @@
             277 [(module) = "settings"];
         CellBroadcastMessageFiltered cb_message_filtered =
             278 [(module) = "cellbroadcast"];
+        TvTunerDvrStatus tv_tuner_dvr_status = 279 [(module) = "framework"];
+        TvCasSessionOpenStatus tv_cas_session_open_status =
+            280 [(module) = "framework"];
 
         // StatsdStats tracks platform atoms with ids upto 500.
         // Update StatsdStats::kMaxPushedAtomId when atom ids here approach that value.
@@ -9242,6 +9245,58 @@
     //  new state
     optional State state = 2;
 }
+
+/**
+ * Logs the status of a dvr playback or record.
+ * This is atom ID 279.
+ *
+ * Logged from:
+ *   frameworks/base/media/java/android/media/tv/tuner/dvr
+ */
+message TvTunerDvrStatus {
+    enum Type {
+        UNKNOWN_TYPE = 0;
+        PLAYBACK = 1; // is a playback
+        RECORD = 2; // is a record
+    }
+    enum State {
+        UNKNOWN_STATE = 0;
+        STARTED = 1; // DVR is started
+        STOPPED = 2; // DVR is stopped
+    }
+    // The uid of the application that sent this custom atom.
+    optional int32 uid = 1 [(is_uid) = true];
+    // DVR type
+    optional Type type = 2;
+    //  DVR state
+    optional State state = 3;
+    //  Identify the segment of a record or playback
+    optional int32 segment_id = 4;
+    // indicate how many overflow or underflow happened between started to stopped
+    optional int32 overflow_underflow_count = 5;
+}
+
+/**
+ * Logs when a cas session opened through MediaCas.
+ * This is atom ID 280.
+ *
+ * Logged from:
+ *   frameworks/base/media/java/android/media/MediaCas.java
+ */
+message TvCasSessionOpenStatus {
+    enum State {
+        UNKNOWN = 0;
+        SUCCEEDED = 1; // indicate that the session is opened successfully.
+        FAILED = 2; // indicate that the session isn’t opened successfully.
+    }
+    // The uid of the application that sent this custom atom.
+    optional int32 uid = 1 [(is_uid) = true];
+    //  Cas system Id
+    optional int32 cas_system_id = 2;
+    // State of the session
+    optional State state = 3;
+}
+
 /**
  * Logs when an app is frozen or unfrozen.
  *
diff --git a/cmds/statsd/src/metrics/ValueMetricProducer.h b/cmds/statsd/src/metrics/ValueMetricProducer.h
index 3de5b99..505b239 100644
--- a/cmds/statsd/src/metrics/ValueMetricProducer.h
+++ b/cmds/statsd/src/metrics/ValueMetricProducer.h
@@ -75,7 +75,7 @@
         if (!mSplitBucketForAppUpgrade) {
             return;
         }
-        if (mIsPulled && mCondition) {
+        if (mIsPulled && mCondition == ConditionState::kTrue) {
             pullAndMatchEventsLocked(eventTimeNs);
         }
         flushCurrentBucketLocked(eventTimeNs, eventTimeNs);
@@ -84,7 +84,7 @@
     // ValueMetric needs special logic if it's a pulled atom.
     void onStatsdInitCompleted(const int64_t& eventTimeNs) override {
         std::lock_guard<std::mutex> lock(mMutex);
-        if (mIsPulled && mCondition) {
+        if (mIsPulled && mCondition == ConditionState::kTrue) {
             pullAndMatchEventsLocked(eventTimeNs);
         }
         flushCurrentBucketLocked(eventTimeNs, eventTimeNs);
diff --git a/cmds/statsd/tests/metrics/ValueMetricProducer_test.cpp b/cmds/statsd/tests/metrics/ValueMetricProducer_test.cpp
index 1bcc35d..58a3259 100644
--- a/cmds/statsd/tests/metrics/ValueMetricProducer_test.cpp
+++ b/cmds/statsd/tests/metrics/ValueMetricProducer_test.cpp
@@ -4722,6 +4722,46 @@
     EXPECT_EQ(5, report.value_metrics().data(4).bucket_info(1).values(0).value_long());
 }
 
+/*
+ * Test bucket splits when condition is unknown.
+ */
+TEST(ValueMetricProducerTest, TestForcedBucketSplitWhenConditionUnknownSkipsBucket) {
+    ValueMetric metric = ValueMetricProducerTestHelper::createMetricWithCondition();
+
+    sp<MockStatsPullerManager> pullerManager = new StrictMock<MockStatsPullerManager>();
+
+    sp<ValueMetricProducer> valueProducer =
+            ValueMetricProducerTestHelper::createValueProducerWithCondition(
+                    pullerManager, metric,
+                    ConditionState::kUnknown);
+
+    // App update event.
+    int64_t appUpdateTimeNs = bucketStartTimeNs + 1000;
+    valueProducer->notifyAppUpgrade(appUpdateTimeNs);
+
+    // Check dump report.
+    ProtoOutputStream output;
+    std::set<string> strSet;
+    int64_t dumpReportTimeNs = bucketStartTimeNs + 10000000000; // 10 seconds
+    valueProducer->onDumpReport(dumpReportTimeNs, false /* include current buckets */, true,
+                                NO_TIME_CONSTRAINTS /* dumpLatency */, &strSet, &output);
+
+    StatsLogReport report = outputStreamToProto(&output);
+    EXPECT_TRUE(report.has_value_metrics());
+    ASSERT_EQ(0, report.value_metrics().data_size());
+    ASSERT_EQ(1, report.value_metrics().skipped_size());
+
+    EXPECT_EQ(NanoToMillis(bucketStartTimeNs),
+              report.value_metrics().skipped(0).start_bucket_elapsed_millis());
+    EXPECT_EQ(NanoToMillis(appUpdateTimeNs),
+              report.value_metrics().skipped(0).end_bucket_elapsed_millis());
+    ASSERT_EQ(1, report.value_metrics().skipped(0).drop_event_size());
+
+    auto dropEvent = report.value_metrics().skipped(0).drop_event(0);
+    EXPECT_EQ(BucketDropReason::NO_DATA, dropEvent.drop_reason());
+    EXPECT_EQ(NanoToMillis(appUpdateTimeNs), dropEvent.drop_time_millis());
+}
+
 }  // namespace statsd
 }  // namespace os
 }  // namespace android
diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java
index 1aae04d..4cba6ea 100644
--- a/core/java/android/app/ResourcesManager.java
+++ b/core/java/android/app/ResourcesManager.java
@@ -52,10 +52,13 @@
 
 import java.io.IOException;
 import java.io.PrintWriter;
+import java.lang.ref.Reference;
+import java.lang.ref.ReferenceQueue;
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
 import java.util.WeakHashMap;
@@ -70,12 +73,6 @@
     private static ResourcesManager sResourcesManager;
 
     /**
-     * Predicate that returns true if a WeakReference is gc'ed.
-     */
-    private static final Predicate<WeakReference<Resources>> sEmptyReferencePredicate =
-            weakRef -> weakRef == null || weakRef.get() == null;
-
-    /**
      * The global compatibility settings.
      */
     private CompatibilityInfo mResCompatibilityInfo;
@@ -100,6 +97,7 @@
      */
     @UnsupportedAppUsage
     private final ArrayList<WeakReference<Resources>> mResourceReferences = new ArrayList<>();
+    private final ReferenceQueue<Resources> mResourcesReferencesQueue = new ReferenceQueue<>();
 
     private static class ApkKey {
         public final String path;
@@ -155,6 +153,7 @@
         }
         public final Configuration overrideConfig = new Configuration();
         public final ArrayList<WeakReference<Resources>> activityResources = new ArrayList<>();
+        final ReferenceQueue<Resources> activityResourcesQueue = new ReferenceQueue<>();
     }
 
     /**
@@ -667,12 +666,15 @@
             @NonNull CompatibilityInfo compatInfo) {
         final ActivityResources activityResources = getOrCreateActivityResourcesStructLocked(
                 activityToken);
+        cleanupReferences(activityResources.activityResources,
+                activityResources.activityResourcesQueue);
 
         Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader)
                 : new Resources(classLoader);
         resources.setImpl(impl);
         resources.setCallbacks(mUpdateCallbacks);
-        activityResources.activityResources.add(new WeakReference<>(resources));
+        activityResources.activityResources.add(
+                new WeakReference<>(resources, activityResources.activityResourcesQueue));
         if (DEBUG) {
             Slog.d(TAG, "- creating new ref=" + resources);
             Slog.d(TAG, "- setting ref=" + resources + " with impl=" + impl);
@@ -682,11 +684,13 @@
 
     private @NonNull Resources createResourcesLocked(@NonNull ClassLoader classLoader,
             @NonNull ResourcesImpl impl, @NonNull CompatibilityInfo compatInfo) {
+        cleanupReferences(mResourceReferences, mResourcesReferencesQueue);
+
         Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader)
                 : new Resources(classLoader);
         resources.setImpl(impl);
         resources.setCallbacks(mUpdateCallbacks);
-        mResourceReferences.add(new WeakReference<>(resources));
+        mResourceReferences.add(new WeakReference<>(resources, mResourcesReferencesQueue));
         if (DEBUG) {
             Slog.d(TAG, "- creating new ref=" + resources);
             Slog.d(TAG, "- setting ref=" + resources + " with impl=" + impl);
@@ -752,7 +756,6 @@
             updateResourcesForActivity(token, overrideConfig, displayId,
                     false /* movedToDifferentDisplay */);
 
-            cleanupReferences(token);
             rebaseKeyForActivity(token, key);
 
             synchronized (this) {
@@ -778,10 +781,6 @@
             final ActivityResources activityResources =
                     getOrCreateActivityResourcesStructLocked(activityToken);
 
-            // Clean up any dead references so they don't pile up.
-            ArrayUtils.unstableRemoveIf(activityResources.activityResources,
-                    sEmptyReferencePredicate);
-
             // Rebase the key's override config on top of the Activity's base override.
             if (key.hasOverrideConfiguration()
                     && !activityResources.overrideConfig.equals(Configuration.EMPTY)) {
@@ -794,21 +793,21 @@
 
     /**
      * Check WeakReferences and remove any dead references so they don't pile up.
-     * @param activityToken optional token to clean up Activity resources
      */
-    private void cleanupReferences(IBinder activityToken) {
-        synchronized (this) {
-            if (activityToken != null) {
-                ActivityResources activityResources = mActivityResourceReferences.get(
-                        activityToken);
-                if (activityResources != null) {
-                    ArrayUtils.unstableRemoveIf(activityResources.activityResources,
-                            sEmptyReferencePredicate);
-                }
-            } else {
-                ArrayUtils.unstableRemoveIf(mResourceReferences, sEmptyReferencePredicate);
-            }
+    private static <T> void cleanupReferences(ArrayList<WeakReference<T>> references,
+            ReferenceQueue<T> referenceQueue) {
+        Reference<? extends T> enduedRef = referenceQueue.poll();
+        if (enduedRef == null) {
+            return;
         }
+
+        final HashSet<Reference<? extends T>> deadReferences = new HashSet<>();
+        for (; enduedRef != null; enduedRef = referenceQueue.poll()) {
+            deadReferences.add(enduedRef);
+        }
+
+        ArrayUtils.unstableRemoveIf(references,
+                (ref) -> ref == null || deadReferences.contains(ref));
     }
 
     /**
@@ -896,8 +895,6 @@
                     loaders == null ? null : loaders.toArray(new ResourcesLoader[0]));
             classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
 
-            cleanupReferences(activityToken);
-
             if (activityToken != null) {
                 rebaseKeyForActivity(activityToken, key);
             }
diff --git a/core/java/android/content/pm/parsing/component/ParsedActivityUtils.java b/core/java/android/content/pm/parsing/component/ParsedActivityUtils.java
index f64560a..fb8fd74 100644
--- a/core/java/android/content/pm/parsing/component/ParsedActivityUtils.java
+++ b/core/java/android/content/pm/parsing/component/ParsedActivityUtils.java
@@ -302,7 +302,14 @@
         }
 
         String permission = array.getNonConfigurationString(permissionAttr, 0);
-        activity.setPermission(permission != null ? permission : pkg.getPermission());
+        if (isAlias) {
+            // An alias will override permissions to allow referencing an Activity through its alias
+            // without needing the original permission. If an alias needs the same permission,
+            // it must be re-declared.
+            activity.setPermission(permission);
+        } else {
+            activity.setPermission(permission != null ? permission : pkg.getPermission());
+        }
 
         final boolean setExported = array.hasValue(exportedAttr);
         if (setExported) {
diff --git a/core/java/android/content/pm/parsing/component/ParsedComponentUtils.java b/core/java/android/content/pm/parsing/component/ParsedComponentUtils.java
index b37b617..6811e06 100644
--- a/core/java/android/content/pm/parsing/component/ParsedComponentUtils.java
+++ b/core/java/android/content/pm/parsing/component/ParsedComponentUtils.java
@@ -20,7 +20,10 @@
 import android.annotation.Nullable;
 import android.content.pm.PackageManager;
 import android.content.pm.parsing.ParsingPackage;
+import android.content.pm.parsing.ParsingPackageUtils;
 import android.content.pm.parsing.ParsingUtils;
+import android.content.pm.parsing.result.ParseInput;
+import android.content.pm.parsing.result.ParseResult;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
@@ -29,9 +32,6 @@
 import android.util.TypedValue;
 
 import com.android.internal.annotations.VisibleForTesting;
-import android.content.pm.parsing.ParsingPackageUtils;
-import android.content.pm.parsing.result.ParseInput;
-import android.content.pm.parsing.result.ParseResult;
 
 /** @hide */
 class ParsedComponentUtils {
@@ -60,16 +60,27 @@
         component.setName(className);
         component.setPackageName(packageName);
 
-        if (useRoundIcon) {
-            component.icon = array.getResourceId(roundIconAttr, 0);
+        int roundIconVal = useRoundIcon ? array.getResourceId(roundIconAttr, 0) : 0;
+        if (roundIconVal != 0) {
+            component.icon = roundIconVal;
+            component.nonLocalizedLabel = null;
+        } else {
+            int iconVal = array.getResourceId(iconAttr, 0);
+            if (iconVal != 0) {
+                component.icon = iconVal;
+                component.nonLocalizedLabel = null;
+            }
         }
 
-        if (component.icon == 0) {
-            component.icon = array.getResourceId(iconAttr, 0);
+        int logoVal = array.getResourceId(logoAttr, 0);
+        if (logoVal != 0) {
+            component.logo = logoVal;
         }
 
-        component.logo = array.getResourceId(logoAttr, 0);
-        component.banner = array.getResourceId(bannerAttr, 0);
+        int bannerVal = array.getResourceId(bannerAttr, 0);
+        if (bannerVal != 0) {
+            component.banner = bannerVal;
+        }
 
         if (descriptionAttr != null) {
             component.descriptionRes = array.getResourceId(descriptionAttr, 0);
diff --git a/core/java/android/hardware/hdmi/HdmiClient.java b/core/java/android/hardware/hdmi/HdmiClient.java
index bff8c39..a921215 100644
--- a/core/java/android/hardware/hdmi/HdmiClient.java
+++ b/core/java/android/hardware/hdmi/HdmiClient.java
@@ -1,6 +1,7 @@
 package android.hardware.hdmi;
 
 import android.annotation.NonNull;
+import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.hardware.hdmi.HdmiControlManager.VendorCommandListener;
 import android.os.RemoteException;
@@ -84,7 +85,8 @@
      * @param hasVendorId {@code true} if the command type will be &lt;Vendor Command With ID&gt;.
      *                    {@code false} if the command will be &lt;Vendor Command&gt;
      */
-    public void sendVendorCommand(int targetAddress, byte[] params, boolean hasVendorId) {
+    public void sendVendorCommand(int targetAddress,
+            @SuppressLint("MissingNullability") byte[] params, boolean hasVendorId) {
         try {
             mService.sendVendorCommand(getDeviceType(), targetAddress, params, hasVendorId);
         } catch (RemoteException e) {
diff --git a/core/java/android/hardware/hdmi/HdmiControlManager.java b/core/java/android/hardware/hdmi/HdmiControlManager.java
index 6bc962b..1ce9b9c 100644
--- a/core/java/android/hardware/hdmi/HdmiControlManager.java
+++ b/core/java/android/hardware/hdmi/HdmiControlManager.java
@@ -18,6 +18,7 @@
 
 import static com.android.internal.os.RoSystemProperties.PROPERTY_HDMI_IS_DEVICE_HDMI_CEC_SWITCH;
 
+import android.annotation.CallbackExecutor;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -28,8 +29,10 @@
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.annotation.SystemService;
+import android.annotation.TestApi;
 import android.content.Context;
 import android.content.pm.PackageManager;
+import android.os.Binder;
 import android.os.RemoteException;
 import android.os.SystemProperties;
 import android.util.ArrayMap;
@@ -40,6 +43,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.Executor;
 
 /**
  * The {@link HdmiControlManager} class is used to send HDMI control messages
@@ -54,6 +58,7 @@
  * @hide
  */
 @SystemApi
+@TestApi
 @SystemService(Context.HDMI_CONTROL_SERVICE)
 @RequiresFeature(PackageManager.FEATURE_HDMI_CEC)
 public final class HdmiControlManager {
@@ -136,6 +141,8 @@
     public static final int POWER_STATUS_TRANSIENT_TO_ON = 2;
     public static final int POWER_STATUS_TRANSIENT_TO_STANDBY = 3;
 
+    /** @hide */
+    @SystemApi
     @IntDef ({
         RESULT_SUCCESS,
         RESULT_TIMEOUT,
@@ -397,8 +404,11 @@
      * See {@link HdmiDeviceInfo#DEVICE_PLAYBACK}
      * See {@link HdmiDeviceInfo#DEVICE_TV}
      * See {@link HdmiDeviceInfo#DEVICE_AUDIO_SYSTEM}
+     *
+     * @hide
      */
     @Nullable
+    @SystemApi
     @SuppressLint("Doclava125")
     public HdmiClient getClient(int type) {
         if (mService == null) {
@@ -427,8 +437,11 @@
      * system if the system is configured to host more than one type of HDMI-CEC logical devices.
      *
      * @return {@link HdmiPlaybackClient} instance. {@code null} on failure.
+     *
+     * @hide
      */
     @Nullable
+    @SystemApi
     @SuppressLint("Doclava125")
     public HdmiPlaybackClient getPlaybackClient() {
         return (HdmiPlaybackClient) getClient(HdmiDeviceInfo.DEVICE_PLAYBACK);
@@ -442,8 +455,11 @@
      * system if the system is configured to host more than one type of HDMI-CEC logical devices.
      *
      * @return {@link HdmiTvClient} instance. {@code null} on failure.
+     *
+     * @hide
      */
     @Nullable
+    @SystemApi
     @SuppressLint("Doclava125")
     public HdmiTvClient getTvClient() {
         return (HdmiTvClient) getClient(HdmiDeviceInfo.DEVICE_TV);
@@ -475,10 +491,8 @@
      * system if the system is configured to host more than one type of HDMI-CEC logical device.
      *
      * @return {@link HdmiSwitchClient} instance. {@code null} on failure.
-     * @hide
      */
     @Nullable
-    @SystemApi
     @SuppressLint("Doclava125")
     public HdmiSwitchClient getSwitchClient() {
         return (HdmiSwitchClient) getClient(HdmiDeviceInfo.DEVICE_PURE_CEC_SWITCH);
@@ -787,7 +801,10 @@
 
     /**
      * Listener used to get hotplug event from HDMI port.
+     *
+     * @hide
      */
+    @SystemApi
     public interface HotplugEventListener {
         void onReceived(HdmiHotplugEvent event);
     }
@@ -818,8 +835,29 @@
             mHdmiControlStatusChangeListeners = new ArrayMap<>();
 
     /**
-     * Listener used to get vendor-specific commands.
+     * Listener used to get the status of the HDMI CEC volume control feature (enabled/disabled).
+     * @hide
      */
+    public interface HdmiCecVolumeControlFeatureListener {
+        /**
+         * Called when the HDMI Control (CEC) volume control feature is enabled/disabled.
+         *
+         * @param enabled status of HDMI CEC volume control feature
+         * @see {@link HdmiControlManager#setHdmiCecVolumeControlEnabled(boolean)} ()}
+         **/
+        void onHdmiCecVolumeControlFeature(boolean enabled);
+    }
+
+    private final ArrayMap<HdmiCecVolumeControlFeatureListener,
+            IHdmiCecVolumeControlFeatureListener>
+            mHdmiCecVolumeControlFeatureListeners = new ArrayMap<>();
+
+    /**
+     * Listener used to get vendor-specific commands.
+     *
+     * @hide
+     */
+    @SystemApi
     public interface VendorCommandListener {
         /**
          * Called when a vendor command is received.
@@ -858,7 +896,10 @@
      *
      * @param listener {@link HotplugEventListener} instance
      * @see HdmiControlManager#removeHotplugEventListener(HotplugEventListener)
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(android.Manifest.permission.HDMI_CEC)
     public void addHotplugEventListener(HotplugEventListener listener) {
         if (mService == null) {
@@ -882,7 +923,10 @@
      * Removes a listener to stop getting informed of {@link HdmiHotplugEvent}.
      *
      * @param listener {@link HotplugEventListener} instance to be removed
+     *
+     * @hide
      */
+    @SystemApi
     @RequiresPermission(android.Manifest.permission.HDMI_CEC)
     public void removeHotplugEventListener(HotplugEventListener listener) {
         if (mService == null) {
@@ -979,4 +1023,76 @@
         };
     }
 
+    /**
+     * Adds a listener to get informed of changes to the state of the HDMI CEC volume control
+     * feature.
+     *
+     * Upon adding a listener, the current state of the HDMI CEC volume control feature will be
+     * sent immediately.
+     *
+     * <p>To stop getting the notification,
+     * use {@link #removeHdmiCecVolumeControlFeatureListener(HdmiCecVolumeControlFeatureListener)}.
+     *
+     * @param listener {@link HdmiCecVolumeControlFeatureListener} instance
+     * @hide
+     * @see #removeHdmiCecVolumeControlFeatureListener(HdmiCecVolumeControlFeatureListener)
+     */
+    @RequiresPermission(android.Manifest.permission.HDMI_CEC)
+    public void addHdmiCecVolumeControlFeatureListener(@NonNull @CallbackExecutor Executor executor,
+            @NonNull HdmiCecVolumeControlFeatureListener listener) {
+        if (mService == null) {
+            Log.e(TAG, "HdmiControlService is not available");
+            return;
+        }
+        if (mHdmiCecVolumeControlFeatureListeners.containsKey(listener)) {
+            Log.e(TAG, "listener is already registered");
+            return;
+        }
+        IHdmiCecVolumeControlFeatureListener wrappedListener =
+                createHdmiCecVolumeControlFeatureListenerWrapper(executor, listener);
+        mHdmiCecVolumeControlFeatureListeners.put(listener, wrappedListener);
+        try {
+            mService.addHdmiCecVolumeControlFeatureListener(wrappedListener);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Removes a listener to stop getting informed of changes to the state of the HDMI CEC volume
+     * control feature.
+     *
+     * @param listener {@link HdmiCecVolumeControlFeatureListener} instance to be removed
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.HDMI_CEC)
+    public void removeHdmiCecVolumeControlFeatureListener(
+            HdmiCecVolumeControlFeatureListener listener) {
+        if (mService == null) {
+            Log.e(TAG, "HdmiControlService is not available");
+            return;
+        }
+        IHdmiCecVolumeControlFeatureListener wrappedListener =
+                mHdmiCecVolumeControlFeatureListeners.remove(listener);
+        if (wrappedListener == null) {
+            Log.e(TAG, "tried to remove not-registered listener");
+            return;
+        }
+        try {
+            mService.removeHdmiCecVolumeControlFeatureListener(wrappedListener);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    private IHdmiCecVolumeControlFeatureListener createHdmiCecVolumeControlFeatureListenerWrapper(
+            Executor executor, final HdmiCecVolumeControlFeatureListener listener) {
+        return new android.hardware.hdmi.IHdmiCecVolumeControlFeatureListener.Stub() {
+            @Override
+            public void onHdmiCecVolumeControlFeature(boolean enabled) {
+                Binder.clearCallingIdentity();
+                executor.execute(() -> listener.onHdmiCecVolumeControlFeature(enabled));
+            }
+        };
+    }
 }
diff --git a/core/java/android/hardware/hdmi/HdmiControlServiceWrapper.java b/core/java/android/hardware/hdmi/HdmiControlServiceWrapper.java
new file mode 100644
index 0000000..0289635
--- /dev/null
+++ b/core/java/android/hardware/hdmi/HdmiControlServiceWrapper.java
@@ -0,0 +1,469 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.hdmi;
+
+import android.annotation.BinderThread;
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+
+import java.util.List;
+
+/**
+ * A wrapper of the Binder interface that clients running in the application process
+ * will use to perform HDMI-CEC features by communicating with other devices
+ * on the bus.
+ *
+ * @hide
+ */
+@TestApi
+public final class HdmiControlServiceWrapper {
+
+    /** Pure CEC switch device type. */
+    public static final int DEVICE_PURE_CEC_SWITCH = HdmiDeviceInfo.DEVICE_PURE_CEC_SWITCH;
+
+    private List<HdmiPortInfo> mInfoList = null;
+    private int[] mTypes = null;
+
+    /**
+     * Create a new HdmiControlManager with the current HdmiControlService wrapper
+     *
+     * @return the created HdmiControlManager
+     */
+    @NonNull
+    public HdmiControlManager createHdmiControlManager() {
+        return new HdmiControlManager(mInterface);
+    }
+
+    private final IHdmiControlService mInterface = new IHdmiControlService.Stub() {
+
+        @Override
+        public int[] getSupportedTypes() {
+            return HdmiControlServiceWrapper.this.getSupportedTypes();
+        }
+
+        @Override
+        public HdmiDeviceInfo getActiveSource() {
+            return HdmiControlServiceWrapper.this.getActiveSource();
+        }
+
+        @Override
+        public void oneTouchPlay(IHdmiControlCallback callback) {
+            HdmiControlServiceWrapper.this.oneTouchPlay(callback);
+        }
+
+        @Override
+        public void queryDisplayStatus(IHdmiControlCallback callback) {
+            HdmiControlServiceWrapper.this.queryDisplayStatus(callback);
+        }
+
+        @Override
+        public void addHdmiControlStatusChangeListener(IHdmiControlStatusChangeListener listener) {
+            HdmiControlServiceWrapper.this.addHdmiControlStatusChangeListener(listener);
+        }
+
+        @Override
+        public void removeHdmiControlStatusChangeListener(
+                IHdmiControlStatusChangeListener listener) {
+            HdmiControlServiceWrapper.this.removeHdmiControlStatusChangeListener(listener);
+        }
+
+        @Override
+        public void addHotplugEventListener(IHdmiHotplugEventListener listener) {
+            HdmiControlServiceWrapper.this.addHotplugEventListener(listener);
+        }
+
+        @Override
+        public void removeHotplugEventListener(IHdmiHotplugEventListener listener) {
+            HdmiControlServiceWrapper.this.removeHotplugEventListener(listener);
+        }
+
+        @Override
+        public void addDeviceEventListener(IHdmiDeviceEventListener listener) {
+            HdmiControlServiceWrapper.this.addDeviceEventListener(listener);
+        }
+
+        @Override
+        public void deviceSelect(int deviceId, IHdmiControlCallback callback) {
+            HdmiControlServiceWrapper.this.deviceSelect(deviceId, callback);
+        }
+
+        @Override
+        public void portSelect(int portId, IHdmiControlCallback callback) {
+            HdmiControlServiceWrapper.this.portSelect(portId, callback);
+        }
+
+        @Override
+        public void sendKeyEvent(int deviceType, int keyCode, boolean isPressed) {
+            HdmiControlServiceWrapper.this.sendKeyEvent(deviceType, keyCode, isPressed);
+        }
+
+        @Override
+        public void sendVolumeKeyEvent(int deviceType, int keyCode, boolean isPressed) {
+            HdmiControlServiceWrapper.this.sendVolumeKeyEvent(deviceType, keyCode, isPressed);
+        }
+
+        @Override
+        public List<HdmiPortInfo> getPortInfo() {
+            return HdmiControlServiceWrapper.this.getPortInfo();
+        }
+
+        @Override
+        public boolean canChangeSystemAudioMode() {
+            return HdmiControlServiceWrapper.this.canChangeSystemAudioMode();
+        }
+
+        @Override
+        public boolean getSystemAudioMode() {
+            return HdmiControlServiceWrapper.this.getSystemAudioMode();
+        }
+
+        @Override
+        public int getPhysicalAddress() {
+            return HdmiControlServiceWrapper.this.getPhysicalAddress();
+        }
+
+        @Override
+        public void setSystemAudioMode(boolean enabled, IHdmiControlCallback callback) {
+            HdmiControlServiceWrapper.this.setSystemAudioMode(enabled, callback);
+        }
+
+        @Override
+        public void addSystemAudioModeChangeListener(IHdmiSystemAudioModeChangeListener listener) {
+            HdmiControlServiceWrapper.this.addSystemAudioModeChangeListener(listener);
+        }
+
+        @Override
+        public void removeSystemAudioModeChangeListener(
+                IHdmiSystemAudioModeChangeListener listener) {
+            HdmiControlServiceWrapper.this.removeSystemAudioModeChangeListener(listener);
+        }
+
+        @Override
+        public void setArcMode(boolean enabled) {
+            HdmiControlServiceWrapper.this.setArcMode(enabled);
+        }
+
+        @Override
+        public void setProhibitMode(boolean enabled) {
+            HdmiControlServiceWrapper.this.setProhibitMode(enabled);
+        }
+
+        @Override
+        public void setSystemAudioVolume(int oldIndex, int newIndex, int maxIndex) {
+            HdmiControlServiceWrapper.this.setSystemAudioVolume(oldIndex, newIndex, maxIndex);
+        }
+
+        @Override
+        public void setSystemAudioMute(boolean mute) {
+            HdmiControlServiceWrapper.this.setSystemAudioMute(mute);
+        }
+
+        @Override
+        public void setInputChangeListener(IHdmiInputChangeListener listener) {
+            HdmiControlServiceWrapper.this.setInputChangeListener(listener);
+        }
+
+        @Override
+        public List<HdmiDeviceInfo> getInputDevices() {
+            return HdmiControlServiceWrapper.this.getInputDevices();
+        }
+
+        @Override
+        public List<HdmiDeviceInfo> getDeviceList() {
+            return HdmiControlServiceWrapper.this.getDeviceList();
+        }
+
+        @Override
+        public void powerOffRemoteDevice(int logicalAddress, int powerStatus) {
+            HdmiControlServiceWrapper.this.powerOffRemoteDevice(logicalAddress, powerStatus);
+        }
+
+        @Override
+        public void powerOnRemoteDevice(int logicalAddress, int powerStatus) {
+            HdmiControlServiceWrapper.this.powerOnRemoteDevice(logicalAddress, powerStatus);
+        }
+
+        @Override
+        public void askRemoteDeviceToBecomeActiveSource(int physicalAddress) {
+            HdmiControlServiceWrapper.this.askRemoteDeviceToBecomeActiveSource(physicalAddress);
+        }
+
+        @Override
+        public void sendVendorCommand(int deviceType, int targetAddress, byte[] params,
+                boolean hasVendorId) {
+            HdmiControlServiceWrapper.this.sendVendorCommand(
+                    deviceType, targetAddress, params, hasVendorId);
+        }
+
+        @Override
+        public void addVendorCommandListener(IHdmiVendorCommandListener listener, int deviceType) {
+            HdmiControlServiceWrapper.this.addVendorCommandListener(listener, deviceType);
+        }
+
+        @Override
+        public void sendStandby(int deviceType, int deviceId) {
+            HdmiControlServiceWrapper.this.sendStandby(deviceType, deviceId);
+        }
+
+        @Override
+        public void setHdmiRecordListener(IHdmiRecordListener callback) {
+            HdmiControlServiceWrapper.this.setHdmiRecordListener(callback);
+        }
+
+        @Override
+        public void startOneTouchRecord(int recorderAddress, byte[] recordSource) {
+            HdmiControlServiceWrapper.this.startOneTouchRecord(recorderAddress, recordSource);
+        }
+
+        @Override
+        public void stopOneTouchRecord(int recorderAddress) {
+            HdmiControlServiceWrapper.this.stopOneTouchRecord(recorderAddress);
+        }
+
+        @Override
+        public void startTimerRecording(int recorderAddress, int sourceType, byte[] recordSource) {
+            HdmiControlServiceWrapper.this.startTimerRecording(
+                    recorderAddress, sourceType, recordSource);
+        }
+
+        @Override
+        public void clearTimerRecording(int recorderAddress, int sourceType, byte[] recordSource) {
+            HdmiControlServiceWrapper.this.clearTimerRecording(
+                    recorderAddress, sourceType, recordSource);
+        }
+
+        @Override
+        public void sendMhlVendorCommand(int portId, int offset, int length, byte[] data) {
+            HdmiControlServiceWrapper.this.sendMhlVendorCommand(portId, offset, length, data);
+        }
+
+        @Override
+        public void addHdmiMhlVendorCommandListener(IHdmiMhlVendorCommandListener listener) {
+            HdmiControlServiceWrapper.this.addHdmiMhlVendorCommandListener(listener);
+        }
+
+        @Override
+        public void setStandbyMode(boolean isStandbyModeOn) {
+            HdmiControlServiceWrapper.this.setStandbyMode(isStandbyModeOn);
+        }
+
+        @Override
+        public void setHdmiCecVolumeControlEnabled(boolean isHdmiCecVolumeControlEnabled) {
+            HdmiControlServiceWrapper.this.setHdmiCecVolumeControlEnabled(
+                    isHdmiCecVolumeControlEnabled);
+        }
+
+        @Override
+        public boolean isHdmiCecVolumeControlEnabled() {
+            return HdmiControlServiceWrapper.this.isHdmiCecVolumeControlEnabled();
+        }
+
+        @Override
+        public void reportAudioStatus(int deviceType, int volume, int maxVolume, boolean isMute) {
+            HdmiControlServiceWrapper.this.reportAudioStatus(deviceType, volume, maxVolume, isMute);
+        }
+
+        @Override
+        public void setSystemAudioModeOnForAudioOnlySource() {
+            HdmiControlServiceWrapper.this.setSystemAudioModeOnForAudioOnlySource();
+        }
+
+        @Override
+        public void addHdmiCecVolumeControlFeatureListener(
+                IHdmiCecVolumeControlFeatureListener listener) {
+            HdmiControlServiceWrapper.this.addHdmiCecVolumeControlFeatureListener(listener);
+        }
+
+        @Override
+        public void removeHdmiCecVolumeControlFeatureListener(
+                IHdmiCecVolumeControlFeatureListener listener) {
+            HdmiControlServiceWrapper.this.removeHdmiCecVolumeControlFeatureListener(listener);
+        }
+    };
+
+    @BinderThread
+    public void setPortInfo(@NonNull List<HdmiPortInfo> infoList) {
+        mInfoList = infoList;
+    }
+
+    @BinderThread
+    public void setDeviceTypes(@NonNull int[] types) {
+        mTypes = types;
+    }
+
+    /** @hide */
+    public List<HdmiPortInfo> getPortInfo() {
+        return mInfoList;
+    }
+
+    /** @hide */
+    public int[] getSupportedTypes() {
+        return mTypes;
+    }
+
+    /** @hide */
+    public HdmiDeviceInfo getActiveSource() {
+        return null;
+    }
+
+    /** @hide */
+    public void oneTouchPlay(IHdmiControlCallback callback) {}
+
+    /** @hide */
+    public void queryDisplayStatus(IHdmiControlCallback callback) {}
+
+    /** @hide */
+    public void addHdmiControlStatusChangeListener(IHdmiControlStatusChangeListener listener) {}
+
+    /** @hide */
+    public void removeHdmiControlStatusChangeListener(IHdmiControlStatusChangeListener listener) {}
+
+    /** @hide */
+    public void addHotplugEventListener(IHdmiHotplugEventListener listener) {}
+
+    /** @hide */
+    public void removeHotplugEventListener(IHdmiHotplugEventListener listener) {}
+
+    /** @hide */
+    public void addDeviceEventListener(IHdmiDeviceEventListener listener) {}
+
+    /** @hide */
+    public void deviceSelect(int deviceId, IHdmiControlCallback callback) {}
+
+    /** @hide */
+    public void portSelect(int portId, IHdmiControlCallback callback) {}
+
+    /** @hide */
+    public void sendKeyEvent(int deviceType, int keyCode, boolean isPressed) {}
+
+    /** @hide */
+    public void sendVolumeKeyEvent(int deviceType, int keyCode, boolean isPressed) {}
+
+    /** @hide */
+    public boolean canChangeSystemAudioMode() {
+        return true;
+    }
+
+    /** @hide */
+    public boolean getSystemAudioMode() {
+        return true;
+    }
+
+    /** @hide */
+    public int getPhysicalAddress() {
+        return 0xffff;
+    }
+
+    /** @hide */
+    public void setSystemAudioMode(boolean enabled, IHdmiControlCallback callback) {}
+
+    /** @hide */
+    public void addSystemAudioModeChangeListener(IHdmiSystemAudioModeChangeListener listener) {}
+
+    /** @hide */
+    public void removeSystemAudioModeChangeListener(IHdmiSystemAudioModeChangeListener listener) {}
+
+    /** @hide */
+    public void setArcMode(boolean enabled) {}
+
+    /** @hide */
+    public void setProhibitMode(boolean enabled) {}
+
+    /** @hide */
+    public void setSystemAudioVolume(int oldIndex, int newIndex, int maxIndex) {}
+
+    /** @hide */
+    public void setSystemAudioMute(boolean mute) {}
+
+    /** @hide */
+    public void setInputChangeListener(IHdmiInputChangeListener listener) {}
+
+    /** @hide */
+    public List<HdmiDeviceInfo> getInputDevices() {
+        return null;
+    }
+
+    /** @hide */
+    public List<HdmiDeviceInfo> getDeviceList() {
+        return null;
+    }
+
+    /** @hide */
+    public void powerOffRemoteDevice(int logicalAddress, int powerStatus) {}
+
+    /** @hide */
+    public void powerOnRemoteDevice(int logicalAddress, int powerStatus) {}
+
+    /** @hide */
+    public void askRemoteDeviceToBecomeActiveSource(int physicalAddress) {}
+
+    /** @hide */
+    public void sendVendorCommand(int deviceType, int targetAddress, byte[] params,
+            boolean hasVendorId) {}
+
+    /** @hide */
+    public void addVendorCommandListener(IHdmiVendorCommandListener listener, int deviceType) {}
+
+    /** @hide */
+    public void sendStandby(int deviceType, int deviceId) {}
+
+    /** @hide */
+    public void setHdmiRecordListener(IHdmiRecordListener callback) {}
+
+    /** @hide */
+    public void startOneTouchRecord(int recorderAddress, byte[] recordSource) {}
+
+    /** @hide */
+    public void stopOneTouchRecord(int recorderAddress) {}
+
+    /** @hide */
+    public void startTimerRecording(int recorderAddress, int sourceType, byte[] recordSource) {}
+
+    /** @hide */
+    public void clearTimerRecording(int recorderAddress, int sourceType, byte[] recordSource) {}
+
+    /** @hide */
+    public void sendMhlVendorCommand(int portId, int offset, int length, byte[] data) {}
+
+    /** @hide */
+    public void addHdmiMhlVendorCommandListener(IHdmiMhlVendorCommandListener listener) {}
+
+    /** @hide */
+    public void setStandbyMode(boolean isStandbyModeOn) {}
+
+    /** @hide */
+    public void setHdmiCecVolumeControlEnabled(boolean isHdmiCecVolumeControlEnabled) {}
+
+    /** @hide */
+    public boolean isHdmiCecVolumeControlEnabled() {
+        return true;
+    }
+
+    /** @hide */
+    public void reportAudioStatus(int deviceType, int volume, int maxVolume, boolean isMute) {}
+
+    /** @hide */
+    public void setSystemAudioModeOnForAudioOnlySource() {}
+
+    /** @hide */
+    public void addHdmiCecVolumeControlFeatureListener(
+            IHdmiCecVolumeControlFeatureListener listener) {}
+
+    /** @hide */
+    public void removeHdmiCecVolumeControlFeatureListener(
+            IHdmiCecVolumeControlFeatureListener listener) {}
+}
diff --git a/core/java/android/hardware/hdmi/HdmiPortInfo.java b/core/java/android/hardware/hdmi/HdmiPortInfo.java
index 2623458..52c3628 100644
--- a/core/java/android/hardware/hdmi/HdmiPortInfo.java
+++ b/core/java/android/hardware/hdmi/HdmiPortInfo.java
@@ -18,6 +18,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
+import android.annotation.TestApi;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -28,6 +29,7 @@
  * @hide
  */
 @SystemApi
+@TestApi
 public final class HdmiPortInfo implements Parcelable {
     /** HDMI port type: Input */
     public static final int PORT_INPUT = 0;
@@ -153,7 +155,9 @@
      * @param dest The Parcel in which the object should be written.
      * @param flags Additional flags about how the object should be written.
      *        May be 0 or {@link Parcelable#PARCELABLE_WRITE_RETURN_VALUE}.
+     * @hide
      */
+    @SystemApi
     @Override
     public void writeToParcel(Parcel dest, int flags) {
         dest.writeInt(mId);
@@ -187,4 +191,9 @@
                 && mCecSupported == other.mCecSupported && mArcSupported == other.mArcSupported
                 && mMhlSupported == other.mMhlSupported;
     }
+
+    @Override
+    public int hashCode() {
+        return mId;
+    }
 }
diff --git a/core/java/android/hardware/hdmi/HdmiSwitchClient.java b/core/java/android/hardware/hdmi/HdmiSwitchClient.java
index 7833653..913edfd0 100644
--- a/core/java/android/hardware/hdmi/HdmiSwitchClient.java
+++ b/core/java/android/hardware/hdmi/HdmiSwitchClient.java
@@ -18,6 +18,7 @@
 import android.annotation.CallbackExecutor;
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
+import android.annotation.TestApi;
 import android.hardware.hdmi.HdmiControlManager.ControlCallbackResult;
 import android.os.Binder;
 import android.os.RemoteException;
@@ -38,6 +39,7 @@
  * @hide
  */
 @SystemApi
+@TestApi
 public class HdmiSwitchClient extends HdmiClient {
 
     private static final String TAG = "HdmiSwitchClient";
@@ -187,11 +189,8 @@
      * <p>This returns an empty list when the current device does not have HDMI input.
      *
      * @return a list of {@link HdmiPortInfo}
-     *
-     * @hide
      */
     @NonNull
-    @SystemApi
     public List<HdmiPortInfo> getPortInfo() {
         try {
             return mService.getPortInfo();
diff --git a/core/java/android/hardware/hdmi/IHdmiCecVolumeControlFeatureListener.aidl b/core/java/android/hardware/hdmi/IHdmiCecVolumeControlFeatureListener.aidl
new file mode 100644
index 0000000..873438b
--- /dev/null
+++ b/core/java/android/hardware/hdmi/IHdmiCecVolumeControlFeatureListener.aidl
@@ -0,0 +1,32 @@
+/*
+ * 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 android.hardware.hdmi;
+
+/**
+ * Listener used to get the status of the HDMI CEC volume control feature (enabled/disabled).
+ * @hide
+ */
+oneway interface IHdmiCecVolumeControlFeatureListener {
+
+    /**
+     * Called when the HDMI Control (CEC) volume control feature is enabled/disabled.
+     *
+     * @param enabled status of HDMI CEC volume control feature
+     * @see {@link HdmiControlManager#setHdmiCecVolumeControlEnabled(boolean)} ()}
+     **/
+    void onHdmiCecVolumeControlFeature(boolean enabled);
+}
diff --git a/core/java/android/hardware/hdmi/IHdmiControlService.aidl b/core/java/android/hardware/hdmi/IHdmiControlService.aidl
index 3582a92..4c724ef 100644
--- a/core/java/android/hardware/hdmi/IHdmiControlService.aidl
+++ b/core/java/android/hardware/hdmi/IHdmiControlService.aidl
@@ -18,6 +18,7 @@
 
 import android.hardware.hdmi.HdmiDeviceInfo;
 import android.hardware.hdmi.HdmiPortInfo;
+import android.hardware.hdmi.IHdmiCecVolumeControlFeatureListener;
 import android.hardware.hdmi.IHdmiControlCallback;
 import android.hardware.hdmi.IHdmiControlStatusChangeListener;
 import android.hardware.hdmi.IHdmiDeviceEventListener;
@@ -44,6 +45,8 @@
     void queryDisplayStatus(IHdmiControlCallback callback);
     void addHdmiControlStatusChangeListener(IHdmiControlStatusChangeListener listener);
     void removeHdmiControlStatusChangeListener(IHdmiControlStatusChangeListener listener);
+    void addHdmiCecVolumeControlFeatureListener(IHdmiCecVolumeControlFeatureListener listener);
+    void removeHdmiCecVolumeControlFeatureListener(IHdmiCecVolumeControlFeatureListener listener);
     void addHotplugEventListener(IHdmiHotplugEventListener listener);
     void removeHotplugEventListener(IHdmiHotplugEventListener listener);
     void addDeviceEventListener(IHdmiDeviceEventListener listener);
diff --git a/core/java/android/inputmethodservice/AbstractInputMethodService.java b/core/java/android/inputmethodservice/AbstractInputMethodService.java
index b0fca00..d7ca63a 100644
--- a/core/java/android/inputmethodservice/AbstractInputMethodService.java
+++ b/core/java/android/inputmethodservice/AbstractInputMethodService.java
@@ -260,4 +260,10 @@
      */
     public void notifyUserActionIfNecessary() {
     }
+
+    /** @hide */
+    @Override
+    public final boolean isUiContext() {
+        return true;
+    }
 }
diff --git a/core/java/android/os/StrictMode.java b/core/java/android/os/StrictMode.java
index 02b822a..772845d 100644
--- a/core/java/android/os/StrictMode.java
+++ b/core/java/android/os/StrictMode.java
@@ -817,6 +817,9 @@
 
             /** @hide */
             public @NonNull Builder permitActivityLeaks() {
+                synchronized (StrictMode.class) {
+                    sExpectedActivityInstanceCount.clear();
+                }
                 return disable(DETECT_VM_ACTIVITY_LEAKS);
             }
 
@@ -2586,8 +2589,10 @@
                 return;
             }
 
+            // Use the instance count from InstanceTracker as initial value.
             Integer expected = sExpectedActivityInstanceCount.get(klass);
-            Integer newExpected = expected == null ? 1 : expected + 1;
+            Integer newExpected =
+                    expected == null ? InstanceTracker.getInstanceCount(klass) + 1 : expected + 1;
             sExpectedActivityInstanceCount.put(klass, newExpected);
         }
     }
diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java
index 4ca48cb..e8806a0 100644
--- a/core/java/android/os/storage/StorageManager.java
+++ b/core/java/android/os/storage/StorageManager.java
@@ -1365,6 +1365,7 @@
                 String[] packageNames = ActivityThread.getPackageManager().getPackagesForUid(
                         android.os.Process.myUid());
                 if (packageNames == null || packageNames.length <= 0) {
+                    Log.w(TAG, "Missing package names; no storage volumes available");
                     return new StorageVolume[0];
                 }
                 packageName = packageNames[0];
@@ -1372,6 +1373,7 @@
             final int uid = ActivityThread.getPackageManager().getPackageUid(packageName,
                     PackageManager.MATCH_DEBUG_TRIAGED_MISSING, userId);
             if (uid <= 0) {
+                Log.w(TAG, "Missing UID; no storage volumes available");
                 return new StorageVolume[0];
             }
             return storageManager.getVolumeList(uid, packageName, flags);
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 52764d5..e10fcea 100755
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -14254,15 +14254,6 @@
         public static final String KERNEL_CPU_THREAD_READER = "kernel_cpu_thread_reader";
 
         /**
-         * Persistent user id that is last logged in to.
-         *
-         * They map to user ids, for example, 10, 11, 12.
-         *
-         * @hide
-         */
-        public static final String LAST_ACTIVE_USER_ID = "last_active_persistent_user_id";
-
-        /**
          * Whether we've enabled native flags health check on this device. Takes effect on
          * reboot. The value "1" enables native flags health check; otherwise it's disabled.
          * @hide
diff --git a/core/java/android/service/controls/Control.java b/core/java/android/service/controls/Control.java
index d01bc25..8383072a 100644
--- a/core/java/android/service/controls/Control.java
+++ b/core/java/android/service/controls/Control.java
@@ -73,25 +73,37 @@
     })
     public @interface Status {};
 
+    /**
+     * Reserved for use with the {@link StatelessBuilder}, and while loading. When state is
+     * requested via {@link ControlsProviderService#createPublisherFor}, use other status codes
+     * to indicate the proper device state.
+     */
     public static final int STATUS_UNKNOWN = 0;
 
     /**
-     * The device corresponding to the {@link Control} is responding correctly.
+     * Used to indicate that the state of the device was successfully retrieved. This includes
+     * all scenarios where the device may have a warning for the user, such as "Lock jammed",
+     * or "Vacuum stuck". Any information for the user should be set through
+     * {@link StatefulBuilder#setStatusText}.
      */
     public static final int STATUS_OK = 1;
 
     /**
-     * The device corresponding to the {@link Control} cannot be found or was removed.
+     * The device corresponding to the {@link Control} cannot be found or was removed. The user
+     * will be alerted and directed to the application to resolve.
      */
     public static final int STATUS_NOT_FOUND = 2;
 
     /**
-     * The device corresponding to the {@link Control} is in an error state.
+     * Used to indicate that there was a temporary error while loading the device state. A default
+     * error message will be displayed in place of any custom text that was set through
+     * {@link StatefulBuilder#setStatusText}.
      */
     public static final int STATUS_ERROR = 3;
 
     /**
-     * The {@link Control} is currently disabled.
+     * The {@link Control} is currently disabled.  A default error message will be displayed in
+     * place of any custom text that was set through {@link StatefulBuilder#setStatusText}.
      */
     public static final int STATUS_DISABLED = 4;
 
diff --git a/core/java/android/telephony/PhoneStateListener.java b/core/java/android/telephony/PhoneStateListener.java
index e4fbf9f..9b293eb 100644
--- a/core/java/android/telephony/PhoneStateListener.java
+++ b/core/java/android/telephony/PhoneStateListener.java
@@ -340,6 +340,10 @@
     /**
      *  Listen for display info changed event.
      *
+     *  Requires Permission: {@link android.Manifest.permission#READ_PHONE_STATE
+     *  READ_PHONE_STATE} or that the calling app has carrier privileges (see
+     *  {@link TelephonyManager#hasCarrierPrivileges}).
+     *
      *  @see #onDisplayInfoChanged
      */
     public static final int LISTEN_DISPLAY_INFO_CHANGED = 0x00100000;
diff --git a/core/java/android/view/GestureDetector.java b/core/java/android/view/GestureDetector.java
index f6c72c4..55c527b 100644
--- a/core/java/android/view/GestureDetector.java
+++ b/core/java/android/view/GestureDetector.java
@@ -16,6 +16,8 @@
 
 package android.view;
 
+import static android.os.StrictMode.vmIncorrectContextUseEnabled;
+
 import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS;
 import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DOUBLE_TAP;
 import static com.android.internal.util.FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS;
@@ -26,9 +28,12 @@
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.os.Build;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.Message;
+import android.os.StrictMode;
 import android.os.SystemClock;
+import android.util.Log;
 
 import com.android.internal.util.FrameworkStatsLog;
 
@@ -228,6 +233,7 @@
         }
     }
 
+    private static final String TAG = GestureDetector.class.getSimpleName();
     @UnsupportedAppUsage
     private int mTouchSlopSquare;
     private int mDoubleTapTouchSlopSquare;
@@ -378,7 +384,8 @@
      * You may only use this constructor from a {@link android.os.Looper} thread.
      * @see android.os.Handler#Handler()
      *
-     * @param context the application's context
+     * @param context An {@link android.app.Activity} or a {@link Context} created from
+     * {@link Context#createWindowContext(int, Bundle)}
      * @param listener the listener invoked for all the callbacks, this must
      * not be null. If the listener implements the {@link OnDoubleTapListener} or
      * {@link OnContextClickListener} then it will also be set as the listener for
@@ -395,7 +402,8 @@
      * thread associated with the supplied {@link android.os.Handler}.
      * @see android.os.Handler#Handler()
      *
-     * @param context the application's context
+     * @param context An {@link android.app.Activity} or a {@link Context} created from
+     * {@link Context#createWindowContext(int, Bundle)}
      * @param listener the listener invoked for all the callbacks, this must
      * not be null. If the listener implements the {@link OnDoubleTapListener} or
      * {@link OnContextClickListener} then it will also be set as the listener for
@@ -425,7 +433,8 @@
      * thread associated with the supplied {@link android.os.Handler}.
      * @see android.os.Handler#Handler()
      *
-     * @param context the application's context
+     * @param context An {@link android.app.Activity} or a {@link Context} created from
+     * {@link Context#createWindowContext(int, Bundle)}
      * @param listener the listener invoked for all the callbacks, this must
      * not be null.
      * @param handler the handler to use for running deferred listener events.
@@ -456,6 +465,17 @@
             mMaximumFlingVelocity = ViewConfiguration.getMaximumFlingVelocity();
             mAmbiguousGestureMultiplier = ViewConfiguration.getAmbiguousGestureMultiplier();
         } else {
+            if (!context.isUiContext() && vmIncorrectContextUseEnabled()) {
+                final String errorMessage =
+                        "Tried to access UI constants from a non-visual Context.";
+                final String message = "GestureDetector must be accessed from Activity or other "
+                        + "visual Context. Use an Activity or a Context created with "
+                        + "Context#createWindowContext(int, Bundle), which are adjusted to the "
+                        + "configuration and visual bounds of an area on screen.";
+                final Exception exception = new IllegalArgumentException(errorMessage);
+                StrictMode.onIncorrectContextUsed(message, exception);
+                Log.e(TAG, errorMessage + message, exception);
+            }
             final ViewConfiguration configuration = ViewConfiguration.get(context);
             touchSlop = configuration.getScaledTouchSlop();
             doubleTapTouchSlop = configuration.getScaledDoubleTapTouchSlop();
diff --git a/core/java/android/window/VirtualDisplayTaskEmbedder.java b/core/java/android/window/VirtualDisplayTaskEmbedder.java
index d2614da..9ccb4c1 100644
--- a/core/java/android/window/VirtualDisplayTaskEmbedder.java
+++ b/core/java/android/window/VirtualDisplayTaskEmbedder.java
@@ -365,8 +365,8 @@
             // Found the topmost stack on target display. Now check if the topmost task's
             // description changed.
             if (taskInfo.taskId == stackInfo.taskIds[stackInfo.taskIds.length - 1]) {
-                mHost.onTaskBackgroundColorChanged(VirtualDisplayTaskEmbedder.this,
-                        taskInfo.taskDescription.getBackgroundColor());
+                mHost.post(()-> mHost.onTaskBackgroundColorChanged(VirtualDisplayTaskEmbedder.this,
+                        taskInfo.taskDescription.getBackgroundColor()));
             }
         }
 
diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java
index 1b75178..508deac 100644
--- a/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java
+++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java
@@ -23,6 +23,7 @@
 import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.createEnableDialogContentView;
 import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.getInstalledTargets;
 import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.getTargets;
+import static com.android.internal.accessibility.util.AccessibilityUtils.isUserSetupCompleted;
 
 import android.annotation.Nullable;
 import android.app.Activity;
@@ -61,18 +62,8 @@
         }
 
         mTargets.addAll(getTargets(this, mShortcutType));
-
-        final String selectDialogTitle =
-                getString(R.string.accessibility_select_shortcut_menu_title);
         mTargetAdapter = new ShortcutTargetAdapter(mTargets);
-        mMenuDialog = new AlertDialog.Builder(this)
-                .setTitle(selectDialogTitle)
-                .setAdapter(mTargetAdapter, /* listener= */ null)
-                .setPositiveButton(
-                        getString(R.string.edit_accessibility_shortcut_menu_button),
-                        /* listener= */ null)
-                .setOnDismissListener(dialog -> finish())
-                .create();
+        mMenuDialog = createMenuDialog();
         mMenuDialog.setOnShowListener(dialog -> updateDialogListeners());
         mMenuDialog.show();
     }
@@ -154,4 +145,22 @@
         mMenuDialog.getListView().setOnItemClickListener(
                 isEditMenuMode ? this::onTargetChecked : this::onTargetSelected);
     }
+
+    private AlertDialog createMenuDialog() {
+        final String dialogTitle =
+                getString(R.string.accessibility_select_shortcut_menu_title);
+
+        final AlertDialog.Builder builder = new AlertDialog.Builder(this)
+                .setTitle(dialogTitle)
+                .setAdapter(mTargetAdapter, /* listener= */ null)
+                .setOnDismissListener(dialog -> finish());
+
+        if (isUserSetupCompleted(this)) {
+            final String positiveButtonText =
+                    getString(R.string.edit_accessibility_shortcut_menu_button);
+            builder.setPositiveButton(positiveButtonText, /* listener= */ null);
+        }
+
+        return builder.create();
+    }
 }
diff --git a/core/java/com/android/internal/accessibility/util/AccessibilityUtils.java b/core/java/com/android/internal/accessibility/util/AccessibilityUtils.java
index 9ee0b0e..4b4e20f 100644
--- a/core/java/com/android/internal/accessibility/util/AccessibilityUtils.java
+++ b/core/java/com/android/internal/accessibility/util/AccessibilityUtils.java
@@ -156,4 +156,16 @@
 
         return false;
     }
+
+    /**
+     * Indicates whether the current user has completed setup via the setup wizard.
+     * {@link android.provider.Settings.Secure#USER_SETUP_COMPLETE}
+     *
+     * @return {@code true} if the setup is completed.
+     */
+    public static boolean isUserSetupCompleted(Context context) {
+        return Settings.Secure.getIntForUser(context.getContentResolver(),
+                Settings.Secure.USER_SETUP_COMPLETE, /* def= */ 0, UserHandle.USER_CURRENT)
+                != /* false */ 0;
+    }
 }
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index 049a76c..b36c71f 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -793,7 +793,6 @@
     private AppPredictor.Callback createAppPredictorCallback(
             ChooserListAdapter chooserListAdapter) {
         return resultList -> {
-            //TODO(arangelov) Take care of edge case when callback called after swiping tabs
             if (isFinishing() || isDestroyed()) {
                 return;
             }
@@ -802,8 +801,6 @@
             }
             if (resultList.isEmpty()) {
                 // APS may be disabled, so try querying targets ourselves.
-                //TODO(arangelov) queryDirectShareTargets indirectly uses mIntents.
-                // Investigate implications for work tab.
                 queryDirectShareTargets(chooserListAdapter, true);
                 return;
             }
@@ -1809,7 +1806,8 @@
         }
     }
 
-    void queryTargetServices(ChooserListAdapter adapter) {
+    @VisibleForTesting
+    protected void queryTargetServices(ChooserListAdapter adapter) {
         mQueriedTargetServicesTimeMs = System.currentTimeMillis();
 
         Context selectedProfileContext = createContextAsUser(
@@ -1962,7 +1960,8 @@
         return driList;
     }
 
-    private void queryDirectShareTargets(
+    @VisibleForTesting
+    protected void queryDirectShareTargets(
                 ChooserListAdapter adapter, boolean skipAppPredictionService) {
         mQueriedSharingShortcutsTimeMs = System.currentTimeMillis();
         UserHandle userHandle = adapter.getUserHandle();
@@ -1974,7 +1973,6 @@
             }
         }
         // Default to just querying ShortcutManager if AppPredictor not present.
-        //TODO(arangelov) we're using mIntents here, investicate possible implications on work tab
         final IntentFilter filter = getTargetIntentFilter();
         if (filter == null) {
             return;
@@ -2654,6 +2652,7 @@
                 if (recyclerView.getVisibility() == View.VISIBLE) {
                     int directShareHeight = 0;
                     rowsToShow = Math.min(4, rowsToShow);
+                    boolean shouldShowExtraRow = shouldShowExtraRow(rowsToShow);
                     mLastNumberOfChildren = recyclerView.getChildCount();
                     for (int i = 0, childCount = recyclerView.getChildCount();
                             i < childCount && rowsToShow > 0; i++) {
@@ -2664,6 +2663,9 @@
                         }
                         int height = child.getHeight();
                         offset += height;
+                        if (shouldShowExtraRow) {
+                            offset += height;
+                        }
 
                         if (gridAdapter.getTargetType(
                                 recyclerView.getChildAdapterPosition(child))
@@ -2699,6 +2701,18 @@
     }
 
     /**
+     * If we have a tabbed view and are showing 1 row in the current profile and an empty
+     * state screen in the other profile, to prevent cropping of the empty state screen we show
+     * a second row in the current profile.
+     */
+    private boolean shouldShowExtraRow(int rowsToShow) {
+        return shouldShowTabs()
+                && rowsToShow == 1
+                && mChooserMultiProfilePagerAdapter.shouldShowEmptyStateScreen(
+                        mChooserMultiProfilePagerAdapter.getInactiveListAdapter());
+    }
+
+    /**
      * Returns {@link #PROFILE_PERSONAL}, {@link #PROFILE_WORK}, or -1 if the given user handle
      * does not match either the personal or work user handle.
      **/
diff --git a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
index 71ee8af..15ba8e8 100644
--- a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
+++ b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
@@ -265,4 +265,16 @@
     void performDirectAction(in IBinder token, String actionId, in Bundle arguments, int taskId,
             IBinder assistToken, in RemoteCallback cancellationCallback,
             in RemoteCallback resultCallback);
+
+    /**
+     * Temporarily disables voice interaction (for example, on Automotive when the display is off).
+     *
+     * It will shutdown the service, and only re-enable it after it's called again (or after a
+     * system restart).
+     *
+     * NOTE: it's only effective when the service itself is available / enabled in the device, so
+     * calling setDisable(false) would be a no-op when it isn't.
+     */
+    void setDisabled(boolean disabled);
+
 }
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index f8eec57..daacd45 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -159,9 +159,6 @@
     protected static final String METRICS_CATEGORY_RESOLVER = "intent_resolver";
     protected static final String METRICS_CATEGORY_CHOOSER = "intent_chooser";
 
-    /**
-     * TODO(arangelov): Remove a couple of weeks after work/personal tabs are finalized.
-     */
     @VisibleForTesting
     public static boolean ENABLE_TABBED_VIEW = true;
     private static final String TAB_TAG_PERSONAL = "personal";
@@ -1819,6 +1816,12 @@
         ResolverListAdapter activeListAdapter =
                 mMultiProfilePagerAdapter.getActiveListAdapter();
         View buttonBarDivider = findViewById(R.id.resolver_button_bar_divider);
+        if (!useLayoutWithDefault()) {
+            int inset = mSystemWindowInsets != null ? mSystemWindowInsets.bottom : 0;
+            buttonLayout.setPadding(buttonLayout.getPaddingLeft(), buttonLayout.getPaddingTop(),
+                    buttonLayout.getPaddingRight(), getResources().getDimensionPixelSize(
+                            R.dimen.resolver_button_bar_spacing) + inset);
+        }
         if (activeListAdapter.isTabLoaded()
                 && mMultiProfilePagerAdapter.shouldShowEmptyStateScreen(activeListAdapter)
                 && !useLayoutWithDefault()) {
@@ -1835,12 +1838,6 @@
         buttonLayout.setVisibility(View.VISIBLE);
         setButtonBarIgnoreOffset(/* ignoreOffset */ true);
 
-        if (!useLayoutWithDefault()) {
-            int inset = mSystemWindowInsets != null ? mSystemWindowInsets.bottom : 0;
-            buttonLayout.setPadding(buttonLayout.getPaddingLeft(), buttonLayout.getPaddingTop(),
-                    buttonLayout.getPaddingRight(), getResources().getDimensionPixelSize(
-                            R.dimen.resolver_button_bar_spacing) + inset);
-        }
         mOnceButton = (Button) buttonLayout.findViewById(R.id.button_once);
         mAlwaysButton = (Button) buttonLayout.findViewById(R.id.button_always);
 
diff --git a/core/java/com/android/internal/app/ResolverListAdapter.java b/core/java/com/android/internal/app/ResolverListAdapter.java
index 97f43d27..b1e8ed1 100644
--- a/core/java/com/android/internal/app/ResolverListAdapter.java
+++ b/core/java/com/android/internal/app/ResolverListAdapter.java
@@ -423,7 +423,6 @@
     // We assume that at this point we've already filtered out the only intent for a different
     // targetUserId which we're going to use.
     private void addResolveInfo(DisplayResolveInfo dri) {
-        // TODO(arangelov): Is that UserHandle.USER_CURRENT check okay?
         if (dri != null && dri.getResolveInfo() != null
                 && dri.getResolveInfo().targetUserId == UserHandle.USER_CURRENT) {
             if (shouldAddResolveInfo(dri)) {
diff --git a/core/java/com/android/internal/widget/ConversationLayout.java b/core/java/com/android/internal/widget/ConversationLayout.java
index c75f72b..0d2dbef 100644
--- a/core/java/com/android/internal/widget/ConversationLayout.java
+++ b/core/java/com/android/internal/widget/ConversationLayout.java
@@ -1223,7 +1223,6 @@
             mExpandButtonContainer.setVisibility(VISIBLE);
             mExpandButtonInnerContainer.setOnClickListener(onClickListener);
         } else {
-            // TODO: handle content paddings to end of layout
             mExpandButtonContainer.setVisibility(GONE);
         }
         updateContentEndPaddings();
diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp
index 3d8cae8..5c444bd 100644
--- a/core/jni/com_android_internal_os_Zygote.cpp
+++ b/core/jni/com_android_internal_os_Zygote.cpp
@@ -1744,6 +1744,8 @@
       heap_tagging_level = M_HEAP_TAGGING_LEVEL_NONE;
   }
   android_mallopt(M_SET_HEAP_TAGGING_LEVEL, &heap_tagging_level, sizeof(heap_tagging_level));
+  // Now that we've used the flag, clear it so that we don't pass unknown flags to the ART runtime.
+  runtime_flags &= ~RuntimeFlags::MEMORY_TAG_LEVEL_MASK;
 
   bool forceEnableGwpAsan = false;
   switch (runtime_flags & RuntimeFlags::GWP_ASAN_LEVEL_MASK) {
@@ -1756,6 +1758,8 @@
       case RuntimeFlags::GWP_ASAN_LEVEL_LOTTERY:
           android_mallopt(M_INITIALIZE_GWP_ASAN, &forceEnableGwpAsan, sizeof(forceEnableGwpAsan));
   }
+  // Now that we've used the flag, clear it so that we don't pass unknown flags to the ART runtime.
+  runtime_flags &= ~RuntimeFlags::GWP_ASAN_LEVEL_MASK;
 
   if (NeedsNoRandomizeWorkaround()) {
     // Work around ARM kernel ASLR lossage (http://b/5817320).
diff --git a/core/res/res/layout/notification_material_action_list.xml b/core/res/res/layout/notification_material_action_list.xml
index ec54091..3615b9e 100644
--- a/core/res/res/layout/notification_material_action_list.xml
+++ b/core/res/res/layout/notification_material_action_list.xml
@@ -22,10 +22,11 @@
         android:layout_gravity="bottom">
 
         <LinearLayout
+            android:id="@+id/actions_container_layout"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:orientation="horizontal"
-            android:paddingEnd="12dp"
+            android:paddingEnd="@dimen/bubble_gone_padding_end"
             >
 
             <com.android.internal.widget.NotificationActionListLayout
diff --git a/core/res/res/values-television/themes_device_defaults.xml b/core/res/res/values-television/themes_device_defaults.xml
index cb3d328..d6bdeee 100644
--- a/core/res/res/values-television/themes_device_defaults.xml
+++ b/core/res/res/values-television/themes_device_defaults.xml
@@ -33,5 +33,6 @@
     <style name="Theme.DeviceDefault.Autofill.Light" parent="Theme.DeviceDefault.Autofill"/>
     <style name="Theme.DeviceDefault.Light.Autofill.Save" parent="Theme.DeviceDefault.Autofill.Save"/>
 
-    <style name="Theme.DeviceDefault.Resolver" parent="Theme.Leanback.Resolver" />
+    <style name="Theme.DeviceDefault.ResolverCommon" parent="Theme.Leanback.Resolver" />
+    <style name="Theme.DeviceDefault.Resolver" parent="Theme.DeviceDefault.ResolverCommon" />
 </resources>
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 2cad9e6..b79c9e8 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -4456,11 +4456,4 @@
     <bool name="config_pdp_reject_enable_retry">false</bool>
     <!-- pdp data reject retry delay in ms -->
     <integer name="config_pdp_reject_retry_delay_ms">-1</integer>
-
-    <!-- Package name that is recognized as an actor for the packages listed in
-         @array/config_overlayableConfiguratorTargets. If an overlay targeting one of the listed
-         targets is signed with the same signature as the configurator, the overlay will be granted
-         the "actor" policy. -->
-    <string name="config_overlayableConfigurator" translatable="false" />
-    <string-array name="config_overlayableConfiguratorTargets" translatable="false" />
 </resources>
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index ad3d20e..59bb052 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -312,6 +312,12 @@
     <!-- The margin of the content to an image-->
     <dimen name="notification_content_image_margin_end">8dp</dimen>
 
+    <!-- The padding at the end of actions when the bubble button is visible-->
+    <dimen name="bubble_visible_padding_end">3dp</dimen>
+
+    <!-- The padding at the end of actions when the bubble button is gone-->
+    <dimen name="bubble_gone_padding_end">12dp</dimen>
+
     <!-- The spacing between messages in Notification.MessagingStyle -->
     <dimen name="notification_messaging_spacing">6dp</dimen>
 
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index f30d482e..051bf7c 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1785,6 +1785,7 @@
   <java-symbol type="string" name="faceunlock_multiple_failures" />
   <java-symbol type="string" name="global_actions" />
   <java-symbol type="string" name="global_action_power_off" />
+  <java-symbol type="string" name="global_action_power_options" />
   <java-symbol type="string" name="global_action_restart" />
   <java-symbol type="string" name="global_actions_airplane_mode_off_status" />
   <java-symbol type="string" name="global_actions_airplane_mode_on_status" />
@@ -2766,6 +2767,7 @@
   <java-symbol type="bool" name="config_mainBuiltInDisplayIsRound" />
 
   <java-symbol type="id" name="actions_container" />
+  <java-symbol type="id" name="actions_container_layout" />
   <java-symbol type="id" name="smart_reply_container" />
   <java-symbol type="id" name="remote_input_tag" />
   <java-symbol type="id" name="pending_intent_tag" />
@@ -3533,6 +3535,8 @@
   <java-symbol type="id" name="clip_to_padding_tag" />
   <java-symbol type="id" name="clip_children_tag" />
   <java-symbol type="id" name="bubble_button" />
+  <java-symbol type="dimen" name="bubble_visible_padding_end" />
+  <java-symbol type="dimen" name="bubble_gone_padding_end" />
   <java-symbol type="dimen" name="messaging_avatar_size" />
   <java-symbol type="dimen" name="messaging_group_sending_progress_size" />
   <java-symbol type="dimen" name="messaging_image_rounding" />
@@ -4033,8 +4037,5 @@
   <java-symbol type="string" name="config_pdp_reject_service_not_subscribed" />
   <java-symbol type="string" name="config_pdp_reject_multi_conn_to_same_pdn_not_allowed" />
 
-  <java-symbol type="string" name="config_overlayableConfigurator" />
-  <java-symbol type="array" name="config_overlayableConfiguratorTargets" />
-
   <java-symbol type="array" name="config_notificationMsgPkgsAllowedAsConvos" />
 </resources>
diff --git a/core/res/res/values/themes_leanback.xml b/core/res/res/values/themes_leanback.xml
index a80725c..9dca912 100644
--- a/core/res/res/values/themes_leanback.xml
+++ b/core/res/res/values/themes_leanback.xml
@@ -128,6 +128,10 @@
 
         <!-- Toolbar attributes -->
         <item name="toolbarStyle">@style/Widget.DeviceDefault.Toolbar</item>
+
+        <!-- Icon sizes -->
+        <item name="iconfactoryIconSize">@dimen/resolver_icon_size</item>
+        <item name="iconfactoryBadgeSize">@dimen/resolver_badge_size</item>
       </style>
 
     <!-- @hide Special theme for the default system Activity-based Alert dialogs. -->
diff --git a/core/tests/coretests/src/android/content/ContextTest.java b/core/tests/coretests/src/android/content/ContextTest.java
index f13a11e..17d1389 100644
--- a/core/tests/coretests/src/android/content/ContextTest.java
+++ b/core/tests/coretests/src/android/content/ContextTest.java
@@ -23,12 +23,14 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 import android.app.ActivityThread;
 import android.content.res.Configuration;
 import android.graphics.PixelFormat;
 import android.hardware.display.DisplayManager;
 import android.hardware.display.VirtualDisplay;
+import android.inputmethodservice.InputMethodService;
 import android.media.ImageReader;
 import android.os.UserHandle;
 import android.view.Display;
@@ -136,6 +138,13 @@
     }
 
     @Test
+    public void testIsUiContext_InputMethodService_returnsTrue() {
+        final InputMethodService ims = new InputMethodService();
+
+        assertTrue(ims.isUiContext());
+    }
+
+    @Test
     public void testGetDisplayFromDisplayContextDerivedContextOnPrimaryDisplay() {
         verifyGetDisplayFromDisplayContextDerivedContext(false /* onSecondaryDisplay */);
     }
diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java
index dcecb5f..5471768 100644
--- a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java
+++ b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java
@@ -34,9 +34,11 @@
 import static com.android.internal.app.ChooserWrapperActivity.sOverrides;
 import static com.android.internal.app.MatcherUtils.first;
 
+import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertNull;
 import static junit.framework.Assert.assertTrue;
 
+import static org.hamcrest.CoreMatchers.allOf;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.CoreMatchers.notNullValue;
@@ -1327,7 +1329,6 @@
         assertThat(activity.getWorkListAdapter().getCount(), is(workProfileTargets));
     }
 
-    @Ignore // b/148156663
     @Test
     public void testWorkTab_selectingWorkTabAppOpensAppInWorkProfile() throws InterruptedException {
         // enable the work tab feature flag
@@ -1354,8 +1355,10 @@
         // wait for the share sheet to expand
         Thread.sleep(ChooserActivity.LIST_VIEW_UPDATE_INTERVAL_IN_MILLIS);
 
-        onView(first(withText(workResolvedComponentInfos.get(0)
-                .getResolveInfoAt(0).activityInfo.applicationInfo.name)))
+        onView(first(allOf(
+                withText(workResolvedComponentInfos.get(0)
+                        .getResolveInfoAt(0).activityInfo.applicationInfo.name),
+                isDisplayed())))
                 .perform(click());
         waitForIdle();
         assertThat(chosen[0], is(workResolvedComponentInfos.get(0).getResolveInfoAt(0)));
@@ -1953,6 +1956,45 @@
         assertThat(activity.getAdapter().getRankedTargetCount(), is(3));
     }
 
+    @Test
+    public void testWorkTab_selectingWorkTabWithPausedWorkProfile_directShareTargetsNotQueried() {
+        // enable the work tab feature flag
+        ResolverActivity.ENABLE_TABBED_VIEW = true;
+        markWorkProfileUserAvailable();
+        List<ResolvedComponentInfo> personalResolvedComponentInfos =
+                createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
+        List<ResolvedComponentInfo> workResolvedComponentInfos =
+                createResolvedComponentsForTest(3);
+        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+        sOverrides.isQuietModeEnabled = true;
+        boolean[] isQueryDirectShareCalledOnWorkProfile = new boolean[] { false };
+        sOverrides.onQueryDirectShareTargets = chooserListAdapter -> {
+            isQueryDirectShareCalledOnWorkProfile[0] =
+                    (chooserListAdapter.getUserHandle().getIdentifier() == 10);
+            return null;
+        };
+        boolean[] isQueryTargetServicesCalledOnWorkProfile = new boolean[] { false };
+        sOverrides.onQueryTargetServices = chooserListAdapter -> {
+            isQueryTargetServicesCalledOnWorkProfile[0] =
+                    (chooserListAdapter.getUserHandle().getIdentifier() == 10);
+            return null;
+        };
+        Intent sendIntent = createSendTextIntent();
+        sendIntent.setType("TestType");
+
+        mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
+        waitForIdle();
+        onView(withId(R.id.contentPanel))
+                .perform(swipeUp());
+        onView(withText(R.string.resolver_work_tab)).perform(click());
+        waitForIdle();
+
+        assertFalse("Direct share targets were queried on a paused work profile",
+                isQueryDirectShareCalledOnWorkProfile[0]);
+        assertFalse("Target services were queried on a paused work profile",
+                isQueryTargetServicesCalledOnWorkProfile[0]);
+    }
+
     private Intent createChooserIntent(Intent intent, Intent[] initialIntents) {
         Intent chooserIntent = new Intent();
         chooserIntent.setAction(Intent.ACTION_CHOOSER);
diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java b/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java
index 749b0e5..44a5263 100644
--- a/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java
+++ b/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java
@@ -205,6 +205,23 @@
         return getApplicationContext();
     }
 
+    @Override
+    protected void queryDirectShareTargets(ChooserListAdapter adapter,
+            boolean skipAppPredictionService) {
+        if (sOverrides.onQueryDirectShareTargets != null) {
+            sOverrides.onQueryDirectShareTargets.apply(adapter);
+        }
+        super.queryDirectShareTargets(adapter, skipAppPredictionService);
+    }
+
+    @Override
+    protected void queryTargetServices(ChooserListAdapter adapter) {
+        if (sOverrides.onQueryTargetServices != null) {
+            sOverrides.onQueryTargetServices.apply(adapter);
+        }
+        super.queryTargetServices(adapter);
+    }
+
     /**
      * We cannot directly mock the activity created since instrumentation creates it.
      * <p>
@@ -214,6 +231,8 @@
         @SuppressWarnings("Since15")
         public Function<PackageManager, PackageManager> createPackageManager;
         public Function<TargetInfo, Boolean> onSafelyStartCallback;
+        public Function<ChooserListAdapter, Void> onQueryDirectShareTargets;
+        public Function<ChooserListAdapter, Void> onQueryTargetServices;
         public ResolverListController resolverListController;
         public ResolverListController workResolverListController;
         public Boolean isVoiceInteraction;
@@ -233,6 +252,8 @@
 
         public void reset() {
             onSafelyStartCallback = null;
+            onQueryDirectShareTargets = null;
+            onQueryTargetServices = null;
             isVoiceInteraction = null;
             createPackageManager = null;
             previewThumbnail = null;
diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java
index 8bee1e5..7dc5a8b 100644
--- a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java
+++ b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java
@@ -592,7 +592,6 @@
                 TextUtils.equals(initialText, currentText));
     }
 
-    @Ignore // b/148156663
     @Test
     public void testWorkTab_noPersonalApps_canStartWorkApps()
             throws InterruptedException {
@@ -617,8 +616,10 @@
         waitForIdle();
         // wait for the share sheet to expand
         Thread.sleep(ChooserActivity.LIST_VIEW_UPDATE_INTERVAL_IN_MILLIS);
-        onView(first(allOf(withText(workResolvedComponentInfos.get(0)
-                .getResolveInfoAt(0).activityInfo.applicationInfo.name), isCompletelyDisplayed())))
+        onView(first(allOf(
+                withText(workResolvedComponentInfos.get(0)
+                        .getResolveInfoAt(0).activityInfo.applicationInfo.name),
+                isDisplayed())))
                 .perform(click());
         onView(withId(R.id.button_once))
                 .perform(click());
diff --git a/core/tests/hdmitests/src/android/hardware/hdmi/HdmiAudioSystemClientTest.java b/core/tests/hdmitests/src/android/hardware/hdmi/HdmiAudioSystemClientTest.java
index 7cd2f3b..a4f2065 100644
--- a/core/tests/hdmitests/src/android/hardware/hdmi/HdmiAudioSystemClientTest.java
+++ b/core/tests/hdmitests/src/android/hardware/hdmi/HdmiAudioSystemClientTest.java
@@ -363,6 +363,16 @@
         public boolean isHdmiCecVolumeControlEnabled() {
             return true;
         }
+
+        @Override
+        public void addHdmiCecVolumeControlFeatureListener(
+                IHdmiCecVolumeControlFeatureListener listener) {
+        }
+
+        @Override
+        public void removeHdmiCecVolumeControlFeatureListener(
+                IHdmiCecVolumeControlFeatureListener listener) {
+        }
     }
 
 }
diff --git a/core/tests/overlaytests/remount/Android.bp b/core/tests/overlaytests/remount/Android.bp
index 5757cfe..4e79a45 100644
--- a/core/tests/overlaytests/remount/Android.bp
+++ b/core/tests/overlaytests/remount/Android.bp
@@ -28,5 +28,6 @@
         ":OverlayRemountedTest_Target",
         ":OverlayRemountedTest_TargetUpgrade",
         ":OverlayRemountedTest_Overlay",
+        ":OverlayRemountedTest_Overlay_SameCert",
     ],
 }
diff --git a/core/tests/overlaytests/remount/src/com/android/overlaytest/remounted/PackagedUpgradedTest.java b/core/tests/overlaytests/remount/src/com/android/overlaytest/remounted/RegenerateIdmapTest.java
similarity index 66%
rename from core/tests/overlaytests/remount/src/com/android/overlaytest/remounted/PackagedUpgradedTest.java
rename to core/tests/overlaytests/remount/src/com/android/overlaytest/remounted/RegenerateIdmapTest.java
index a465640..2b68015 100644
--- a/core/tests/overlaytests/remount/src/com/android/overlaytest/remounted/PackagedUpgradedTest.java
+++ b/core/tests/overlaytests/remount/src/com/android/overlaytest/remounted/RegenerateIdmapTest.java
@@ -22,7 +22,9 @@
 import org.junit.runner.RunWith;
 
 @RunWith(DeviceJUnit4ClassRunner.class)
-public class PackagedUpgradedTest extends OverlayRemountedTestBase {
+public class RegenerateIdmapTest extends OverlayRemountedTestBase {
+    private static final String OVERLAY_SIGNATURE_APK =
+            "OverlayRemountedTest_Overlay_SameCert.apk";
     private static final String TARGET_UPGRADE_APK = "OverlayRemountedTest_TargetUpgrade.apk";
 
     @Test
@@ -66,4 +68,32 @@
         assertResource(targetReference, "@" + 0x7f0100ff + " -> true");
         assertResource(targetOverlaid, "true");
     }
+
+    @Test
+    public void testIdmapPoliciesChanged() throws Exception {
+        final String targetResource = resourceName(TARGET_PACKAGE, "bool",
+                "signature_policy_overlaid");
+
+        mPreparer.pushResourceFile(TARGET_APK, "/product/app/OverlayTarget.apk")
+                .pushResourceFile(OVERLAY_APK, "/product/overlay/TestOverlay.apk")
+                .reboot()
+                .setOverlayEnabled(OVERLAY_PACKAGE, false);
+
+        assertResource(targetResource, "false");
+
+        // The overlay is not signed with the same signature as the target.
+        mPreparer.setOverlayEnabled(OVERLAY_PACKAGE, true);
+        assertResource(targetResource, "false");
+
+        // Replace the overlay with a version of the overlay that is signed with the same signature
+        // as the target.
+        mPreparer.pushResourceFile(OVERLAY_SIGNATURE_APK, "/product/overlay/TestOverlay.apk")
+                .reboot();
+
+        // The idmap should have been recreated with the signature policy fulfilled.
+        assertResource(targetResource, "true");
+
+        mPreparer.setOverlayEnabled(OVERLAY_PACKAGE, false);
+        assertResource(targetResource, "false");
+    }
 }
diff --git a/core/tests/overlaytests/remount/test-apps/Overlay/Android.bp b/core/tests/overlaytests/remount/test-apps/Overlay/Android.bp
index a1fdbfd..032a0cd 100644
--- a/core/tests/overlaytests/remount/test-apps/Overlay/Android.bp
+++ b/core/tests/overlaytests/remount/test-apps/Overlay/Android.bp
@@ -19,3 +19,9 @@
         "com.android.overlaytest.overlay",
     ],
 }
+
+android_test_helper_app {
+    name: "OverlayRemountedTest_Overlay_SameCert",
+    certificate: ":rro-remounted-test-a",
+    sdk_version: "current",
+}
\ No newline at end of file
diff --git a/core/tests/overlaytests/remount/test-apps/Overlay/res/values/values.xml b/core/tests/overlaytests/remount/test-apps/Overlay/res/values/values.xml
index 675e44f..927d37f 100644
--- a/core/tests/overlaytests/remount/test-apps/Overlay/res/values/values.xml
+++ b/core/tests/overlaytests/remount/test-apps/Overlay/res/values/values.xml
@@ -17,4 +17,5 @@
 
 <resources>
     <bool name="target_overlaid">true</bool>
+    <bool name="signature_policy_overlaid">true</bool>
 </resources>
diff --git a/core/tests/overlaytests/remount/test-apps/Target/Android.bp b/core/tests/overlaytests/remount/test-apps/Target/Android.bp
index 19947b1..e4b4eaa 100644
--- a/core/tests/overlaytests/remount/test-apps/Target/Android.bp
+++ b/core/tests/overlaytests/remount/test-apps/Target/Android.bp
@@ -15,6 +15,7 @@
 android_test_helper_app {
     name: "OverlayRemountedTest_Target",
     sdk_version: "test_current",
+    certificate: ":rro-remounted-test-a",
     apex_available: [
         "com.android.overlaytest.overlaid",
     ],
@@ -23,6 +24,7 @@
 
 android_test_helper_app {
     name: "OverlayRemountedTest_TargetUpgrade",
+    certificate: ":rro-remounted-test-a",
     resource_dirs: ["res_upgrade"],
     sdk_version: "test_current",
 }
diff --git a/core/tests/overlaytests/remount/test-apps/Target/res/values/overlayable.xml b/core/tests/overlaytests/remount/test-apps/Target/res/values/overlayable.xml
index 4aa5bce..79c9a67 100644
--- a/core/tests/overlaytests/remount/test-apps/Target/res/values/overlayable.xml
+++ b/core/tests/overlaytests/remount/test-apps/Target/res/values/overlayable.xml
@@ -20,5 +20,8 @@
         <policy type="public">
             <item type="bool" name="target_overlaid" />
         </policy>
+        <policy type="signature">
+            <item type="bool" name="signature_policy_overlaid" />
+        </policy>
     </overlayable>
 </resources>
diff --git a/core/tests/overlaytests/remount/test-apps/Target/res/values/values.xml b/core/tests/overlaytests/remount/test-apps/Target/res/values/values.xml
index 76253a9..64a1683 100644
--- a/core/tests/overlaytests/remount/test-apps/Target/res/values/values.xml
+++ b/core/tests/overlaytests/remount/test-apps/Target/res/values/values.xml
@@ -23,4 +23,6 @@
     <bool name="target_overlaid">false</bool>
     <public type="bool" name="target_overlaid" id="0x7f010000" />
     <bool name="target_reference">@bool/target_overlaid</bool>
+
+    <bool name="signature_policy_overlaid">false</bool>
 </resources>
diff --git a/core/tests/overlaytests/remount/test-apps/certs/Android.bp b/core/tests/overlaytests/remount/test-apps/certs/Android.bp
new file mode 100644
index 0000000..06114ef
--- /dev/null
+++ b/core/tests/overlaytests/remount/test-apps/certs/Android.bp
@@ -0,0 +1,19 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// development/tools/make_key rro-remounted-test-a '/CN=rro_test_a'
+android_app_certificate {
+    name: "rro-remounted-test-a",
+    certificate: "rro-remounted-test-a",
+}
diff --git a/core/tests/overlaytests/remount/test-apps/certs/rro-remounted-test-a.pk8 b/core/tests/overlaytests/remount/test-apps/certs/rro-remounted-test-a.pk8
new file mode 100644
index 0000000..aa6cc97
--- /dev/null
+++ b/core/tests/overlaytests/remount/test-apps/certs/rro-remounted-test-a.pk8
Binary files differ
diff --git a/core/tests/overlaytests/remount/test-apps/certs/rro-remounted-test-a.x509.pem b/core/tests/overlaytests/remount/test-apps/certs/rro-remounted-test-a.x509.pem
new file mode 100644
index 0000000..be491c7
--- /dev/null
+++ b/core/tests/overlaytests/remount/test-apps/certs/rro-remounted-test-a.x509.pem
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDCzCCAfOgAwIBAgIUfphI+C6W6V6RomsP7+CW5dO5cGcwDQYJKoZIhvcNAQEL
+BQAwFTETMBEGA1UEAwwKcnJvX3Rlc3RfYTAeFw0yMDA1MTQxNjM4MDBaFw00NzA5
+MzAxNjM4MDBaMBUxEzARBgNVBAMMCnJyb190ZXN0X2EwggEiMA0GCSqGSIb3DQEB
+AQUAA4IBDwAwggEKAoIBAQCvXM8tcqQFwH7iQG6+8mAx2ADhwbtq+8Rcmiz7wviW
+Yf/eFDRuvZ/ma6lxeVJ7mcbF7A5rinEKdgN5hlW2UlbTmuX5YOiXnX3Y2J5t+8Pi
+aq787IvWxkawwkj0Oy1Hk01Z4w3HTYntYqi36bq4QyNpwh515VqgvEyCHT7IPtQi
+XjfwcTW0thUlSDyDkgxq9NxNEJgaHHOamKkeMCO8CkBWkhlcPXvjcM8DPFmyzDI9
+Czv8IYFZQbcG/N2GPH9hSteMnuC+zyoMio0V/VRctQGlAA8ATsheBkng0zcNRu9Z
+GIavk5AaClmBFTeQx01j3HFSO8UDdDJ5Hk8uDTqecPLpAgMBAAGjUzBRMB0GA1Ud
+DgQWBBSPbIdzSkPbzltj3qIS13LNDiyIiDAfBgNVHSMEGDAWgBSPbIdzSkPbzltj
+3qIS13LNDiyIiDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCH
+QvvMGyMvVJaWMEJwVdUnszdXiAlUtDd/2HpdGOxW6xVVAvveP8hJ71gWFQ7Qs3Mr
+3jxclbC37qVAPiQb8kkD8qUgoQYMC43asif6Jn65OU1QkDRF3bFHP+rZVSPEwtvl
+YMbOzHPOLr7HESwlM7TB6EoZ4oOso++jTYI/OSif1MOKOMbOt4X/DE/PXf81ayFs
+uRjpocBqnLwOourABMcaKbA92jB0LRTtgv5ngOJ3+5P1cTiHktFbnqVWa8/A3uSA
+dNR5dpOUjH+nCHTwPl64b7R70PgDxnoqMs0xI7VtJovXor64OZy9P8WTdurz5V/z
+k2IVSi032bf0aTxamvqV
+-----END CERTIFICATE-----
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index aa4e9f2..9b503eb 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -378,6 +378,9 @@
         <permission name="android.permission.SET_WALLPAPER" />
         <permission name="android.permission.SET_WALLPAPER_COMPONENT" />
         <permission name="android.permission.REQUEST_NOTIFICATION_ASSISTANT_SERVICE" />
+        <!-- Permissions required for Incremental CTS tests -->
+        <permission name="com.android.permission.USE_INSTALLER_V2"/>
+        <permission name="android.permission.LOADER_USAGE_STATS"/>
         <!-- Permission required to test system only camera devices. -->
         <permission name="android.permission.SYSTEM_CAMERA" />
         <!-- Permission required to test ExplicitHealthCheckServiceImpl. -->
diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h
index 2bfc7fc..21be81c 100644
--- a/libs/androidfw/include/androidfw/ResourceTypes.h
+++ b/libs/androidfw/include/androidfw/ResourceTypes.h
@@ -41,7 +41,7 @@
 namespace android {
 
 constexpr const static uint32_t kIdmapMagic = 0x504D4449u;
-constexpr const static uint32_t kIdmapCurrentVersion = 0x00000003u;
+constexpr const static uint32_t kIdmapCurrentVersion = 0x00000004u;
 
 /**
  * In C++11, char16_t is defined as *at least* 16 bits. We do a lot of
@@ -1746,6 +1746,9 @@
   uint32_t target_crc32;
   uint32_t overlay_crc32;
 
+  uint32_t fulfilled_policies;
+  uint8_t enforce_overlayable;
+
   uint8_t target_path[256];
   uint8_t overlay_path[256];
 
diff --git a/libs/androidfw/tests/data/overlay/overlay.apk b/libs/androidfw/tests/data/overlay/overlay.apk
index 62e9866..f1ed592 100644
--- a/libs/androidfw/tests/data/overlay/overlay.apk
+++ b/libs/androidfw/tests/data/overlay/overlay.apk
Binary files differ
diff --git a/libs/androidfw/tests/data/overlay/overlay.idmap b/libs/androidfw/tests/data/overlay/overlay.idmap
index 3759ed6..29c5eb6 100644
--- a/libs/androidfw/tests/data/overlay/overlay.idmap
+++ b/libs/androidfw/tests/data/overlay/overlay.idmap
Binary files differ
diff --git a/media/java/android/media/MediaCas.java b/media/java/android/media/MediaCas.java
index c652628..590def4 100644
--- a/media/java/android/media/MediaCas.java
+++ b/media/java/android/media/MediaCas.java
@@ -20,6 +20,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.TestApi;
+import android.app.ActivityManager;
 import android.content.Context;
 import android.hardware.cas.V1_0.HidlCasPluginDescriptor;
 import android.hardware.cas.V1_0.ICas;
@@ -43,6 +44,8 @@
 import android.util.Log;
 import android.util.Singleton;
 
+import com.android.internal.util.FrameworkStatsLog;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
@@ -122,6 +125,7 @@
     private String mTvInputServiceSessionId;
     private int mClientId;
     private int mCasSystemId;
+    private int mUserId;
     private TunerResourceManager mTunerResourceManager = null;
     private final Map<Session, Integer> mSessionMap = new HashMap<>();
 
@@ -673,6 +677,8 @@
      */
     public MediaCas(int CA_system_id) throws UnsupportedCasException {
         try {
+            mCasSystemId = CA_system_id;
+            mUserId = ActivityManager.getCurrentUser();
             IMediaCasService service = getService();
             android.hardware.cas.V1_2.IMediaCasService serviceV12 =
                     android.hardware.cas.V1_2.IMediaCasService.castFrom(service);
@@ -721,7 +727,6 @@
         this(casSystemId);
 
         Objects.requireNonNull(context, "context must not be null");
-        mCasSystemId = casSystemId;
         mTunerResourceManager = (TunerResourceManager)
                 context.getSystemService(Context.TV_TUNER_RESOURCE_MGR_SERVICE);
         if (mTunerResourceManager != null) {
@@ -925,10 +930,18 @@
             mICas.openSession(cb);
             MediaCasException.throwExceptionIfNeeded(cb.mStatus);
             addSessionToResourceMap(cb.mSession, sessionResourceHandle);
+            Log.d(TAG, "Write Stats Log for succeed to Open Session.");
+            FrameworkStatsLog
+                    .write(FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS, mUserId, mCasSystemId,
+                        FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS__STATE__SUCCEEDED);
             return cb.mSession;
         } catch (RemoteException e) {
             cleanupAndRethrowIllegalState();
         }
+        Log.d(TAG, "Write Stats Log for fail to Open Session.");
+        FrameworkStatsLog
+                .write(FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS, mUserId, mCasSystemId,
+                    FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS__STATE__FAILED);
         return null;
     }
 
@@ -964,10 +977,18 @@
             mICasV12.openSession_1_2(sessionUsage, scramblingMode, cb);
             MediaCasException.throwExceptionIfNeeded(cb.mStatus);
             addSessionToResourceMap(cb.mSession, sessionResourceHandle);
+            Log.d(TAG, "Write Stats Log for succeed to Open Session.");
+            FrameworkStatsLog
+                    .write(FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS, mUserId, mCasSystemId,
+                        FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS__STATE__SUCCEEDED);
             return cb.mSession;
         } catch (RemoteException e) {
             cleanupAndRethrowIllegalState();
         }
+        Log.d(TAG, "Write Stats Log for fail to Open Session.");
+        FrameworkStatsLog
+                .write(FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS, mUserId, mCasSystemId,
+                    FrameworkStatsLog.TV_CAS_SESSION_OPEN_STATUS__STATE__FAILED);
         return null;
     }
 
diff --git a/media/java/android/media/tv/TvRecordingClient.java b/media/java/android/media/tv/TvRecordingClient.java
index 8ae98ae..23fadac 100644
--- a/media/java/android/media/tv/TvRecordingClient.java
+++ b/media/java/android/media/tv/TvRecordingClient.java
@@ -146,6 +146,8 @@
         mPendingAppPrivateCommands.clear();
         if (mSession != null) {
             mSession.release();
+            mIsTuned = false;
+            mIsRecordingStarted = false;
             mSession = null;
         }
     }
diff --git a/media/java/android/media/tv/tuner/dvr/DvrPlayback.java b/media/java/android/media/tv/tuner/dvr/DvrPlayback.java
index 68071b0..bb00bb3 100644
--- a/media/java/android/media/tv/tuner/dvr/DvrPlayback.java
+++ b/media/java/android/media/tv/tuner/dvr/DvrPlayback.java
@@ -20,12 +20,16 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
+import android.app.ActivityManager;
 import android.hardware.tv.tuner.V1_0.Constants;
 import android.media.tv.tuner.Tuner;
 import android.media.tv.tuner.Tuner.Result;
 import android.media.tv.tuner.TunerUtils;
 import android.media.tv.tuner.filter.Filter;
 import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import com.android.internal.util.FrameworkStatsLog;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -72,9 +76,15 @@
      */
     public static final int PLAYBACK_STATUS_FULL = Constants.PlaybackStatus.SPACE_FULL;
 
+    private static final String TAG = "TvTunerPlayback";
+
     private long mNativeContext;
     private OnPlaybackStatusChangedListener mListener;
     private Executor mExecutor;
+    private int mUserId;
+    private static int sInstantId = 0;
+    private int mSegmentId = 0;
+    private int mUnderflow;
 
     private native int nativeAttachFilter(Filter filter);
     private native int nativeDetachFilter(Filter filter);
@@ -88,6 +98,9 @@
     private native long nativeRead(byte[] bytes, long offset, long size);
 
     private DvrPlayback() {
+        mUserId = ActivityManager.getCurrentUser();
+        mSegmentId = (sInstantId & 0x0000ffff) << 16;
+        sInstantId++;
     }
 
     /** @hide */
@@ -98,6 +111,9 @@
     }
 
     private void onPlaybackStatusChanged(int status) {
+        if (status == PLAYBACK_STATUS_EMPTY) {
+            mUnderflow++;
+        }
         if (mExecutor != null && mListener != null) {
             mExecutor.execute(() -> mListener.onPlaybackStatusChanged(status));
         }
@@ -154,6 +170,13 @@
      */
     @Result
     public int start() {
+        mSegmentId =  (mSegmentId & 0xffff0000) | (((mSegmentId & 0x0000ffff) + 1) & 0x0000ffff);
+        mUnderflow = 0;
+        Log.d(TAG, "Write Stats Log for Playback.");
+        FrameworkStatsLog
+                .write(FrameworkStatsLog.TV_TUNER_DVR_STATUS, mUserId,
+                    FrameworkStatsLog.TV_TUNER_DVR_STATUS__TYPE__PLAYBACK,
+                    FrameworkStatsLog.TV_TUNER_DVR_STATUS__STATE__STARTED, mSegmentId, 0);
         return nativeStartDvr();
     }
 
@@ -167,6 +190,11 @@
      */
     @Result
     public int stop() {
+        Log.d(TAG, "Write Stats Log for Playback.");
+        FrameworkStatsLog
+                .write(FrameworkStatsLog.TV_TUNER_DVR_STATUS, mUserId,
+                    FrameworkStatsLog.TV_TUNER_DVR_STATUS__TYPE__PLAYBACK,
+                    FrameworkStatsLog.TV_TUNER_DVR_STATUS__STATE__STOPPED, mSegmentId, mUnderflow);
         return nativeStopDvr();
     }
 
diff --git a/media/java/android/media/tv/tuner/dvr/DvrRecorder.java b/media/java/android/media/tv/tuner/dvr/DvrRecorder.java
index 198bd0f..8871167 100644
--- a/media/java/android/media/tv/tuner/dvr/DvrRecorder.java
+++ b/media/java/android/media/tv/tuner/dvr/DvrRecorder.java
@@ -19,14 +19,19 @@
 import android.annotation.BytesLong;
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
+import android.app.ActivityManager;
 import android.media.tv.tuner.Tuner;
 import android.media.tv.tuner.Tuner.Result;
 import android.media.tv.tuner.TunerUtils;
 import android.media.tv.tuner.filter.Filter;
 import android.os.ParcelFileDescriptor;
+import android.util.Log;
+
+import com.android.internal.util.FrameworkStatsLog;
 
 import java.util.concurrent.Executor;
 
+
 /**
  * Digital Video Record (DVR) recorder class which provides record control on Demux's output buffer.
  *
@@ -34,9 +39,14 @@
  */
 @SystemApi
 public class DvrRecorder implements AutoCloseable {
+    private static final String TAG = "TvTunerRecord";
     private long mNativeContext;
     private OnRecordStatusChangedListener mListener;
     private Executor mExecutor;
+    private int mUserId;
+    private static int sInstantId = 0;
+    private int mSegmentId = 0;
+    private int mOverflow;
 
     private native int nativeAttachFilter(Filter filter);
     private native int nativeDetachFilter(Filter filter);
@@ -50,6 +60,9 @@
     private native long nativeWrite(byte[] bytes, long offset, long size);
 
     private DvrRecorder() {
+        mUserId = ActivityManager.getCurrentUser();
+        mSegmentId = (sInstantId & 0x0000ffff) << 16;
+        sInstantId++;
     }
 
     /** @hide */
@@ -60,6 +73,9 @@
     }
 
     private void onRecordStatusChanged(int status) {
+        if (status == Filter.STATUS_OVERFLOW) {
+            mOverflow++;
+        }
         if (mExecutor != null && mListener != null) {
             mExecutor.execute(() -> mListener.onRecordStatusChanged(status));
         }
@@ -112,6 +128,13 @@
      */
     @Result
     public int start() {
+        mSegmentId =  (mSegmentId & 0xffff0000) | (((mSegmentId & 0x0000ffff) + 1) & 0x0000ffff);
+        mOverflow = 0;
+        Log.d(TAG, "Write Stats Log for Record.");
+        FrameworkStatsLog
+                .write(FrameworkStatsLog.TV_TUNER_DVR_STATUS, mUserId,
+                    FrameworkStatsLog.TV_TUNER_DVR_STATUS__TYPE__RECORD,
+                    FrameworkStatsLog.TV_TUNER_DVR_STATUS__STATE__STARTED, mSegmentId, 0);
         return nativeStartDvr();
     }
 
@@ -124,6 +147,11 @@
      */
     @Result
     public int stop() {
+        Log.d(TAG, "Write Stats Log for Playback.");
+        FrameworkStatsLog
+                .write(FrameworkStatsLog.TV_TUNER_DVR_STATUS, mUserId,
+                    FrameworkStatsLog.TV_TUNER_DVR_STATUS__TYPE__RECORD,
+                    FrameworkStatsLog.TV_TUNER_DVR_STATUS__STATE__STOPPED, mSegmentId, mOverflow);
         return nativeStopDvr();
     }
 
diff --git a/packages/SettingsLib/AdaptiveIcon/res/values/colors.xml b/packages/SettingsLib/AdaptiveIcon/res/values/colors.xml
index 76d106a..8f5b2ed 100644
--- a/packages/SettingsLib/AdaptiveIcon/res/values/colors.xml
+++ b/packages/SettingsLib/AdaptiveIcon/res/values/colors.xml
@@ -18,4 +18,8 @@
     <color name="homepage_generic_icon_background">#1A73E8</color>
 
     <color name="bt_outline_color">#1f000000</color> <!-- icon outline color -->
+
+    <color name="advanced_outline_color">#BDC1C6</color> <!-- icon outline color -->
+
+    <color name="advanced_icon_color">#3C4043</color>
 </resources>
diff --git a/packages/SettingsLib/AdaptiveIcon/res/values/dimens.xml b/packages/SettingsLib/AdaptiveIcon/res/values/dimens.xml
index 7f5b58c..8f6e358 100644
--- a/packages/SettingsLib/AdaptiveIcon/res/values/dimens.xml
+++ b/packages/SettingsLib/AdaptiveIcon/res/values/dimens.xml
@@ -18,6 +18,8 @@
 <resources>
     <!-- Dashboard foreground image inset (from background edge to foreground edge) -->
     <dimen name="dashboard_tile_foreground_image_inset">6dp</dimen>
+    <!-- Advanced dashboard foreground image inset (from background edge to foreground edge) -->
+    <dimen name="advanced_dashboard_tile_foreground_image_inset">9dp</dimen>
 
     <!-- Stroke size of adaptive outline -->
     <dimen name="adaptive_outline_stroke">1dp</dimen>
diff --git a/packages/SettingsLib/AdaptiveIcon/src/com/android/settingslib/widget/AdaptiveOutlineDrawable.java b/packages/SettingsLib/AdaptiveIcon/src/com/android/settingslib/widget/AdaptiveOutlineDrawable.java
index 1c65bc2..4438893 100644
--- a/packages/SettingsLib/AdaptiveIcon/src/com/android/settingslib/widget/AdaptiveOutlineDrawable.java
+++ b/packages/SettingsLib/AdaptiveIcon/src/com/android/settingslib/widget/AdaptiveOutlineDrawable.java
@@ -16,6 +16,10 @@
 
 package com.android.settingslib.widget;
 
+import static com.android.settingslib.widget.AdaptiveOutlineDrawable.AdaptiveOutlineIconType.TYPE_ADVANCED;
+import static com.android.settingslib.widget.AdaptiveOutlineDrawable.AdaptiveOutlineIconType.TYPE_DEFAULT;
+
+import android.annotation.ColorInt;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
@@ -27,35 +31,90 @@
 import android.graphics.drawable.DrawableWrapper;
 import android.util.PathParser;
 
+import androidx.annotation.IntDef;
 import androidx.annotation.VisibleForTesting;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
 /**
  * Adaptive outline drawable with white plain background color and black outline
  */
 public class AdaptiveOutlineDrawable extends DrawableWrapper {
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({TYPE_DEFAULT, TYPE_ADVANCED})
+    public @interface AdaptiveOutlineIconType {
+        int TYPE_DEFAULT = 0;
+        int TYPE_ADVANCED = 1;
+    }
+
     @VisibleForTesting
-    final Paint mOutlinePaint;
+    Paint mOutlinePaint;
     private Path mPath;
-    private final int mInsetPx;
-    private final Bitmap mBitmap;
+    private int mInsetPx;
+    private int mStrokeWidth;
+    private Bitmap mBitmap;
+    private int mType;
 
     public AdaptiveOutlineDrawable(Resources resources, Bitmap bitmap) {
         super(new AdaptiveIconShapeDrawable(resources));
 
+        init(resources, bitmap, TYPE_DEFAULT);
+    }
+
+    public AdaptiveOutlineDrawable(Resources resources, Bitmap bitmap,
+            @AdaptiveOutlineIconType int type) {
+        super(new AdaptiveIconShapeDrawable(resources));
+
+        init(resources, bitmap, type);
+    }
+
+    private void init(Resources resources, Bitmap bitmap,
+            @AdaptiveOutlineIconType int type) {
+        mType = type;
         getDrawable().setTint(Color.WHITE);
         mPath = new Path(PathParser.createPathFromPathData(
                 resources.getString(com.android.internal.R.string.config_icon_mask)));
+        mStrokeWidth = resources.getDimensionPixelSize(R.dimen.adaptive_outline_stroke);
         mOutlinePaint = new Paint();
-        mOutlinePaint.setColor(resources.getColor(R.color.bt_outline_color, null));
+        mOutlinePaint.setColor(getColor(resources, type));
         mOutlinePaint.setStyle(Paint.Style.STROKE);
-        mOutlinePaint.setStrokeWidth(resources.getDimension(R.dimen.adaptive_outline_stroke));
+        mOutlinePaint.setStrokeWidth(mStrokeWidth);
         mOutlinePaint.setAntiAlias(true);
 
-        mInsetPx = resources
-                .getDimensionPixelSize(R.dimen.dashboard_tile_foreground_image_inset);
+        mInsetPx = getDimensionPixelSize(resources, type);
         mBitmap = bitmap;
     }
 
+    private @ColorInt int getColor(Resources resources, @AdaptiveOutlineIconType int type) {
+        int resId;
+        switch (type) {
+            case TYPE_ADVANCED:
+                resId = R.color.advanced_outline_color;
+                break;
+            case TYPE_DEFAULT:
+            default:
+                resId = R.color.bt_outline_color;
+                break;
+        }
+        return resources.getColor(resId, /* theme */ null);
+    }
+
+    private int getDimensionPixelSize(Resources resources, @AdaptiveOutlineIconType int type) {
+        int resId;
+        switch (type) {
+            case TYPE_ADVANCED:
+                resId = R.dimen.advanced_dashboard_tile_foreground_image_inset;
+                break;
+            case TYPE_DEFAULT:
+            default:
+                resId = R.dimen.dashboard_tile_foreground_image_inset;
+                break;
+        }
+        return resources.getDimensionPixelSize(resId);
+    }
+
     @Override
     public void draw(Canvas canvas) {
         super.draw(canvas);
@@ -68,7 +127,12 @@
         final int count = canvas.save();
         canvas.scale(scaleX, scaleY);
         // Draw outline
-        canvas.drawPath(mPath, mOutlinePaint);
+        if (mType == TYPE_DEFAULT) {
+            canvas.drawPath(mPath, mOutlinePaint);
+        } else {
+            canvas.drawCircle(2 * mInsetPx, 2 * mInsetPx, 2 * mInsetPx - mStrokeWidth,
+                    mOutlinePaint);
+        }
         canvas.restoreToCount(count);
 
         // Draw the foreground icon
diff --git a/packages/SettingsLib/SearchWidget/res/values-fa/strings.xml b/packages/SettingsLib/SearchWidget/res/values-fa/strings.xml
index fa5f9bd..2c9aaa5 100644
--- a/packages/SettingsLib/SearchWidget/res/values-fa/strings.xml
+++ b/packages/SettingsLib/SearchWidget/res/values-fa/strings.xml
@@ -17,5 +17,5 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="search_menu" msgid="1914043873178389845">"جستجوی تنظیمات"</string>
+    <string name="search_menu" msgid="1914043873178389845">"تنظیمات جستجو"</string>
 </resources>
diff --git a/packages/SettingsLib/SearchWidget/res/values-tl/strings.xml b/packages/SettingsLib/SearchWidget/res/values-tl/strings.xml
index 111cf5a..14b7b2f 100644
--- a/packages/SettingsLib/SearchWidget/res/values-tl/strings.xml
+++ b/packages/SettingsLib/SearchWidget/res/values-tl/strings.xml
@@ -17,5 +17,5 @@
 
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="search_menu" msgid="1914043873178389845">"Mga setting ng paghahanap"</string>
+    <string name="search_menu" msgid="1914043873178389845">"Maghanap sa mga setting"</string>
 </resources>
diff --git a/packages/SettingsLib/res/values-bn/arrays.xml b/packages/SettingsLib/res/values-bn/arrays.xml
index a131a3b..b19cde4 100644
--- a/packages/SettingsLib/res/values-bn/arrays.xml
+++ b/packages/SettingsLib/res/values-bn/arrays.xml
@@ -40,7 +40,7 @@
     <item msgid="8339720953594087771">"<xliff:g id="NETWORK_NAME">%1$s</xliff:g> এর সাথে কানেক্ট হচ্ছে…"</item>
     <item msgid="3028983857109369308">"<xliff:g id="NETWORK_NAME">%1$s</xliff:g> দিয়ে যাচাইকরণ করা হচ্ছে..."</item>
     <item msgid="4287401332778341890">"<xliff:g id="NETWORK_NAME">%1$s</xliff:g> থেকে আইপি অ্যাড্রেস জানা হচ্ছে…"</item>
-    <item msgid="1043944043827424501">"<xliff:g id="NETWORK_NAME">%1$s</xliff:g> তে কানেক্ট হয়েছে"</item>
+    <item msgid="1043944043827424501">"<xliff:g id="NETWORK_NAME">%1$s</xliff:g>-এ কানেক্ট হয়েছে"</item>
     <item msgid="7445993821842009653">"স্থগিত করা হয়েছে"</item>
     <item msgid="1175040558087735707">"<xliff:g id="NETWORK_NAME">%1$s</xliff:g> থেকে ডিসকানেক্ট হচ্ছে…"</item>
     <item msgid="699832486578171722">"ডিসকানেক্ট করা হয়েছে"</item>
diff --git a/packages/SettingsLib/res/values-el/strings.xml b/packages/SettingsLib/res/values-el/strings.xml
index 8db0b7e..2644cb9 100644
--- a/packages/SettingsLib/res/values-el/strings.xml
+++ b/packages/SettingsLib/res/values-el/strings.xml
@@ -194,7 +194,7 @@
     <item msgid="581904787661470707">"Ταχύτατη"</item>
   </string-array>
     <string name="choose_profile" msgid="343803890897657450">"Επιλογή προφίλ"</string>
-    <string name="category_personal" msgid="6236798763159385225">"Προσωπικός"</string>
+    <string name="category_personal" msgid="6236798763159385225">"Προσωπικό"</string>
     <string name="category_work" msgid="4014193632325996115">"Εργασίας"</string>
     <string name="development_settings_title" msgid="140296922921597393">"Επιλογές για προγραμματιστές"</string>
     <string name="development_settings_enable" msgid="4285094651288242183">"Ενεργοποίηση επιλογών για προγραμματιστές"</string>
diff --git a/packages/SettingsLib/res/values-in/arrays.xml b/packages/SettingsLib/res/values-in/arrays.xml
index e73febc..d20bf38 100644
--- a/packages/SettingsLib/res/values-in/arrays.xml
+++ b/packages/SettingsLib/res/values-in/arrays.xml
@@ -40,7 +40,7 @@
     <item msgid="8339720953594087771">"Menyambung ke <xliff:g id="NETWORK_NAME">%1$s</xliff:g>…"</item>
     <item msgid="3028983857109369308">"Mengautentikasi dengan <xliff:g id="NETWORK_NAME">%1$s</xliff:g>…"</item>
     <item msgid="4287401332778341890">"Mendapatkan alamat IP dari <xliff:g id="NETWORK_NAME">%1$s</xliff:g>…"</item>
-    <item msgid="1043944043827424501">"Tersambung ke <xliff:g id="NETWORK_NAME">%1$s</xliff:g>"</item>
+    <item msgid="1043944043827424501">"Terhubung ke <xliff:g id="NETWORK_NAME">%1$s</xliff:g>"</item>
     <item msgid="7445993821842009653">"Ditangguhkan"</item>
     <item msgid="1175040558087735707">"Diputus dari <xliff:g id="NETWORK_NAME">%1$s</xliff:g>…"</item>
     <item msgid="699832486578171722">"Sambungan terputus"</item>
diff --git a/packages/SettingsLib/res/values/dimens.xml b/packages/SettingsLib/res/values/dimens.xml
index a5ec5e6..e552d78 100644
--- a/packages/SettingsLib/res/values/dimens.xml
+++ b/packages/SettingsLib/res/values/dimens.xml
@@ -94,4 +94,7 @@
 
     <!-- Define minimal size of the tap area -->
     <dimen name="min_tap_target_size">48dp</dimen>
+
+    <!-- Size of advanced icon -->
+    <dimen name="advanced_icon_size">18dp</dimen>
 </resources>
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
index 9d1b3cf..95e916b 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
@@ -1,5 +1,7 @@
 package com.android.settingslib.bluetooth;
 
+import static com.android.settingslib.widget.AdaptiveOutlineDrawable.AdaptiveOutlineIconType.TYPE_ADVANCED;
+
 import android.bluetooth.BluetoothClass;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothProfile;
@@ -7,6 +9,7 @@
 import android.content.Intent;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
+import android.graphics.Canvas;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
@@ -213,6 +216,46 @@
     }
 
     /**
+     * Build device icon with advanced outline
+     */
+    public static Drawable buildAdvancedDrawable(Context context, Drawable drawable) {
+        final int iconSize = context.getResources().getDimensionPixelSize(
+                R.dimen.advanced_icon_size);
+        final Resources resources = context.getResources();
+
+        Bitmap bitmap = null;
+        if (drawable instanceof BitmapDrawable) {
+            bitmap = ((BitmapDrawable) drawable).getBitmap();
+        } else {
+            final int width = drawable.getIntrinsicWidth();
+            final int height = drawable.getIntrinsicHeight();
+            bitmap = createBitmap(drawable,
+                    width > 0 ? width : 1,
+                    height > 0 ? height : 1);
+        }
+
+        if (bitmap != null) {
+            final Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, iconSize,
+                    iconSize, false);
+            bitmap.recycle();
+            return new AdaptiveOutlineDrawable(resources, resizedBitmap, TYPE_ADVANCED);
+        }
+
+        return drawable;
+    }
+
+    /**
+     * Creates a drawable with specified width and height.
+     */
+    public static Bitmap createBitmap(Drawable drawable, int width, int height) {
+        final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+        final Canvas canvas = new Canvas(bitmap);
+        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+        drawable.draw(canvas);
+        return bitmap;
+    }
+
+    /**
      * Get boolean Bluetooth metadata
      *
      * @param bluetoothDevice the BluetoothDevice to get metadata
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java
index 40d8048..00f94f5 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java
@@ -21,7 +21,6 @@
 import android.graphics.drawable.Drawable;
 import android.media.MediaRoute2Info;
 import android.media.MediaRouter2Manager;
-import android.util.Pair;
 
 import com.android.settingslib.R;
 import com.android.settingslib.bluetooth.BluetoothUtils;
@@ -57,13 +56,11 @@
 
     @Override
     public Drawable getIcon() {
-        final Pair<Drawable, String> pair = BluetoothUtils
-                .getBtRainbowDrawableWithDescription(mContext, mCachedDevice);
-        return isFastPairDevice()
-                ? pair.first
-                : BluetoothUtils.buildBtRainbowDrawable(mContext,
-                        mContext.getDrawable(R.drawable.ic_headphone),
-                        mCachedDevice.getAddress().hashCode());
+        final Drawable drawable = getIconWithoutBackground();
+        if (!isFastPairDevice()) {
+            setColorFilter(drawable);
+        }
+        return BluetoothUtils.buildAdvancedDrawable(mContext, drawable);
     }
 
     @Override
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java
index 8d6bc5c..ea71e52 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java
@@ -55,9 +55,9 @@
 
     @Override
     public Drawable getIcon() {
-        //TODO(b/120669861): Return remote device icon uri once api is ready.
-        return BluetoothUtils.buildBtRainbowDrawable(mContext,
-                mContext.getDrawable(getDrawableResId()), getId().hashCode());
+        final Drawable drawable = getIconWithoutBackground();
+        setColorFilter(drawable);
+        return BluetoothUtils.buildAdvancedDrawable(mContext, drawable);
     }
 
     @Override
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
index 317077b..126f9b9 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java
@@ -31,6 +31,9 @@
 import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET;
 
 import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
 import android.graphics.drawable.Drawable;
 import android.media.MediaRoute2Info;
 import android.media.MediaRouter2Manager;
@@ -39,6 +42,8 @@
 import androidx.annotation.IntDef;
 import androidx.annotation.VisibleForTesting;
 
+import com.android.settingslib.R;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
@@ -131,6 +136,14 @@
                 getId());
     }
 
+    void setColorFilter(Drawable drawable) {
+        final ColorStateList list =
+                mContext.getResources().getColorStateList(
+                        R.color.advanced_icon_color, mContext.getTheme());
+        drawable.setColorFilter(new PorterDuffColorFilter(list.getDefaultColor(),
+                PorterDuff.Mode.SRC_IN));
+    }
+
     /**
      * Get name from MediaDevice.
      *
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
index c43a512..b6c0b30 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java
@@ -85,8 +85,9 @@
 
     @Override
     public Drawable getIcon() {
-        return BluetoothUtils.buildBtRainbowDrawable(mContext,
-                mContext.getDrawable(getDrawableResId()), getId().hashCode());
+        final Drawable drawable = getIconWithoutBackground();
+        setColorFilter(drawable);
+        return BluetoothUtils.buildAdvancedDrawable(mContext, drawable);
     }
 
     @Override
@@ -105,7 +106,7 @@
             case TYPE_HDMI:
             case TYPE_WIRED_HEADSET:
             case TYPE_WIRED_HEADPHONES:
-                resId = com.android.internal.R.drawable.ic_bt_headphones_a2dp;
+                resId = R.drawable.ic_headphone;
                 break;
             case TYPE_BUILTIN_SPEAKER:
             default:
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java
index 421e5c0..00d1f76 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/PhoneMediaDeviceTest.java
@@ -79,13 +79,11 @@
     public void getDrawableResId_returnCorrectResId() {
         when(mInfo.getType()).thenReturn(TYPE_WIRED_HEADPHONES);
 
-        assertThat(mPhoneMediaDevice.getDrawableResId())
-                .isEqualTo(com.android.internal.R.drawable.ic_bt_headphones_a2dp);
+        assertThat(mPhoneMediaDevice.getDrawableResId()).isEqualTo(R.drawable.ic_headphone);
 
         when(mInfo.getType()).thenReturn(TYPE_WIRED_HEADSET);
 
-        assertThat(mPhoneMediaDevice.getDrawableResId())
-                .isEqualTo(com.android.internal.R.drawable.ic_bt_headphones_a2dp);
+        assertThat(mPhoneMediaDevice.getDrawableResId()).isEqualTo(R.drawable.ic_headphone);
 
         when(mInfo.getType()).thenReturn(TYPE_BUILTIN_SPEAKER);
 
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index fa87b62..eb7ad72 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -316,7 +316,6 @@
                     Settings.Global.KERNEL_CPU_THREAD_READER,
                     Settings.Global.LANG_ID_UPDATE_CONTENT_URL,
                     Settings.Global.LANG_ID_UPDATE_METADATA_URL,
-                    Settings.Global.LAST_ACTIVE_USER_ID,
                     Settings.Global.LOCATION_BACKGROUND_THROTTLE_INTERVAL_MS,
                     Settings.Global.LOCATION_BACKGROUND_THROTTLE_PROXIMITY_ALERT_INTERVAL_MS,
                     Settings.Global.LOCATION_BACKGROUND_THROTTLE_PACKAGE_WHITELIST,
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 44349c5..4e17062 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -201,6 +201,9 @@
 
     <uses-permission android:name="android.permission.MANAGE_APPOPS" />
 
+    <!-- Permission required for IncrementalLogCollectionTest -->
+    <uses-permission android:name="android.permission.LOADER_USAGE_STATS" />
+
     <!-- Permission required for storage tests - FuseDaemonHostTest -->
     <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
 
diff --git a/packages/Shell/res/layout/dialog_bugreport_info.xml b/packages/Shell/res/layout/dialog_bugreport_info.xml
index 4bd8711..ea24279 100644
--- a/packages/Shell/res/layout/dialog_bugreport_info.xml
+++ b/packages/Shell/res/layout/dialog_bugreport_info.xml
@@ -14,58 +14,63 @@
      limitations under the License.
 -->
 
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:orientation="vertical"
-    android:paddingTop="15dp"
-    android:paddingStart="24dp"
-    android:paddingEnd="24dp"
-    android:focusableInTouchMode="false"
-    android:focusable="false"
-    android:importantForAutofill="noExcludeDescendants"
-    android:layout_width="wrap_content"
+<ScrollView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
     android:layout_height="wrap_content">
-    <TextView
+    <LinearLayout
+        android:orientation="vertical"
+        android:paddingTop="15dp"
+        android:paddingStart="24dp"
+        android:paddingEnd="24dp"
         android:focusableInTouchMode="false"
         android:focusable="false"
-        android:inputType="textNoSuggestions"
+        android:importantForAutofill="noExcludeDescendants"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:text="@string/bugreport_info_name"/>
-    <EditText
-        android:id="@+id/name"
-        android:nextFocusDown="@+id/title"
-        android:maxLength="30"
-        android:singleLine="true"
-        android:inputType="textNoSuggestions"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"/>
-    <TextView
-        android:focusableInTouchMode="false"
-        android:focusable="false"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:text="@string/bugreport_info_title"/>
-    <EditText
-        android:id="@+id/title"
-        android:nextFocusUp="@+id/name"
-        android:nextFocusDown="@+id/description"
-        android:maxLength="80"
-        android:singleLine="true"
-        android:inputType="textAutoCorrect|textCapSentences"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"/>
-    <TextView
-        android:focusableInTouchMode="false"
-        android:focusable="false"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:editable="false"
-        android:text="@string/bugreport_info_description"/>
-    <EditText
-        android:id="@+id/description"
-        android:nextFocusUp="@+id/title"
-        android:singleLine="false"
-        android:inputType="textMultiLine|textAutoCorrect|textCapSentences"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"/>
-</LinearLayout>
+        android:layout_height="wrap_content">
+        <TextView
+            android:focusableInTouchMode="false"
+            android:focusable="false"
+            android:inputType="textNoSuggestions"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/bugreport_info_name"/>
+        <EditText
+            android:id="@+id/name"
+            android:nextFocusDown="@+id/title"
+            android:maxLength="30"
+            android:singleLine="true"
+            android:inputType="textNoSuggestions"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"/>
+        <TextView
+            android:focusableInTouchMode="false"
+            android:focusable="false"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="@string/bugreport_info_title"/>
+        <EditText
+            android:id="@+id/title"
+            android:nextFocusUp="@+id/name"
+            android:nextFocusDown="@+id/description"
+            android:maxLength="80"
+            android:singleLine="true"
+            android:inputType="textAutoCorrect|textCapSentences"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"/>
+        <TextView
+            android:focusableInTouchMode="false"
+            android:focusable="false"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:editable="false"
+            android:text="@string/bugreport_info_description"/>
+        <EditText
+            android:id="@+id/description"
+            android:nextFocusUp="@+id/title"
+            android:singleLine="false"
+            android:inputType="textMultiLine|textAutoCorrect|textCapSentences"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"/>
+    </LinearLayout>
+</ScrollView>
diff --git a/packages/SystemUI/res/drawable/dismiss_circle_background.xml b/packages/SystemUI/res/drawable/dismiss_circle_background.xml
index e311c52..7809c83 100644
--- a/packages/SystemUI/res/drawable/dismiss_circle_background.xml
+++ b/packages/SystemUI/res/drawable/dismiss_circle_background.xml
@@ -21,8 +21,8 @@
 
     <stroke
         android:width="1dp"
-        android:color="#66FFFFFF" />
+        android:color="#AAFFFFFF" />
 
-    <solid android:color="#B3000000" />
+    <solid android:color="#77000000" />
 
 </shape>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/dismiss_target_x.xml b/packages/SystemUI/res/drawable/dismiss_target_x.xml
deleted file mode 100644
index 3672eff..0000000
--- a/packages/SystemUI/res/drawable/dismiss_target_x.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ Copyright (C) 2020 The Android Open Source Project
-  ~
-  ~ Licensed under the Apache License, Version 2.0 (the "License");
-  ~ you may not use this file except in compliance with the License.
-  ~ You may obtain a copy of the License at
-  ~
-  ~      http://www.apache.org/licenses/LICENSE-2.0
-  ~
-  ~ Unless required by applicable law or agreed to in writing, software
-  ~ distributed under the License is distributed on an "AS IS" BASIS,
-  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  ~ See the License for the specific language governing permissions and
-  ~ limitations under the License.
-  -->
-
-<!-- 'X' icon. -->
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="24.0dp"
-        android:height="24.0dp"
-        android:viewportWidth="24.0"
-        android:viewportHeight="24.0">
-    <path
-        android:pathData="M19.000000,6.400000l-1.400000,-1.400000 -5.600000,5.600000 -5.600000,-5.600000 -1.400000,1.400000 5.600000,5.600000 -5.600000,5.600000 1.400000,1.400000 5.600000,-5.600000 5.600000,5.600000 1.400000,-1.400000 -5.600000,-5.600000z"
-        android:fillColor="#FFFFFFFF"
-        android:strokeColor="#FF000000"/>
-</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/floating_dismiss_gradient.xml b/packages/SystemUI/res/drawable/floating_dismiss_gradient.xml
new file mode 100644
index 0000000..8f7fb10
--- /dev/null
+++ b/packages/SystemUI/res/drawable/floating_dismiss_gradient.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<shape
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <gradient
+        android:angle="270"
+        android:startColor="#00000000"
+        android:endColor="#77000000"
+        android:type="linear" />
+</shape>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/floating_dismiss_gradient_transition.xml b/packages/SystemUI/res/drawable/floating_dismiss_gradient_transition.xml
new file mode 100644
index 0000000..6a0695e
--- /dev/null
+++ b/packages/SystemUI/res/drawable/floating_dismiss_gradient_transition.xml
@@ -0,0 +1,19 @@
+<?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.
+-->
+<transition xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:drawable="@color/transparent" />
+    <item android:drawable="@drawable/floating_dismiss_gradient" />
+</transition>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/ic_create_bubble.xml b/packages/SystemUI/res/drawable/ic_create_bubble.xml
index d58e9a3..4abbc81 100644
--- a/packages/SystemUI/res/drawable/ic_create_bubble.xml
+++ b/packages/SystemUI/res/drawable/ic_create_bubble.xml
@@ -15,11 +15,11 @@
     limitations under the License.
 -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="24dp"
-    android:height="24dp"
+    android:width="20dp"
+    android:height="20dp"
     android:viewportWidth="24"
     android:viewportHeight="24">
   <path
       android:fillColor="#FF000000"
-      android:pathData="M22,12C22,12 22,12 22,12C22,12 22,12 22,12c0,0.56 -0.06,1.1 -0.15,1.64l-1.97,-0.33c0.15,-0.91 0.15,-1.84 -0.02,-2.75c-0.01,-0.03 -0.01,-0.07 -0.02,-0.1c-0.03,-0.18 -0.08,-0.36 -0.13,-0.54c-0.02,-0.08 -0.04,-0.16 -0.06,-0.24c-0.04,-0.14 -0.09,-0.27 -0.14,-0.41c-0.04,-0.12 -0.08,-0.24 -0.13,-0.35c-0.04,-0.09 -0.08,-0.18 -0.13,-0.27c-0.07,-0.15 -0.14,-0.3 -0.22,-0.45c-0.03,-0.05 -0.06,-0.09 -0.08,-0.14c-0.72,-1.26 -1.77,-2.31 -3.03,-3.03c-0.05,-0.03 -0.09,-0.06 -0.14,-0.08c-0.15,-0.08 -0.3,-0.15 -0.45,-0.22c-0.09,-0.04 -0.18,-0.09 -0.27,-0.13c-0.11,-0.05 -0.23,-0.09 -0.35,-0.13c-0.14,-0.05 -0.27,-0.1 -0.41,-0.14c-0.08,-0.02 -0.16,-0.04 -0.23,-0.06c-0.18,-0.05 -0.36,-0.1 -0.54,-0.13c-0.03,-0.01 -0.07,-0.01 -0.1,-0.01c-0.95,-0.17 -1.93,-0.17 -2.88,0c-0.03,0.01 -0.07,0.01 -0.1,0.01c-0.18,0.04 -0.36,0.08 -0.54,0.13C9.85,4.3 9.77,4.32 9.69,4.34C9.55,4.38 9.42,4.44 9.28,4.49C9.17,4.53 9.05,4.57 8.93,4.61C8.84,4.65 8.75,4.7 8.66,4.74c-0.15,0.07 -0.3,0.14 -0.45,0.22C8.16,4.98 8.12,5.01 8.07,5.04C5.64,6.42 4,9.02 4,12c0,2.74 1.39,5.16 3.49,6.6c0.01,0.01 0.03,0.02 0.04,0.03c0.16,0.11 0.33,0.2 0.49,0.3c0.06,0.04 0.12,0.08 0.19,0.11c0.13,0.07 0.27,0.13 0.4,0.19c0.11,0.05 0.21,0.1 0.32,0.15c0.1,0.04 0.2,0.07 0.29,0.11c0.15,0.06 0.31,0.11 0.46,0.16c0.05,0.02 0.11,0.03 0.17,0.04c1.11,0.31 2.27,0.35 3.4,0.18l0.35,1.98c-0.54,0.09 -1.08,0.14 -1.62,0.14V22c-0.65,0 -1.28,-0.07 -1.9,-0.19c-0.01,0 -0.01,0 -0.02,0c-0.25,-0.05 -0.49,-0.11 -0.73,-0.18c-0.08,-0.02 -0.16,-0.04 -0.23,-0.06c-0.19,-0.06 -0.37,-0.13 -0.55,-0.19c-0.13,-0.05 -0.26,-0.09 -0.39,-0.14c-0.13,-0.05 -0.25,-0.12 -0.38,-0.18c-0.18,-0.08 -0.35,-0.16 -0.53,-0.25c-0.07,-0.04 -0.14,-0.08 -0.21,-0.13c-0.22,-0.12 -0.43,-0.25 -0.64,-0.39c-0.01,-0.01 -0.02,-0.02 -0.04,-0.03c-0.51,-0.35 -1,-0.74 -1.45,-1.2l0,0C3.12,17.26 2,14.76 2,12c0,-2.76 1.12,-5.26 2.93,-7.07l0,0c0.45,-0.45 0.93,-0.84 1.44,-1.19C6.39,3.73 6.4,3.72 6.42,3.71c0.2,-0.14 0.41,-0.26 0.62,-0.38c0.08,-0.05 0.15,-0.09 0.23,-0.14c0.17,-0.09 0.33,-0.16 0.5,-0.24c0.13,-0.06 0.27,-0.13 0.4,-0.19C8.3,2.71 8.42,2.67 8.55,2.63c0.19,-0.07 0.38,-0.14 0.58,-0.2c0.07,-0.02 0.14,-0.03 0.21,-0.05C10.18,2.14 11.07,2 12,2c0.65,0 1.29,0.07 1.91,0.19c0,0 0,0 0,0c0.25,0.05 0.5,0.11 0.75,0.18c0.07,0.02 0.14,0.03 0.22,0.06c0.19,0.06 0.38,0.13 0.57,0.2c0.12,0.05 0.25,0.09 0.37,0.14c0.14,0.06 0.27,0.12 0.4,0.18c0.17,0.08 0.34,0.16 0.51,0.25c0.08,0.04 0.15,0.09 0.23,0.14c0.21,0.12 0.42,0.24 0.62,0.38c0.01,0.01 0.03,0.02 0.04,0.03c0.51,0.35 0.99,0.74 1.45,1.19c0.24,0.24 0.47,0.49 0.68,0.75c0.04,0.04 0.06,0.09 0.1,0.13c0.17,0.22 0.34,0.45 0.5,0.68c0.01,0.01 0.02,0.03 0.03,0.04c0.69,1.05 1.17,2.21 1.42,3.44c0,0 0,0.01 0,0.01c0.06,0.29 0.1,0.58 0.13,0.87c0.01,0.04 0.01,0.09 0.02,0.13C21.98,11.32 22,11.66 22,12zM18.5,15c-1.93,0 -3.5,1.57 -3.5,3.5s1.57,3.5 3.5,3.5s3.5,-1.57 3.5,-3.5S20.43,15 18.5,15z"/>
-</vector>
\ No newline at end of file
+      android:pathData="M23,5v8h-2V5H3v14h10v2v0H3c-1.1,0 -2,-0.9 -2,-2V5c0,-1.1 0.9,-2 2,-2h18C22.1,3 23,3.9 23,5zM10,8v2.59L5.71,6.29L4.29,7.71L8.59,12H6v2h6V8H10zM19,15c-1.66,0 -3,1.34 -3,3s1.34,3 3,3s3,-1.34 3,-3S20.66,15 19,15z"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_music_note.xml b/packages/SystemUI/res/drawable/ic_music_note.xml
new file mode 100644
index 0000000..30959a8
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_music_note.xml
@@ -0,0 +1,24 @@
+<!--
+  ~ 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.
+  -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24"
+        android:viewportHeight="24">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M12,3v10.55c-0.59,-0.34 -1.27,-0.55 -2,-0.55 -2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4V7h4V3h-6z"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_stop_bubble.xml b/packages/SystemUI/res/drawable/ic_stop_bubble.xml
index 11bc741..6cf67a7 100644
--- a/packages/SystemUI/res/drawable/ic_stop_bubble.xml
+++ b/packages/SystemUI/res/drawable/ic_stop_bubble.xml
@@ -15,11 +15,11 @@
     limitations under the License.
 -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="24dp"
-    android:height="24dp"
+    android:width="20dp"
+    android:height="20dp"
     android:viewportWidth="24"
     android:viewportHeight="24">
   <path
       android:fillColor="#FF000000"
-      android:pathData="M21.98,18.32l-3.3,-3.3C20.47,15.11 21.89,16.53 21.98,18.32zM8.66,4.74C8.75,4.7 8.84,4.65 8.93,4.61c0.11,-0.05 0.23,-0.09 0.35,-0.13c0.14,-0.05 0.27,-0.1 0.41,-0.14C9.77,4.32 9.85,4.3 9.92,4.28c0.18,-0.05 0.36,-0.1 0.54,-0.13c0.03,-0.01 0.07,-0.01 0.1,-0.01c0.95,-0.17 1.93,-0.17 2.88,0c0.03,0.01 0.07,0.01 0.1,0.01c0.18,0.04 0.36,0.08 0.54,0.13c0.08,0.02 0.16,0.04 0.23,0.06c0.14,0.04 0.27,0.09 0.41,0.14c0.12,0.04 0.23,0.08 0.35,0.13c0.09,0.04 0.18,0.09 0.27,0.13c0.15,0.07 0.3,0.14 0.45,0.22c0.05,0.03 0.09,0.06 0.14,0.08c1.26,0.72 2.31,1.77 3.03,3.03c0.03,0.05 0.06,0.09 0.08,0.14c0.08,0.15 0.15,0.3 0.22,0.45c0.04,0.09 0.09,0.18 0.13,0.27c0.05,0.11 0.09,0.23 0.13,0.35c0.05,0.13 0.1,0.27 0.14,0.41c0.02,0.08 0.04,0.16 0.06,0.24c0.05,0.18 0.1,0.36 0.13,0.54c0.01,0.03 0.01,0.07 0.02,0.1c0.16,0.91 0.17,1.84 0.02,2.75l1.97,0.33C21.94,13.1 22,12.56 22,12c0,0 0,0 0,0s0,0 0,0c0,-0.34 -0.02,-0.68 -0.05,-1.01c0,-0.04 -0.01,-0.09 -0.02,-0.13c-0.03,-0.29 -0.07,-0.58 -0.13,-0.87c0,0 0,-0.01 0,-0.01c-0.25,-1.23 -0.73,-2.39 -1.42,-3.44c-0.01,-0.01 -0.02,-0.03 -0.03,-0.04c-0.15,-0.23 -0.32,-0.46 -0.5,-0.68c-0.03,-0.04 -0.06,-0.09 -0.1,-0.13c-0.21,-0.26 -0.44,-0.51 -0.68,-0.75c-0.45,-0.45 -0.94,-0.84 -1.45,-1.19c-0.01,-0.01 -0.03,-0.02 -0.04,-0.03c-0.2,-0.14 -0.41,-0.26 -0.62,-0.38c-0.08,-0.04 -0.15,-0.09 -0.23,-0.14c-0.17,-0.09 -0.34,-0.17 -0.51,-0.25c-0.13,-0.06 -0.26,-0.13 -0.4,-0.18c-0.12,-0.05 -0.25,-0.09 -0.37,-0.14c-0.19,-0.07 -0.38,-0.14 -0.57,-0.2c-0.07,-0.02 -0.14,-0.04 -0.22,-0.06c-0.25,-0.07 -0.5,-0.13 -0.75,-0.18c0,0 0,0 0,0C13.29,2.07 12.65,2 12,2c-0.93,0 -1.82,0.14 -2.67,0.37C9.26,2.39 9.19,2.41 9.12,2.43c-0.2,0.06 -0.39,0.13 -0.58,0.2C8.42,2.67 8.3,2.71 8.18,2.76c-0.14,0.06 -0.27,0.12 -0.4,0.19C7.61,3.03 7.44,3.1 7.27,3.19C7.19,3.24 7.12,3.29 7.04,3.33C7.03,3.34 7.02,3.34 7.01,3.35l1.48,1.48C8.55,4.8 8.6,4.77 8.66,4.74zM2.71,1.29L1.29,2.71l2.97,2.97C2.85,7.4 2,9.6 2,12c0,2.76 1.12,5.26 2.93,7.07l0,0c0.45,0.45 0.94,0.85 1.45,1.2c0.01,0.01 0.02,0.02 0.04,0.03c0.21,0.14 0.42,0.27 0.64,0.39c0.07,0.04 0.14,0.09 0.21,0.13c0.17,0.09 0.35,0.17 0.53,0.25c0.13,0.06 0.25,0.12 0.38,0.18c0.13,0.05 0.26,0.1 0.39,0.14c0.18,0.07 0.36,0.14 0.55,0.19c0.08,0.02 0.16,0.04 0.23,0.06c0.24,0.07 0.48,0.13 0.73,0.18c0.01,0 0.01,0 0.02,0C10.72,21.93 11.35,22 12,22v-0.01c0.54,0 1.08,-0.05 1.62,-0.14l-0.35,-1.98c-1.13,0.18 -2.29,0.13 -3.4,-0.18c-0.06,-0.02 -0.11,-0.03 -0.17,-0.04c-0.16,-0.05 -0.31,-0.11 -0.46,-0.16c-0.1,-0.04 -0.2,-0.07 -0.29,-0.11c-0.11,-0.05 -0.22,-0.1 -0.32,-0.15c-0.13,-0.06 -0.27,-0.12 -0.4,-0.19c-0.06,-0.03 -0.13,-0.08 -0.19,-0.11c-0.17,-0.1 -0.33,-0.19 -0.49,-0.3c-0.01,-0.01 -0.03,-0.02 -0.04,-0.03C5.39,17.16 4,14.74 4,12c0,-1.85 0.64,-3.54 1.7,-4.89l9.73,9.73C15.16,17.33 15,17.9 15,18.5c0,1.93 1.57,3.5 3.5,3.5c0.6,0 1.17,-0.16 1.66,-0.43l1.13,1.13l1.41,-1.41L2.71,1.29z"/>
+      android:pathData="M11.29,14.71L7,10.41V13H5V7h6v2H8.41l4.29,4.29L11.29,14.71zM21,3H3C1.9,3 1,3.9 1,5v14c0,1.1 0.9,2 2,2h10v0v-2H3V5h18v8h2V5C23,3.9 22.1,3 21,3zM19,15c-1.66,0 -3,1.34 -3,3s1.34,3 3,3s3,-1.34 3,-3S20.66,15 19,15z"/>
 </vector>
diff --git a/packages/SystemUI/res/layout/controls_detail_dialog.xml b/packages/SystemUI/res/layout/controls_detail_dialog.xml
index d61122f..ee5315a 100644
--- a/packages/SystemUI/res/layout/controls_detail_dialog.xml
+++ b/packages/SystemUI/res/layout/controls_detail_dialog.xml
@@ -42,6 +42,7 @@
         android:layout_height="1dp" />
     <ImageView
         android:id="@+id/control_detail_open_in_app"
+        android:contentDescription="@string/controls_open_app"
         android:src="@drawable/ic_open_in_new"
         android:background="?android:attr/selectableItemBackgroundBorderless"
         android:tint="@color/control_primary_text"
diff --git a/packages/SystemUI/res/layout/quick_settings_footer.xml b/packages/SystemUI/res/layout/quick_settings_footer.xml
index 846c538..15f398a 100644
--- a/packages/SystemUI/res/layout/quick_settings_footer.xml
+++ b/packages/SystemUI/res/layout/quick_settings_footer.xml
@@ -23,7 +23,7 @@
     android:paddingStart="@dimen/qs_footer_padding_start"
     android:paddingEnd="@dimen/qs_footer_padding_end"
     android:gravity="center_vertical"
-    android:background="?android:attr/colorPrimary" >
+    android:background="@android:color/transparent">
 
     <TextView
         android:id="@+id/footer_text"
@@ -32,7 +32,7 @@
         android:gravity="start"
         android:layout_weight="1"
         android:textAppearance="@style/TextAppearance.QS.TileLabel"
-        android:textColor="?android:attr/textColorSecondary"/>
+        style="@style/qs_security_footer"/>
 
     <ImageView
         android:id="@+id/footer_icon"
@@ -40,6 +40,6 @@
         android:layout_height="@dimen/qs_footer_icon_size"
         android:contentDescription="@null"
         android:src="@drawable/ic_info_outline"
-        android:tint="?android:attr/textColorSecondary"/>
+        style="@style/qs_security_footer"/>
 
 </LinearLayout>
diff --git a/packages/SystemUI/res/values-night/styles.xml b/packages/SystemUI/res/values-night/styles.xml
index 4fdeb6f..50261e1 100644
--- a/packages/SystemUI/res/values-night/styles.xml
+++ b/packages/SystemUI/res/values-night/styles.xml
@@ -29,4 +29,9 @@
         <item name="android:textColor">?android:attr/textColorPrimary</item>
     </style>
 
+    <style name="qs_security_footer" parent="@style/qs_theme">
+        <item name="android:textColor">#B3FFFFFF</item> <!-- 70% white -->
+        <item name="android:tint">#FFFFFFFF</item>
+    </style>
+
 </resources>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index c464608..73d8e9a 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -974,7 +974,9 @@
     <dimen name="recents_quick_scrub_onboarding_margin_start">8dp</dimen>
 
     <!-- The height of the gradient indicating the dismiss edge when moving a PIP. -->
-    <dimen name="floating_dismiss_gradient_height">176dp</dimen>
+    <dimen name="floating_dismiss_gradient_height">250dp</dimen>
+
+    <dimen name="floating_dismiss_bottom_margin">50dp</dimen>
 
     <!-- The bottom margin of the PIP drag to dismiss info text shown when moving a PIP. -->
     <dimen name="pip_dismiss_text_bottom_margin">24dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 39237ac..0314fc8 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2781,6 +2781,8 @@
 
     <!-- Close the controls associated with a specific media session [CHAR_LIMIT=NONE] -->
     <string name="controls_media_close_session">Close this media session</string>
+    <!-- Label for button to resume media playback [CHAR_LIMIT=NONE] -->
+    <string name="controls_media_resume">Resume</string>
 
     <!-- Error message indicating that a control timed out while waiting for an update [CHAR_LIMIT=30] -->
     <string name="controls_error_timeout">Inactive, check app</string>
@@ -2788,7 +2790,13 @@
          a retry will be attempted [CHAR LIMIT=30] -->
     <string name="controls_error_retryable">Error, retrying\u2026</string>
     <!-- Error message indicating that the control is no longer available in the application [CHAR LIMIT=30] -->
-    <string name="controls_error_removed">Device removed</string>
+    <string name="controls_error_removed">Not found</string>
+    <!-- Title for dialog indicating that the control is no longer available in the application [CHAR LIMIT=30] -->
+    <string name="controls_error_removed_title">Control is unavailable</string>
+    <!-- Message body for dialog indicating that the control is no longer available in the application [CHAR LIMIT=NONE] -->
+    <string name="controls_error_removed_message">Couldn\u2019t access <xliff:g id="device" example="Backdoor lock">%1$s</xliff:g>. Check the <xliff:g id="application" example="Google Home">%2$s</xliff:g> app to make sure the control is still available and that the app settings haven\u2019t changed.</string>
+    <!-- Text for button to open the corresponding application [CHAR_LIMIT=20] -->
+    <string name="controls_open_app">Open app</string>
     <!-- Error message indicating that an unspecified error occurred while getting the status [CHAR LIMIT=30] -->
     <string name="controls_error_generic">Can\u2019t load status</string>
     <!-- Error message indicating that a control action failed [CHAR_LIMIT=30] -->
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index ed36bdb..39f78bf 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -387,6 +387,11 @@
         <item name="android:homeAsUpIndicator">@drawable/ic_arrow_back</item>
     </style>
 
+    <style name="qs_security_footer" parent="@style/qs_theme">
+        <item name="android:textColor">?android:attr/textColorSecondary</item>
+        <item name="android:tint">?android:attr/textColorSecondary</item>
+    </style>
+
     <style name="systemui_theme_remote_input" parent="@android:style/Theme.DeviceDefault.Light">
         <item name="android:colorAccent">@color/remote_input_accent</item>
     </style>
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
index 15eda06..97a7304 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/Bubble.java
@@ -21,6 +21,7 @@
 
 import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
 
+import android.annotation.DimenRes;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.Notification;
@@ -104,27 +105,39 @@
     private Path mDotPath;
     private int mFlags;
 
+    @NonNull
+    private UserHandle mUser;
+    @NonNull
+    private String mPackageName;
+    private int mDesiredHeight;
+    @DimenRes
+    private int mDesiredHeightResId;
+
     /**
      * Create a bubble with limited information based on given {@link ShortcutInfo}.
      * Note: Currently this is only being used when the bubble is persisted to disk.
      */
-    Bubble(@NonNull final String key, @NonNull final ShortcutInfo shortcutInfo) {
+    Bubble(@NonNull final String key, @NonNull final ShortcutInfo shortcutInfo,
+            final int desiredHeight, final int desiredHeightResId) {
         Objects.requireNonNull(key);
         Objects.requireNonNull(shortcutInfo);
         mShortcutInfo = shortcutInfo;
         mKey = key;
         mFlags = 0;
+        mUser = shortcutInfo.getUserHandle();
+        mPackageName = shortcutInfo.getPackage();
+        mDesiredHeight = desiredHeight;
+        mDesiredHeightResId = desiredHeightResId;
     }
 
     /** Used in tests when no UI is required. */
     @VisibleForTesting(visibility = PRIVATE)
-    Bubble(NotificationEntry e,
-            BubbleController.NotificationSuppressionChangedListener listener) {
-        mEntry = e;
+    Bubble(@NonNull final NotificationEntry e,
+            @Nullable final BubbleController.NotificationSuppressionChangedListener listener) {
+        Objects.requireNonNull(e);
         mKey = e.getKey();
-        mLastUpdated = e.getSbn().getPostTime();
         mSuppressionListener = listener;
-        mFlags = e.getSbn().getNotification().flags;
+        setEntry(e);
     }
 
     @Override
@@ -137,17 +150,14 @@
         return mEntry;
     }
 
-    @Nullable
+    @NonNull
     public UserHandle getUser() {
-        if (mEntry != null) return mEntry.getSbn().getUser();
-        if (mShortcutInfo != null) return mShortcutInfo.getUserHandle();
-        return null;
+        return mUser;
     }
 
+    @NonNull
     public String getPackageName() {
-        return mEntry == null
-                ? mShortcutInfo == null ? null : mShortcutInfo.getPackage()
-                : mEntry.getSbn().getPackageName();
+        return mPackageName;
     }
 
     @Override
@@ -318,9 +328,18 @@
     /**
      * Sets the entry associated with this bubble.
      */
-    void setEntry(NotificationEntry entry) {
+    void setEntry(@NonNull final NotificationEntry entry) {
+        Objects.requireNonNull(entry);
+        Objects.requireNonNull(entry.getSbn());
         mEntry = entry;
         mLastUpdated = entry.getSbn().getPostTime();
+        mFlags = entry.getSbn().getNotification().flags;
+        mPackageName = entry.getSbn().getPackageName();
+        mUser = entry.getSbn().getUser();
+        if (entry.getBubbleMetadata() != null) {
+            mDesiredHeight = entry.getBubbleMetadata().getDesiredHeight();
+            mDesiredHeightResId = entry.getBubbleMetadata().getDesiredHeightResId();
+        }
     }
 
     /**
@@ -434,28 +453,30 @@
         return mFlyoutMessage;
     }
 
+    int getRawDesiredHeight() {
+        return mDesiredHeight;
+    }
+
+    int getRawDesiredHeightResId() {
+        return mDesiredHeightResId;
+    }
+
     float getDesiredHeight(Context context) {
-        if (mEntry == null) return 0;
-        Notification.BubbleMetadata data = mEntry.getBubbleMetadata();
-        boolean useRes = data.getDesiredHeightResId() != 0;
+        boolean useRes = mDesiredHeightResId != 0;
         if (useRes) {
-            return getDimenForPackageUser(context, data.getDesiredHeightResId(),
-                    mEntry.getSbn().getPackageName(),
-                    mEntry.getSbn().getUser().getIdentifier());
+            return getDimenForPackageUser(context, mDesiredHeightResId, mPackageName,
+                    mUser.getIdentifier());
         } else {
-            return data.getDesiredHeight()
-                    * context.getResources().getDisplayMetrics().density;
+            return mDesiredHeight * context.getResources().getDisplayMetrics().density;
         }
     }
 
     String getDesiredHeightString() {
-        if (mEntry == null) return String.valueOf(0);
-        Notification.BubbleMetadata data = mEntry.getBubbleMetadata();
-        boolean useRes = data.getDesiredHeightResId() != 0;
+        boolean useRes = mDesiredHeightResId != 0;
         if (useRes) {
-            return String.valueOf(data.getDesiredHeightResId());
+            return String.valueOf(mDesiredHeightResId);
         } else {
-            return String.valueOf(data.getDesiredHeight());
+            return String.valueOf(mDesiredHeight);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index 8a2c101..734199e 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -578,6 +578,18 @@
     }
 
     /**
+     * Called when the status bar has become visible or invisible (either permanently or
+     * temporarily).
+     */
+    public void onStatusBarVisibilityChanged(boolean visible) {
+        if (mStackView != null) {
+            // Hide the stack temporarily if the status bar has been made invisible, and the stack
+            // is collapsed. An expanded stack should remain visible until collapsed.
+            mStackView.setTemporarilyInvisible(!visible && !isStackExpanded());
+        }
+    }
+
+    /**
      * Sets whether to perform inflation on the same thread as the caller. This method should only
      * be used in tests, not in production.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDataRepository.kt b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDataRepository.kt
index c2b9195..d20f405 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDataRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleDataRepository.kt
@@ -77,7 +77,8 @@
             var shortcutId = b.shortcutInfo?.id
             if (shortcutId == null) shortcutId = b.entry?.bubbleMetadata?.shortcutId
             if (shortcutId == null) return@mapNotNull null
-            BubbleEntity(userId, b.packageName, shortcutId, b.key)
+            BubbleEntity(userId, b.packageName, shortcutId, b.key, b.rawDesiredHeight,
+                    b.rawDesiredHeightResId)
         }
     }
 
@@ -158,7 +159,8 @@
         val bubbles = entities.mapNotNull { entity ->
             shortcutMap[ShortcutKey(entity.userId, entity.packageName)]
                     ?.first { shortcutInfo -> entity.shortcutId == shortcutInfo.id }
-                    ?.let { shortcutInfo -> Bubble(entity.key, shortcutInfo) }
+                    ?.let { shortcutInfo -> Bubble(entity.key, shortcutInfo, entity.desiredHeight,
+                            entity.desiredHeightResId) }
         }
         uiScope.launch { cb(bubbles) }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index dfa71ba..95c8d08 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -47,6 +47,7 @@
 import android.graphics.Rect;
 import android.graphics.RectF;
 import android.graphics.Region;
+import android.graphics.drawable.TransitionDrawable;
 import android.os.Bundle;
 import android.provider.Settings;
 import android.util.Log;
@@ -79,6 +80,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ContrastColorUtil;
 import com.android.internal.widget.ViewClippingUtil;
+import com.android.systemui.Interpolators;
 import com.android.systemui.Prefs;
 import com.android.systemui.R;
 import com.android.systemui.bubbles.animation.ExpandedAnimationController;
@@ -88,6 +90,7 @@
 import com.android.systemui.shared.system.QuickStepContract;
 import com.android.systemui.shared.system.SysUiStatsLog;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.phone.CollapsedStatusBarFragment;
 import com.android.systemui.statusbar.phone.NotificationShadeWindowController;
 import com.android.systemui.util.DismissCircleView;
 import com.android.systemui.util.FloatingContentCoordinator;
@@ -133,6 +136,9 @@
     /** Percent to darken the bubbles when they're in the dismiss target. */
     private static final float DARKEN_PERCENT = 0.3f;
 
+    /** Duration of the dismiss scrim fading in/out. */
+    private static final int DISMISS_TRANSITION_DURATION_MS = 200;
+
     /** How long to wait, in milliseconds, before hiding the flyout. */
     @VisibleForTesting
     static final int FLYOUT_HIDE_AFTER = 5000;
@@ -239,6 +245,9 @@
     /** Whether a touch gesture, such as a stack/bubble drag or flyout drag, is in progress. */
     private boolean mIsGestureInProgress = false;
 
+    /** Whether or not the stack is temporarily invisible off the side of the screen. */
+    private boolean mTemporarilyInvisible = false;
+
     /** Description of current animation controller state. */
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         pw.println("Stack view state:");
@@ -261,7 +270,7 @@
     private boolean mShowingDismiss = false;
 
     /** The view to desaturate/darken when magneted to the dismiss target. */
-    private View mDesaturateAndDarkenTargetView;
+    @Nullable private View mDesaturateAndDarkenTargetView;
 
     private LayoutInflater mInflater;
 
@@ -643,6 +652,7 @@
         }
     };
 
+    private View mDismissTargetCircle;
     private ViewGroup mDismissTargetContainer;
     private PhysicsAnimator<View> mDismissTargetAnimator;
     private PhysicsAnimator.SpringConfig mDismissTargetSpring = new PhysicsAnimator.SpringConfig(
@@ -749,25 +759,32 @@
         mFlyoutTransitionSpring.addEndListener(mAfterFlyoutTransitionSpring);
 
         final int targetSize = res.getDimensionPixelSize(R.dimen.dismiss_circle_size);
-        final View targetView = new DismissCircleView(context);
+        mDismissTargetCircle = new DismissCircleView(context);
         final FrameLayout.LayoutParams newParams =
                 new FrameLayout.LayoutParams(targetSize, targetSize);
-        newParams.gravity = Gravity.CENTER;
-        targetView.setLayoutParams(newParams);
-        mDismissTargetAnimator = PhysicsAnimator.getInstance(targetView);
+        newParams.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
+        mDismissTargetCircle.setLayoutParams(newParams);
+        mDismissTargetAnimator = PhysicsAnimator.getInstance(mDismissTargetCircle);
 
         mDismissTargetContainer = new FrameLayout(context);
         mDismissTargetContainer.setLayoutParams(new FrameLayout.LayoutParams(
                 MATCH_PARENT,
                 getResources().getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height),
                 Gravity.BOTTOM));
+
+        final int bottomMargin =
+                getResources().getDimensionPixelSize(R.dimen.floating_dismiss_bottom_margin);
+        mDismissTargetContainer.setPadding(0, 0, 0, bottomMargin);
+        mDismissTargetContainer.setClipToPadding(false);
         mDismissTargetContainer.setClipChildren(false);
-        mDismissTargetContainer.addView(targetView);
+        mDismissTargetContainer.addView(mDismissTargetCircle);
         mDismissTargetContainer.setVisibility(View.INVISIBLE);
+        mDismissTargetContainer.setBackgroundResource(
+                R.drawable.floating_dismiss_gradient_transition);
         addView(mDismissTargetContainer);
 
         // Start translated down so the target springs up.
-        targetView.setTranslationY(
+        mDismissTargetCircle.setTranslationY(
                 getResources().getDimensionPixelSize(R.dimen.floating_dismiss_gradient_height));
 
         final ContentResolver contentResolver = getContext().getContentResolver();
@@ -776,7 +793,7 @@
 
         // Save the MagneticTarget instance for the newly set up view - we'll add this to the
         // MagnetizedObjects.
-        mMagneticTarget = new MagnetizedObject.MagneticTarget(targetView, dismissRadius);
+        mMagneticTarget = new MagnetizedObject.MagneticTarget(mDismissTargetCircle, dismissRadius);
 
         mExpandedViewXAnim =
                 new SpringAnimation(mExpandedViewContainer, DynamicAnimation.TRANSLATION_X);
@@ -892,7 +909,10 @@
 
             // Update the paint and apply it to the bubble container.
             mDesaturateAndDarkenPaint.setColorFilter(new ColorMatrixColorFilter(animatedMatrix));
-            mDesaturateAndDarkenTargetView.setLayerPaint(mDesaturateAndDarkenPaint);
+
+            if (mDesaturateAndDarkenTargetView != null) {
+                mDesaturateAndDarkenTargetView.setLayerPaint(mDesaturateAndDarkenPaint);
+            }
         });
 
         // If the stack itself is touched, it means none of its touchable views (bubbles, flyouts,
@@ -908,6 +928,38 @@
 
             return true;
         });
+
+        animate()
+                .setInterpolator(Interpolators.PANEL_CLOSE_ACCELERATED)
+                .setDuration(CollapsedStatusBarFragment.FADE_IN_DURATION);
+    }
+
+    /**
+     * Sets whether or not the stack should become temporarily invisible by moving off the side of
+     * the screen.
+     *
+     * If a flyout comes in while it's invisible, it will animate back in while the flyout is
+     * showing but disappear again when the flyout is gone.
+     */
+    public void setTemporarilyInvisible(boolean invisible) {
+        mTemporarilyInvisible = invisible;
+        animateTemporarilyInvisible();
+    }
+
+    /**
+     * Animates onto or off the screen depending on whether we're temporarily invisible, and whether
+     * a flyout is visible.
+     */
+    private void animateTemporarilyInvisible() {
+        if (mTemporarilyInvisible && mFlyout.getVisibility() != View.VISIBLE) {
+            if (mStackAnimationController.isStackOnLeftSide()) {
+                animate().translationX(-mBubbleSize).start();
+            } else {
+                animate().translationX(mBubbleSize).start();
+            }
+        } else {
+            animate().translationX(0).start();
+        }
     }
 
     private void setUpManageMenu() {
@@ -1123,6 +1175,13 @@
         }
         mExpandedAnimationController.updateResources(mOrientation, mDisplaySize);
         mStackAnimationController.updateResources(mOrientation);
+
+        final int targetSize = res.getDimensionPixelSize(R.dimen.dismiss_circle_size);
+        mDismissTargetCircle.getLayoutParams().width = targetSize;
+        mDismissTargetCircle.getLayoutParams().height = targetSize;
+        mDismissTargetCircle.requestLayout();
+
+        mMagneticTarget.setMagneticFieldRadiusPx(mBubbleSize * 2);
     }
 
     @Override
@@ -1846,6 +1905,10 @@
     private void animateDesaturateAndDarken(View targetView, boolean desaturateAndDarken) {
         mDesaturateAndDarkenTargetView = targetView;
 
+        if (mDesaturateAndDarkenTargetView == null) {
+            return;
+        }
+
         if (desaturateAndDarken) {
             // Use the animated paint for the bubbles.
             mDesaturateAndDarkenTargetView.setLayerType(
@@ -1867,9 +1930,14 @@
     }
 
     private void resetDesaturationAndDarken() {
+
         mDesaturateAndDarkenAnimator.removeAllListeners();
         mDesaturateAndDarkenAnimator.cancel();
-        mDesaturateAndDarkenTargetView.setLayerType(View.LAYER_TYPE_NONE, null);
+
+        if (mDesaturateAndDarkenTargetView != null) {
+            mDesaturateAndDarkenTargetView.setLayerType(View.LAYER_TYPE_NONE, null);
+            mDesaturateAndDarkenTargetView = null;
+        }
     }
 
     /** Animates in the dismiss target. */
@@ -1884,6 +1952,9 @@
         mDismissTargetContainer.setZ(Short.MAX_VALUE - 1);
         mDismissTargetContainer.setVisibility(VISIBLE);
 
+        ((TransitionDrawable) mDismissTargetContainer.getBackground()).startTransition(
+                DISMISS_TRANSITION_DURATION_MS);
+
         mDismissTargetAnimator.cancel();
         mDismissTargetAnimator
                 .spring(DynamicAnimation.TRANSLATION_Y, 0f, mDismissTargetSpring)
@@ -1901,6 +1972,9 @@
 
         mShowingDismiss = false;
 
+        ((TransitionDrawable) mDismissTargetContainer.getBackground()).reverseTransition(
+                DISMISS_TRANSITION_DURATION_MS);
+
         mDismissTargetAnimator
                 .spring(DynamicAnimation.TRANSLATION_Y, mDismissTargetContainer.getHeight(),
                         mDismissTargetSpring)
@@ -1972,6 +2046,9 @@
             // Stop suppressing the dot now that the flyout has morphed into the dot.
             bubbleView.removeDotSuppressionFlag(
                     BadgedImageView.SuppressionFlag.FLYOUT_VISIBLE);
+
+            mFlyout.setVisibility(INVISIBLE);
+            animateTemporarilyInvisible();
         };
         mFlyout.setVisibility(INVISIBLE);
 
@@ -1989,6 +2066,7 @@
             final Runnable expandFlyoutAfterDelay = () -> {
                 mAnimateInFlyout = () -> {
                     mFlyout.setVisibility(VISIBLE);
+                    animateTemporarilyInvisible();
                     mFlyoutDragDeltaX =
                             mStackAnimationController.isStackOnLeftSide()
                                     ? -mFlyout.getWidth()
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleEntity.kt b/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleEntity.kt
index 4348261..355c4b1 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleEntity.kt
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleEntity.kt
@@ -15,11 +15,14 @@
  */
 package com.android.systemui.bubbles.storage
 
+import android.annotation.DimenRes
 import android.annotation.UserIdInt
 
 data class BubbleEntity(
     @UserIdInt val userId: Int,
     val packageName: String,
     val shortcutId: String,
-    val key: String
+    val key: String,
+    val desiredHeight: Int,
+    @DimenRes val desiredHeightResId: Int
 )
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleXmlHelper.kt b/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleXmlHelper.kt
index 1df9f72..a8faf25 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleXmlHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/storage/BubbleXmlHelper.kt
@@ -31,6 +31,8 @@
 private const val ATTR_PACKAGE = "pkg"
 private const val ATTR_SHORTCUT_ID = "sid"
 private const val ATTR_KEY = "key"
+private const val ATTR_DESIRED_HEIGHT = "h"
+private const val ATTR_DESIRED_HEIGHT_RES_ID = "hid"
 
 /**
  * Writes the bubbles in xml format into given output stream.
@@ -59,6 +61,8 @@
         serializer.attribute(null, ATTR_PACKAGE, bubble.packageName)
         serializer.attribute(null, ATTR_SHORTCUT_ID, bubble.shortcutId)
         serializer.attribute(null, ATTR_KEY, bubble.key)
+        serializer.attribute(null, ATTR_DESIRED_HEIGHT, bubble.desiredHeight.toString())
+        serializer.attribute(null, ATTR_DESIRED_HEIGHT_RES_ID, bubble.desiredHeightResId.toString())
         serializer.endTag(null, TAG_BUBBLE)
     } catch (e: IOException) {
         throw RuntimeException(e)
@@ -86,7 +90,9 @@
             parser.getAttributeWithName(ATTR_USER_ID)?.toInt() ?: return null,
             parser.getAttributeWithName(ATTR_PACKAGE) ?: return null,
             parser.getAttributeWithName(ATTR_SHORTCUT_ID) ?: return null,
-            parser.getAttributeWithName(ATTR_KEY) ?: return null
+            parser.getAttributeWithName(ATTR_KEY) ?: return null,
+            parser.getAttributeWithName(ATTR_DESIRED_HEIGHT)?.toInt() ?: return null,
+            parser.getAttributeWithName(ATTR_DESIRED_HEIGHT_RES_ID)?.toInt() ?: return null
     )
 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt
index f07f316..994557c 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/ControlViewHolder.kt
@@ -118,6 +118,7 @@
     var behavior: Behavior? = null
     var lastAction: ControlAction? = null
     var isLoading = false
+    var visibleDialog: Dialog? = null
     private var lastChallengeDialog: Dialog? = null
     private val onDialogCancel: () -> Unit = { lastChallengeDialog = null }
 
@@ -197,18 +198,24 @@
     fun dismiss() {
         lastChallengeDialog?.dismiss()
         lastChallengeDialog = null
+        visibleDialog?.dismiss()
+        visibleDialog = null
     }
 
     fun setTransientStatus(tempStatus: String) {
         val previousText = status.getText()
 
         cancelUpdate = uiExecutor.executeDelayed({
-            setStatusText(previousText)
-            updateContentDescription()
+            animateStatusChange(/* animated */ true, {
+                setStatusText(previousText, /* immediately */ true)
+                updateContentDescription()
+            })
         }, UPDATE_DELAY_IN_MILLIS)
 
-        setStatusText(tempStatus)
-        updateContentDescription()
+        animateStatusChange(/* animated */ true, {
+            setStatusText(tempStatus, /* immediately */ true)
+            updateContentDescription()
+        })
     }
 
     private fun updateContentDescription() =
diff --git a/packages/SystemUI/src/com/android/systemui/controls/ui/StatusBehavior.kt b/packages/SystemUI/src/com/android/systemui/controls/ui/StatusBehavior.kt
index bf3835d..6bf1897 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/ui/StatusBehavior.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/ui/StatusBehavior.kt
@@ -16,7 +16,13 @@
 
 package com.android.systemui.controls.ui
 
+import android.app.AlertDialog
+import android.app.PendingIntent
+import android.content.DialogInterface
+import android.content.pm.PackageManager
 import android.service.controls.Control
+import android.view.View
+import android.view.WindowManager
 
 import com.android.systemui.R
 
@@ -31,7 +37,17 @@
         val status = cws.control?.status ?: Control.STATUS_UNKNOWN
         val msg = when (status) {
             Control.STATUS_ERROR -> R.string.controls_error_generic
-            Control.STATUS_NOT_FOUND -> R.string.controls_error_removed
+            Control.STATUS_DISABLED -> R.string.controls_error_timeout
+            Control.STATUS_NOT_FOUND -> {
+                cvh.layout.setOnClickListener(View.OnClickListener() {
+                    showNotFoundDialog(cvh, cws)
+                })
+                cvh.layout.setOnLongClickListener(View.OnLongClickListener() {
+                    showNotFoundDialog(cvh, cws)
+                    true
+                })
+                R.string.controls_error_removed
+            }
             else -> {
                 cvh.isLoading = true
                 com.android.internal.R.string.loading
@@ -40,4 +56,42 @@
         cvh.setStatusText(cvh.context.getString(msg))
         cvh.applyRenderInfo(false, colorOffset)
     }
+
+    private fun showNotFoundDialog(cvh: ControlViewHolder, cws: ControlWithState) {
+        val pm = cvh.context.getPackageManager()
+        val ai = pm.getApplicationInfo(cws.componentName.packageName, PackageManager.GET_META_DATA)
+        val appLabel = pm.getApplicationLabel(ai)
+        val builder = AlertDialog.Builder(
+            cvh.context,
+            android.R.style.Theme_DeviceDefault_Dialog_Alert
+        ).apply {
+            val res = cvh.context.resources
+            setTitle(res.getString(R.string.controls_error_removed_title))
+            setMessage(res.getString(
+                R.string.controls_error_removed_message, cvh.title.getText(), appLabel))
+            setPositiveButton(
+                R.string.controls_open_app,
+                DialogInterface.OnClickListener { dialog, _ ->
+                    try {
+                        cws.control?.getAppIntent()?.send()
+                    } catch (e: PendingIntent.CanceledException) {
+                        cvh.setTransientStatus(
+                            cvh.context.resources.getString(R.string.controls_error_failed))
+                    }
+                    dialog.dismiss()
+            })
+            setNegativeButton(
+                android.R.string.cancel,
+                DialogInterface.OnClickListener { dialog, _ ->
+                    dialog.cancel()
+                }
+            )
+        }
+        cvh.visibleDialog = builder.create().apply {
+            getWindow().apply {
+                setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY)
+                show()
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
index 7e009b4..5e5ebe9 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialog.java
@@ -175,7 +175,7 @@
     private static final String GLOBAL_ACTION_KEY_SILENT = "silent";
     private static final String GLOBAL_ACTION_KEY_USERS = "users";
     private static final String GLOBAL_ACTION_KEY_SETTINGS = "settings";
-    private static final String GLOBAL_ACTION_KEY_LOCKDOWN = "lockdown";
+    static final String GLOBAL_ACTION_KEY_LOCKDOWN = "lockdown";
     private static final String GLOBAL_ACTION_KEY_VOICEASSIST = "voiceassist";
     private static final String GLOBAL_ACTION_KEY_ASSIST = "assist";
     static final String GLOBAL_ACTION_KEY_RESTART = "restart";
@@ -213,7 +213,9 @@
     @VisibleForTesting
     protected final ArrayList<Action> mItems = new ArrayList<>();
     @VisibleForTesting
-    final ArrayList<Action> mOverflowItems = new ArrayList<>();
+    protected final ArrayList<Action> mOverflowItems = new ArrayList<>();
+    @VisibleForTesting
+    protected final ArrayList<Action> mPowerItems = new ArrayList<>();
 
     @VisibleForTesting
     protected ActionsDialog mDialog;
@@ -223,6 +225,7 @@
 
     private MyAdapter mAdapter;
     private MyOverflowAdapter mOverflowAdapter;
+    private MyPowerOptionsAdapter mPowerAdapter;
 
     private boolean mKeyguardShowing = false;
     private boolean mDeviceProvisioned = false;
@@ -584,14 +587,19 @@
 
         mItems.clear();
         mOverflowItems.clear();
-
+        mPowerItems.clear();
         String[] defaultActions = getDefaultActions();
+
+        ShutDownAction shutdownAction = new ShutDownAction();
+        RestartAction restartAction = new RestartAction();
+        ArraySet<String> addedKeys = new ArraySet<String>();
+
         // make sure emergency affordance action is first, if needed
         if (mEmergencyAffordanceManager.needsEmergencyAffordance()) {
             addActionItem(new EmergencyAffordanceAction());
+            addedKeys.add(GLOBAL_ACTION_KEY_EMERGENCY);
         }
 
-        ArraySet<String> addedKeys = new ArraySet<String>();
         for (int i = 0; i < defaultActions.length; i++) {
             String actionKey = defaultActions[i];
             if (addedKeys.contains(actionKey)) {
@@ -599,7 +607,7 @@
                 continue;
             }
             if (GLOBAL_ACTION_KEY_POWER.equals(actionKey)) {
-                addActionItem(new PowerAction());
+                addActionItem(shutdownAction);
             } else if (GLOBAL_ACTION_KEY_AIRPLANE.equals(actionKey)) {
                 addActionItem(mAirplaneModeOn);
             } else if (GLOBAL_ACTION_KEY_BUGREPORT.equals(actionKey)) {
@@ -618,10 +626,7 @@
             } else if (GLOBAL_ACTION_KEY_SETTINGS.equals(actionKey)) {
                 addActionItem(getSettingsAction());
             } else if (GLOBAL_ACTION_KEY_LOCKDOWN.equals(actionKey)) {
-                int userId = getCurrentUser().id;
-                if (Settings.Secure.getIntForUser(mContentResolver,
-                        Settings.Secure.LOCKDOWN_IN_POWER_MENU, 0, userId) != 0
-                        && shouldDisplayLockdown(userId)) {
+                if (shouldDisplayLockdown(getCurrentUser())) {
                     addActionItem(getLockdownAction());
                 }
             } else if (GLOBAL_ACTION_KEY_VOICEASSIST.equals(actionKey)) {
@@ -629,7 +634,7 @@
             } else if (GLOBAL_ACTION_KEY_ASSIST.equals(actionKey)) {
                 addActionItem(getAssistAction());
             } else if (GLOBAL_ACTION_KEY_RESTART.equals(actionKey)) {
-                addActionItem(new RestartAction());
+                addActionItem(restartAction);
             } else if (GLOBAL_ACTION_KEY_SCREENSHOT.equals(actionKey)) {
                 addActionItem(new ScreenshotAction());
             } else if (GLOBAL_ACTION_KEY_LOGOUT.equals(actionKey)) {
@@ -638,15 +643,32 @@
                     addActionItem(new LogoutAction());
                 }
             } else if (GLOBAL_ACTION_KEY_EMERGENCY.equals(actionKey)) {
-                if (!mEmergencyAffordanceManager.needsEmergencyAffordance()) {
-                    addActionItem(new EmergencyDialerAction());
-                }
+                addActionItem(new EmergencyDialerAction());
             } else {
                 Log.e(TAG, "Invalid global action key " + actionKey);
             }
             // Add here so we don't add more than one.
             addedKeys.add(actionKey);
         }
+
+        // replace power and restart with a single power options action, if needed
+        if (mItems.contains(shutdownAction) && mItems.contains(restartAction)
+                && mOverflowItems.size() > 0) {
+            // transfer shutdown and restart to their own list of power actions
+            mItems.remove(shutdownAction);
+            mItems.remove(restartAction);
+            mPowerItems.add(shutdownAction);
+            mPowerItems.add(restartAction);
+
+            // add the PowerOptionsAction after Emergency, if present
+            int powerIndex = addedKeys.contains(GLOBAL_ACTION_KEY_EMERGENCY) ? 1 : 0;
+            mItems.add(powerIndex, new PowerOptionsAction());
+
+            // transfer the first overflow action to the main set of items
+            Action firstOverflowAction = mOverflowItems.get(0);
+            mOverflowItems.remove(0);
+            mItems.add(firstOverflowAction);
+        }
     }
 
     private void onRotate() {
@@ -664,6 +686,7 @@
 
         mAdapter = new MyAdapter();
         mOverflowAdapter = new MyOverflowAdapter();
+        mPowerAdapter = new MyPowerOptionsAdapter();
 
         mDepthController.setShowingHomeControls(true);
         GlobalActionsPanelPlugin.PanelViewController walletViewController =
@@ -676,7 +699,7 @@
                 walletViewController, mDepthController, mSysuiColorExtractor,
                 mStatusBarService, mNotificationShadeWindowController,
                 controlsAvailable(), uiController,
-                mSysUiState, this::onRotate, mKeyguardShowing);
+                mSysUiState, this::onRotate, mKeyguardShowing, mPowerAdapter);
         boolean walletViewAvailable = walletViewController != null
                 && walletViewController.getPanelContent() != null;
         if (shouldShowLockMessage(walletViewAvailable)) {
@@ -689,7 +712,20 @@
         return dialog;
     }
 
-    private boolean shouldDisplayLockdown(int userId) {
+    @VisibleForTesting
+    protected boolean shouldDisplayLockdown(UserInfo user) {
+        if (user == null) {
+            return false;
+        }
+
+        int userId = user.id;
+
+        // No lockdown option if it's not turned on in Settings
+        if (Settings.Secure.getIntForUser(mContentResolver,
+                Settings.Secure.LOCKDOWN_IN_POWER_MENU, 0, userId) == 0) {
+            return false;
+        }
+
         // Lockdown is meaningless without a place to go.
         if (!mKeyguardStateController.isMethodSecure()) {
             return false;
@@ -740,8 +776,32 @@
         mActivityStarter.startPendingIntentDismissingKeyguard(pendingIntent);
     }
 
-    private final class PowerAction extends SinglePressAction implements LongPressAction {
-        private PowerAction() {
+    @VisibleForTesting
+    protected final class PowerOptionsAction extends SinglePressAction {
+        private PowerOptionsAction() {
+            super(R.drawable.ic_lock_power_off, R.string.global_action_power_options);
+        }
+
+        @Override
+        public boolean showDuringKeyguard() {
+            return true;
+        }
+
+        @Override
+        public boolean showBeforeProvisioning() {
+            return true;
+        }
+
+        @Override
+        public void onPress() {
+            if (mDialog != null) {
+                mDialog.showPowerOptionsMenu();
+            }
+        }
+    }
+
+    private final class ShutDownAction extends SinglePressAction implements LongPressAction {
+        private ShutDownAction() {
             super(R.drawable.ic_lock_power_off,
                     R.string.global_action_power_off);
         }
@@ -772,7 +832,8 @@
         }
     }
 
-    private abstract class EmergencyAction extends SinglePressAction {
+    @VisibleForTesting
+    protected abstract class EmergencyAction extends SinglePressAction {
         EmergencyAction(int iconResId, int messageResId) {
             super(iconResId, messageResId);
         }
@@ -1317,7 +1378,10 @@
             Action item = mAdapter.getItem(position);
             if (!(item instanceof SilentModeTriStateAction)) {
                 if (mDialog != null) {
-                    mDialog.dismiss();
+                    // don't dismiss the dialog if we're opening the power options menu
+                    if (!(item instanceof PowerOptionsAction)) {
+                        mDialog.dismiss();
+                    }
                 } else {
                     Log.w(TAG, "Action clicked while mDialog is null.");
                 }
@@ -1334,6 +1398,70 @@
     /**
      * The adapter used for items in the overflow menu.
      */
+    public class MyPowerOptionsAdapter extends BaseAdapter {
+        @Override
+        public int getCount() {
+            return mPowerItems.size();
+        }
+
+        @Override
+        public Action getItem(int position) {
+            return mPowerItems.get(position);
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            Action action = getItem(position);
+            if (action == null) {
+                Log.w(TAG, "No power options action found at position: " + position);
+                return null;
+            }
+            int viewLayoutResource = com.android.systemui.R.layout.controls_more_item;
+            View view = convertView != null ? convertView
+                    : LayoutInflater.from(mContext).inflate(viewLayoutResource, parent, false);
+            TextView textView = (TextView) view;
+            if (action.getMessageResId() != 0) {
+                textView.setText(action.getMessageResId());
+            } else {
+                textView.setText(action.getMessage());
+            }
+            return textView;
+        }
+
+        private boolean onLongClickItem(int position) {
+            final Action action = getItem(position);
+            if (action instanceof LongPressAction) {
+                if (mDialog != null) {
+                    mDialog.dismiss();
+                } else {
+                    Log.w(TAG, "Action long-clicked while mDialog is null.");
+                }
+                return ((LongPressAction) action).onLongPress();
+            }
+            return false;
+        }
+
+        private void onClickItem(int position) {
+            Action item = getItem(position);
+            if (!(item instanceof SilentModeTriStateAction)) {
+                if (mDialog != null) {
+                    mDialog.dismiss();
+                } else {
+                    Log.w(TAG, "Action clicked while mDialog is null.");
+                }
+                item.onPress();
+            }
+        }
+    }
+
+    /**
+     * The adapter used for items in the power options menu, triggered by the PowerOptionsAction.
+     */
     public class MyOverflowAdapter extends BaseAdapter {
         @Override
         public int getCount() {
@@ -1373,7 +1501,6 @@
             final Action action = getItem(position);
             if (action instanceof LongPressAction) {
                 if (mDialog != null) {
-                    mDialog.hidePowerOverflowMenu();
                     mDialog.dismiss();
                 } else {
                     Log.w(TAG, "Action long-clicked while mDialog is null.");
@@ -1387,7 +1514,6 @@
             Action item = getItem(position);
             if (!(item instanceof SilentModeTriStateAction)) {
                 if (mDialog != null) {
-                    mDialog.hidePowerOverflowMenu();
                     mDialog.dismiss();
                 } else {
                     Log.w(TAG, "Action clicked while mDialog is null.");
@@ -1495,7 +1621,6 @@
             }
         }
 
-
         public int getMessageResId() {
             return mMessageResId;
         }
@@ -1846,6 +1971,8 @@
             mAirplaneState = inAirplaneMode ? ToggleState.On : ToggleState.Off;
             mAirplaneModeOn.updateState(mAirplaneState);
             mAdapter.notifyDataSetChanged();
+            mOverflowAdapter.notifyDataSetChanged();
+            mPowerAdapter.notifyDataSetChanged();
         }
     };
 
@@ -1928,6 +2055,7 @@
         private final Context mContext;
         private final MyAdapter mAdapter;
         private final MyOverflowAdapter mOverflowAdapter;
+        private final MyPowerOptionsAdapter mPowerOptionsAdapter;
         private final IStatusBarService mStatusBarService;
         private final IBinder mToken = new Binder();
         private MultiListLayout mGlobalActionsLayout;
@@ -1943,6 +2071,7 @@
         private final NotificationShadeDepthController mDepthController;
         private final SysUiState mSysUiState;
         private ListPopupWindow mOverflowPopup;
+        private ListPopupWindow mPowerOptionsPopup;
         private final Runnable mOnRotateCallback;
         private final boolean mControlsAvailable;
 
@@ -1958,11 +2087,13 @@
                 SysuiColorExtractor sysuiColorExtractor, IStatusBarService statusBarService,
                 NotificationShadeWindowController notificationShadeWindowController,
                 boolean controlsAvailable, @Nullable ControlsUiController controlsUiController,
-                SysUiState sysuiState, Runnable onRotateCallback, boolean keyguardShowing) {
+                SysUiState sysuiState, Runnable onRotateCallback, boolean keyguardShowing,
+                MyPowerOptionsAdapter powerAdapter) {
             super(context, com.android.systemui.R.style.Theme_SystemUI_Dialog_GlobalActions);
             mContext = context;
             mAdapter = adapter;
             mOverflowAdapter = overflowAdapter;
+            mPowerOptionsAdapter = powerAdapter;
             mDepthController = depthController;
             mColorExtractor = sysuiColorExtractor;
             mStatusBarService = statusBarService;
@@ -2076,6 +2207,21 @@
             }
         }
 
+        private ListPopupWindow createPowerOptionsPopup() {
+            GlobalActionsPopupMenu popup = new GlobalActionsPopupMenu(
+                    new ContextThemeWrapper(
+                            mContext,
+                            com.android.systemui.R.style.Control_ListPopupWindow
+                    ), false /* isDropDownMode */);
+            popup.setOnItemClickListener(
+                    (parent, view, position, id) -> mPowerOptionsAdapter.onClickItem(position));
+            popup.setOnItemLongClickListener(
+                    (parent, view, position, id) -> mPowerOptionsAdapter.onLongClickItem(position));
+            popup.setAnchorView(mGlobalActionsLayout);
+            popup.setAdapter(mPowerOptionsAdapter);
+            return popup;
+        }
+
         private ListPopupWindow createPowerOverflowPopup() {
             GlobalActionsPopupMenu popup = new GlobalActionsPopupMenu(
                     new ContextThemeWrapper(
@@ -2093,16 +2239,16 @@
             return popup;
         }
 
+        public void showPowerOptionsMenu() {
+            mPowerOptionsPopup = createPowerOptionsPopup();
+            mPowerOptionsPopup.show();
+        }
+
         private void showPowerOverflowMenu() {
             mOverflowPopup = createPowerOverflowPopup();
             mOverflowPopup.show();
         }
 
-        private void hidePowerOverflowMenu() {
-            mOverflowPopup.dismiss();
-            mOverflowPopup = null;
-        }
-
         private void initializeLayout() {
             setContentView(com.android.systemui.R.layout.global_actions_grid_v2);
             fixNavBarClipping();
@@ -2278,6 +2424,7 @@
 
                 // close first, as popup windows will not fade during the animation
                 dismissOverflow(false);
+                dismissPowerOptions(false);
                 if (mControlsUiController != null) mControlsUiController.closeDialogs(false);
             });
         }
@@ -2302,6 +2449,7 @@
             resetOrientation();
             dismissWallet();
             dismissOverflow(true);
+            dismissPowerOptions(true);
             if (mControlsUiController != null) mControlsUiController.hide();
             mNotificationShadeWindowController.setForceHasTopUi(mHadTopUi);
             mDepthController.updateGlobalDialogVisibility(0, null /* view */);
@@ -2326,6 +2474,16 @@
             }
         }
 
+        private void dismissPowerOptions(boolean immediate) {
+            if (mPowerOptionsPopup != null) {
+                if (immediate) {
+                    mPowerOptionsPopup.dismissImmediate();
+                } else {
+                    mPowerOptionsPopup.dismiss();
+                }
+            }
+        }
+
         private void setRotationSuggestionsEnabled(boolean enabled) {
             try {
                 final int userId = Binder.getCallingUserHandle().getIdentifier();
@@ -2369,6 +2527,7 @@
             // ensure dropdown menus are dismissed before re-initializing the dialog
             dismissWallet();
             dismissOverflow(true);
+            dismissPowerOptions(true);
             if (mControlsUiController != null) {
                 mControlsUiController.hide();
             }
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
index 5595201..f039fc2 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
@@ -17,12 +17,8 @@
 package com.android.systemui.media;
 
 import android.app.PendingIntent;
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
 import android.content.res.ColorStateList;
 import android.graphics.Bitmap;
 import android.graphics.Canvas;
@@ -35,7 +31,6 @@
 import android.media.session.MediaController;
 import android.media.session.MediaSession;
 import android.media.session.PlaybackState;
-import android.service.media.MediaBrowserService;
 import android.util.Log;
 import android.view.View;
 import android.widget.ImageButton;
@@ -54,16 +49,17 @@
 import com.android.settingslib.media.MediaOutputSliceConstants;
 import com.android.settingslib.widget.AdaptiveIcon;
 import com.android.systemui.R;
+import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.plugins.ActivityStarter;
-import com.android.systemui.qs.QSMediaBrowser;
 import com.android.systemui.util.animation.TransitionLayout;
-import com.android.systemui.util.concurrency.DelayableExecutor;
 
 import org.jetbrains.annotations.NotNull;
 
 import java.util.List;
 import java.util.concurrent.Executor;
 
+import javax.inject.Inject;
+
 /**
  * A view controller used for Media Playback.
  */
@@ -81,7 +77,6 @@
 
     private final SeekBarViewModel mSeekBarViewModel;
     private SeekBarObserver mSeekBarObserver;
-    private final Executor mForegroundExecutor;
     protected final Executor mBackgroundExecutor;
     private final ActivityStarter mActivityStarter;
 
@@ -91,51 +86,23 @@
     private MediaSession.Token mToken;
     private MediaController mController;
     private int mBackgroundColor;
-    protected ComponentName mServiceComponent;
-    private boolean mIsRegistered = false;
-    private String mKey;
     private int mAlbumArtSize;
     private int mAlbumArtRadius;
-    private int mViewWidth;
-
-    public static final String MEDIA_PREFERENCES = "media_control_prefs";
-    public static final String MEDIA_PREFERENCE_KEY = "browser_components";
-    private SharedPreferences mSharedPrefs;
-    private boolean mCheckedForResumption = false;
-    private QSMediaBrowser mQSMediaBrowser;
-
-    private final MediaController.Callback mSessionCallback = new MediaController.Callback() {
-        @Override
-        public void onSessionDestroyed() {
-            Log.d(TAG, "session destroyed");
-            mController.unregisterCallback(mSessionCallback);
-            clearControls();
-        }
-        @Override
-        public void onPlaybackStateChanged(PlaybackState state) {
-            final int s = state != null ? state.getState() : PlaybackState.STATE_NONE;
-            if (s == PlaybackState.STATE_NONE) {
-                Log.d(TAG, "playback state change will trigger resumption, state=" + state);
-                clearControls();
-            }
-        }
-    };
 
     /**
      * Initialize a new control panel
      * @param context
-     * @param foregroundExecutor foreground executor
      * @param backgroundExecutor background executor, used for processing artwork
      * @param activityStarter activity starter
      */
-    public MediaControlPanel(Context context, Executor foregroundExecutor,
-            DelayableExecutor backgroundExecutor, ActivityStarter activityStarter,
-            MediaHostStatesManager mediaHostStatesManager) {
+    @Inject
+    public MediaControlPanel(Context context, @Background Executor backgroundExecutor,
+            ActivityStarter activityStarter, MediaHostStatesManager mediaHostStatesManager,
+            SeekBarViewModel seekBarViewModel) {
         mContext = context;
-        mForegroundExecutor = foregroundExecutor;
         mBackgroundExecutor = backgroundExecutor;
         mActivityStarter = activityStarter;
-        mSeekBarViewModel = new SeekBarViewModel(backgroundExecutor);
+        mSeekBarViewModel = seekBarViewModel;
         mMediaViewController = new MediaViewController(context, mediaHostStatesManager);
         loadDimens();
     }
@@ -214,45 +181,18 @@
         MediaSession.Token token = data.getToken();
         mBackgroundColor = data.getBackgroundColor();
         if (mToken == null || !mToken.equals(token)) {
-            if (mQSMediaBrowser != null) {
-                Log.d(TAG, "Disconnecting old media browser");
-                mQSMediaBrowser.disconnect();
-                mQSMediaBrowser = null;
-            }
             mToken = token;
-            mServiceComponent = null;
-            mCheckedForResumption = false;
         }
 
-        mController = new MediaController(mContext, mToken);
+        if (mToken != null) {
+            mController = new MediaController(mContext, mToken);
+        } else {
+            mController = null;
+        }
 
         ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
         ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
 
-        // Try to find a browser service component for this app
-        // TODO also check for a media button receiver intended for restarting (b/154127084)
-        // Only check if we haven't tried yet or the session token changed
-        final String pkgName = data.getPackageName();
-        if (mServiceComponent == null && !mCheckedForResumption) {
-            Log.d(TAG, "Checking for service component");
-            PackageManager pm = mContext.getPackageManager();
-            Intent resumeIntent = new Intent(MediaBrowserService.SERVICE_INTERFACE);
-            List<ResolveInfo> resumeInfo = pm.queryIntentServices(resumeIntent, 0);
-            // TODO: look into this resumption
-            if (resumeInfo != null) {
-                for (ResolveInfo inf : resumeInfo) {
-                    if (inf.serviceInfo.packageName.equals(mController.getPackageName())) {
-                        mBackgroundExecutor.execute(() ->
-                                tryUpdateResumptionList(inf.getComponentInfo().getComponentName()));
-                        break;
-                    }
-                }
-            }
-            mCheckedForResumption = true;
-        }
-
-        mController.registerCallback(mSessionCallback);
-
         mViewHolder.getPlayer().setBackgroundTintList(
                 ColorStateList.valueOf(mBackgroundColor));
 
@@ -267,12 +207,22 @@
         ImageView albumView = mViewHolder.getAlbumView();
         // TODO: migrate this to a view with rounded corners instead of baking the rounding
         // into the bitmap
-        Drawable artwork = createRoundedBitmap(data.getArtwork());
-        albumView.setImageDrawable(artwork);
+        boolean hasArtwork = data.getArtwork() != null;
+        if (hasArtwork) {
+            Drawable artwork = createRoundedBitmap(data.getArtwork());
+            albumView.setImageDrawable(artwork);
+        }
+        setVisibleAndAlpha(collapsedSet, R.id.album_art, hasArtwork);
+        setVisibleAndAlpha(expandedSet, R.id.album_art, hasArtwork);
 
         // App icon
         ImageView appIcon = mViewHolder.getAppIcon();
-        appIcon.setImageDrawable(data.getAppIcon());
+        if (data.getAppIcon() != null) {
+            appIcon.setImageDrawable(data.getAppIcon());
+        } else {
+            Drawable iconDrawable = mContext.getDrawable(R.drawable.ic_music_note);
+            appIcon.setImageDrawable(iconDrawable);
+        }
 
         // Song name
         TextView titleText = mViewHolder.getTitleText();
@@ -294,7 +244,7 @@
             final Intent intent = new Intent()
                     .setAction(MediaOutputSliceConstants.ACTION_MEDIA_OUTPUT)
                     .putExtra(MediaOutputSliceConstants.EXTRA_PACKAGE_NAME,
-                            mController.getPackageName())
+                            data.getPackageName())
                     .putExtra(MediaOutputSliceConstants.KEY_MEDIA_SESSION_TOKEN, mToken);
             mActivityStarter.startActivity(intent, false, true /* dismissShade */,
                     Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
@@ -350,15 +300,11 @@
             MediaAction mediaAction = actionIcons.get(i);
             button.setImageDrawable(mediaAction.getDrawable());
             button.setContentDescription(mediaAction.getContentDescription());
-            PendingIntent actionIntent = mediaAction.getIntent();
+            Runnable action = mediaAction.getAction();
 
             button.setOnClickListener(v -> {
-                if (actionIntent != null) {
-                    try {
-                        actionIntent.send();
-                    } catch (PendingIntent.CanceledException e) {
-                        e.printStackTrace();
-                    }
+                if (action != null) {
+                    action.run();
                 }
             });
             boolean visibleInCompat = actionsWhenCollapsed.contains(i);
@@ -444,14 +390,6 @@
     }
 
     /**
-     * Return the original notification's key
-     * @return The notification key
-     */
-    public String getKey()  {
-        return mKey;
-    }
-
-    /**
      * Check whether this player has an attached media session.
      * @return whether there is a controller with a current media session.
      */
@@ -485,150 +423,8 @@
         return (state.getState() == PlaybackState.STATE_PLAYING);
     }
 
-    /**
-     * Puts controls into a resumption state if possible, or calls removePlayer if no component was
-     * found that could resume playback
-     */
-    public void clearControls() {
-        Log.d(TAG, "clearControls to resumption state package=" + getMediaPlayerPackage());
-        if (mServiceComponent == null) {
-            // If we don't have a way to resume, just remove the player altogether
-            Log.d(TAG, "Removing unresumable controls");
-            removePlayer();
-            return;
-        }
-        resetButtons();
-    }
-
-    /**
-     * Hide the media buttons and show only a restart button
-     */
-    protected void resetButtons() {
-        if (mViewHolder == null) {
-            return;
-        }
-        // Hide all the old buttons
-
-        ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
-        ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
-        for (int i = 1; i < ACTION_IDS.length; i++) {
-            setVisibleAndAlpha(expandedSet, ACTION_IDS[i], false /*visible */);
-            setVisibleAndAlpha(collapsedSet, ACTION_IDS[i], false /*visible */);
-        }
-
-        // Add a restart button
-        ImageButton btn = mViewHolder.getAction0();
-        btn.setOnClickListener(v -> {
-            Log.d(TAG, "Attempting to restart session");
-            if (mQSMediaBrowser != null) {
-                mQSMediaBrowser.disconnect();
-            }
-            mQSMediaBrowser = new QSMediaBrowser(mContext, new QSMediaBrowser.Callback(){
-                @Override
-                public void onConnected() {
-                    Log.d(TAG, "Successfully restarted");
-                }
-                @Override
-                public void onError() {
-                    Log.e(TAG, "Error restarting");
-                    mQSMediaBrowser.disconnect();
-                    mQSMediaBrowser = null;
-                }
-            }, mServiceComponent);
-            mQSMediaBrowser.restart();
-        });
-        btn.setImageDrawable(mContext.getResources().getDrawable(R.drawable.lb_ic_play));
-        setVisibleAndAlpha(expandedSet, ACTION_IDS[0], true /*visible */);
-        setVisibleAndAlpha(collapsedSet, ACTION_IDS[0], true /*visible */);
-
-        mSeekBarViewModel.clearController();
-        // TODO: fix guts
-        //        View guts = mMediaNotifView.findViewById(R.id.media_guts);
-        View options = mViewHolder.getOptions();
-
-        mViewHolder.getPlayer().setOnLongClickListener(v -> {
-            // Replace player view with close/cancel view
-//            guts.setVisibility(View.GONE);
-            options.setVisibility(View.VISIBLE);
-            return true; // consumed click
-        });
-        mMediaViewController.refreshState();
-    }
-
     private void setVisibleAndAlpha(ConstraintSet set, int actionId, boolean visible) {
         set.setVisibility(actionId, visible? ConstraintSet.VISIBLE : ConstraintSet.GONE);
         set.setAlpha(actionId, visible ? 1.0f : 0.0f);
     }
-
-    /**
-     * Verify that we can connect to the given component with a MediaBrowser, and if so, add that
-     * component to the list of resumption components
-     */
-    private void tryUpdateResumptionList(ComponentName componentName) {
-        Log.d(TAG, "Testing if we can connect to " + componentName);
-        if (mQSMediaBrowser != null) {
-            mQSMediaBrowser.disconnect();
-        }
-        mQSMediaBrowser = new QSMediaBrowser(mContext,
-                new QSMediaBrowser.Callback() {
-                    @Override
-                    public void onConnected() {
-                        Log.d(TAG, "yes we can resume with " + componentName);
-                        mServiceComponent = componentName;
-                        updateResumptionList(componentName);
-                        mQSMediaBrowser.disconnect();
-                        mQSMediaBrowser = null;
-                    }
-
-                    @Override
-                    public void onError() {
-                        Log.d(TAG, "Cannot resume with " + componentName);
-                        mServiceComponent = null;
-                        if (!hasMediaSession()) {
-                            // If it's not active and we can't resume, remove
-                            removePlayer();
-                        }
-                        mQSMediaBrowser.disconnect();
-                        mQSMediaBrowser = null;
-                    }
-                },
-                componentName);
-        mQSMediaBrowser.testConnection();
-    }
-
-    /**
-     * Add the component to the saved list of media browser services, checking for duplicates and
-     * removing older components that exceed the maximum limit
-     * @param componentName
-     */
-    private synchronized void updateResumptionList(ComponentName componentName) {
-        // Add to front of saved list
-        if (mSharedPrefs == null) {
-            mSharedPrefs = mContext.getSharedPreferences(MEDIA_PREFERENCES, 0);
-        }
-        String componentString = componentName.flattenToString();
-        String listString = mSharedPrefs.getString(MEDIA_PREFERENCE_KEY, null);
-        if (listString == null) {
-            listString = componentString;
-        } else {
-            String[] components = listString.split(QSMediaBrowser.DELIMITER);
-            StringBuilder updated = new StringBuilder(componentString);
-            int nBrowsers = 1;
-            for (int i = 0; i < components.length
-                    && nBrowsers < QSMediaBrowser.MAX_RESUMPTION_CONTROLS; i++) {
-                if (componentString.equals(components[i])) {
-                    continue;
-                }
-                updated.append(QSMediaBrowser.DELIMITER).append(components[i]);
-                nBrowsers++;
-            }
-            listString = updated.toString();
-        }
-        mSharedPrefs.edit().putString(MEDIA_PREFERENCE_KEY, listString).apply();
-    }
-
-    /**
-     * Called when a player can't be resumed to give it an opportunity to hide or remove itself
-     */
-    protected void removePlayer() { }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt
index a94f6a8..5d28178 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaData.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaData.kt
@@ -32,17 +32,19 @@
     val artwork: Icon?,
     val actions: List<MediaAction>,
     val actionsToShowInCompact: List<Int>,
-    val packageName: String?,
+    val packageName: String,
     val token: MediaSession.Token?,
     val clickIntent: PendingIntent?,
     val device: MediaDeviceData?,
-    val notificationKey: String = "INVALID"
+    var resumeAction: Runnable?,
+    val notificationKey: String = "INVALID",
+    var hasCheckedForResume: Boolean = false
 )
 
 /** State of a media action. */
 data class MediaAction(
     val drawable: Drawable?,
-    val intent: PendingIntent?,
+    val action: Runnable?,
     val contentDescription: CharSequence?
 )
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt
index 67cf21a..11cbc48 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataCombineLatest.kt
@@ -32,9 +32,15 @@
 
     init {
         dataSource.addListener(object : MediaDataManager.Listener {
-            override fun onMediaDataLoaded(key: String, data: MediaData) {
-                entries[key] = data to entries[key]?.second
-                update(key)
+            override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
+                if (oldKey != null && !oldKey.equals(key)) {
+                    val s = entries[oldKey]?.second
+                    entries[key] = data to entries[oldKey]?.second
+                    entries.remove(oldKey)
+                } else {
+                    entries[key] = data to entries[key]?.second
+                }
+                update(key, oldKey)
             }
             override fun onMediaDataRemoved(key: String) {
                 remove(key)
@@ -43,7 +49,7 @@
         deviceSource.addListener(object : MediaDeviceManager.Listener {
             override fun onMediaDeviceChanged(key: String, data: MediaDeviceData?) {
                 entries[key] = entries[key]?.first to data
-                update(key)
+                update(key, key)
             }
             override fun onKeyRemoved(key: String) {
                 remove(key)
@@ -61,13 +67,13 @@
      */
     fun removeListener(listener: MediaDataManager.Listener) = listeners.remove(listener)
 
-    private fun update(key: String) {
+    private fun update(key: String, oldKey: String?) {
         val (entry, device) = entries[key] ?: null to null
         if (entry != null && device != null) {
             val data = entry.copy(device = device)
             val listenersCopy = listeners.toSet()
             listenersCopy.forEach {
-                it.onMediaDataLoaded(key, data)
+                it.onMediaDataLoaded(key, oldKey, data)
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
index d949857..094c5be 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.media
 
 import android.app.Notification
+import android.app.PendingIntent
 import android.content.ContentResolver
 import android.content.Context
 import android.graphics.Bitmap
@@ -25,6 +26,7 @@
 import android.graphics.ImageDecoder
 import android.graphics.drawable.Drawable
 import android.graphics.drawable.Icon
+import android.media.MediaDescription
 import android.media.MediaMetadata
 import android.media.session.MediaSession
 import android.net.Uri
@@ -32,8 +34,10 @@
 import android.text.TextUtils
 import android.util.Log
 import com.android.internal.graphics.ColorUtils
+import com.android.systemui.R
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.statusbar.NotificationMediaManager
 import com.android.systemui.statusbar.notification.MediaNotificationProcessor
 import com.android.systemui.statusbar.notification.NotificationEntryManager
 import com.android.systemui.statusbar.notification.NotificationEntryManager.UNDEFINED_DISMISS_REASON
@@ -58,7 +62,7 @@
 private const val SATURATION_MULTIPLIER = 0.8f
 
 private val LOADING = MediaData(false, 0, null, null, null, null, null,
-        emptyList(), emptyList(), null, null, null, null)
+        emptyList(), emptyList(), "INVALID", null, null, null, null)
 
 fun isMediaNotification(sbn: StatusBarNotification): Boolean {
     if (!sbn.notification.hasMediaSession()) {
@@ -81,34 +85,92 @@
     private val mediaControllerFactory: MediaControllerFactory,
     private val mediaTimeoutListener: MediaTimeoutListener,
     private val notificationEntryManager: NotificationEntryManager,
+    private val mediaResumeListener: MediaResumeListener,
     @Background private val backgroundExecutor: Executor,
     @Main private val foregroundExecutor: Executor
 ) {
 
     private val listeners: MutableSet<Listener> = mutableSetOf()
     private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
+    private val useMediaResumption: Boolean = Utils.useMediaResumption(context)
 
     init {
         mediaTimeoutListener.timeoutCallback = { token: String, timedOut: Boolean ->
             setTimedOut(token, timedOut) }
         addListener(mediaTimeoutListener)
+
+        if (useMediaResumption) {
+            mediaResumeListener.addTrackToResumeCallback = { desc: MediaDescription,
+                resumeAction: Runnable, token: MediaSession.Token, appName: String,
+                appIntent: PendingIntent, packageName: String ->
+                addResumptionControls(desc, resumeAction, token, appName, appIntent, packageName)
+            }
+            mediaResumeListener.resumeComponentFoundCallback = { key: String, action: Runnable? ->
+                mediaEntries.get(key)?.resumeAction = action
+                mediaEntries.get(key)?.hasCheckedForResume = true
+            }
+            addListener(mediaResumeListener)
+        }
     }
 
     fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
         if (Utils.useQsMediaPlayer(context) && isMediaNotification(sbn)) {
             Assert.isMainThread()
-            if (!mediaEntries.containsKey(key)) {
-                mediaEntries.put(key, LOADING)
+            val oldKey = findExistingEntry(key, sbn.packageName)
+            if (oldKey == null) {
+                val temp = LOADING.copy(packageName = sbn.packageName)
+                mediaEntries.put(key, temp)
+            } else if (oldKey != key) {
+                // Move to new key
+                val oldData = mediaEntries.remove(oldKey)!!
+                mediaEntries.put(key, oldData)
             }
-            loadMediaData(key, sbn)
+            loadMediaData(key, sbn, oldKey)
         } else {
             onNotificationRemoved(key)
         }
     }
 
-    private fun loadMediaData(key: String, sbn: StatusBarNotification) {
+    private fun addResumptionControls(
+        desc: MediaDescription,
+        action: Runnable,
+        token: MediaSession.Token,
+        appName: String,
+        appIntent: PendingIntent,
+        packageName: String
+    ) {
+        // Resume controls don't have a notification key, so store by package name instead
+        if (!mediaEntries.containsKey(packageName)) {
+            val resumeData = LOADING.copy(packageName = packageName, resumeAction = action)
+            mediaEntries.put(packageName, resumeData)
+        }
         backgroundExecutor.execute {
-            loadMediaDataInBg(key, sbn)
+            loadMediaDataInBg(desc, action, token, appName, appIntent, packageName)
+        }
+    }
+
+    /**
+     * Check if there is an existing entry that matches the key or package name.
+     * Returns the key that matches, or null if not found.
+     */
+    private fun findExistingEntry(key: String, packageName: String): String? {
+        if (mediaEntries.containsKey(key)) {
+            return key
+        }
+        // Check if we already had a resume player
+        if (mediaEntries.containsKey(packageName)) {
+            return packageName
+        }
+        return null
+    }
+
+    private fun loadMediaData(
+        key: String,
+        sbn: StatusBarNotification,
+        oldKey: String?
+    ) {
+        backgroundExecutor.execute {
+            loadMediaDataInBg(key, sbn, oldKey)
         }
     }
 
@@ -132,7 +194,50 @@
         }
     }
 
-    private fun loadMediaDataInBg(key: String, sbn: StatusBarNotification) {
+    private fun loadMediaDataInBg(
+        desc: MediaDescription,
+        resumeAction: Runnable,
+        token: MediaSession.Token,
+        appName: String,
+        appIntent: PendingIntent,
+        packageName: String
+    ) {
+        if (resumeAction == null) {
+            Log.e(TAG, "Resume action cannot be null")
+            return
+        }
+
+        if (TextUtils.isEmpty(desc.title)) {
+            Log.e(TAG, "Description incomplete")
+            return
+        }
+
+        Log.d(TAG, "adding track from browser: $desc")
+
+        // Album art
+        var artworkBitmap = desc.iconBitmap
+        if (artworkBitmap == null && desc.iconUri != null) {
+            artworkBitmap = loadBitmapFromUri(desc.iconUri!!)
+        }
+        val artworkIcon = if (artworkBitmap != null) {
+            Icon.createWithBitmap(artworkBitmap)
+        } else {
+            null
+        }
+
+        val mediaAction = getResumeMediaAction(resumeAction)
+        foregroundExecutor.execute {
+            onMediaDataLoaded(packageName, null, MediaData(true, Color.DKGRAY, appName,
+                null, desc.subtitle, desc.title, artworkIcon, listOf(mediaAction), listOf(0),
+                packageName, token, appIntent, null, resumeAction, packageName))
+        }
+    }
+
+    private fun loadMediaDataInBg(
+        key: String,
+        sbn: StatusBarNotification,
+        oldKey: String?
+    ) {
         val token = sbn.notification.extras.getParcelable(Notification.EXTRA_MEDIA_SESSION)
                 as MediaSession.Token?
         val metadata = mediaControllerFactory.create(token).metadata
@@ -234,16 +339,23 @@
                 }
                 val mediaAction = MediaAction(
                         action.getIcon().loadDrawable(packageContext),
-                        action.actionIntent,
+                        Runnable {
+                            try {
+                                action.actionIntent.send()
+                            } catch (e: PendingIntent.CanceledException) {
+                                Log.d(TAG, "Intent canceled", e)
+                            }
+                        },
                         action.title)
                 actionIcons.add(mediaAction)
             }
         }
 
+        val resumeAction: Runnable? = mediaEntries.get(key)?.resumeAction
         foregroundExecutor.execute {
-            onMediaDataLoaded(key, MediaData(true, bgColor, app, smallIconDrawable, artist, song,
-                    artWorkIcon, actionIcons, actionsToShowCollapsed, sbn.packageName, token,
-                    notif.contentIntent, null, key))
+            onMediaDataLoaded(key, oldKey, MediaData(true, bgColor, app, smallIconDrawable, artist,
+                    song, artWorkIcon, actionIcons, actionsToShowCollapsed, sbn.packageName, token,
+                    notif.contentIntent, null, resumeAction, key))
         }
     }
 
@@ -257,7 +369,7 @@
                 val albumArt = loadBitmapFromUri(Uri.parse(uriString))
                 if (albumArt != null) {
                     Log.d(TAG, "loaded art from $uri")
-                    break
+                    return albumArt
                 }
             }
         }
@@ -283,27 +395,52 @@
 
         val source = ImageDecoder.createSource(context.getContentResolver(), uri)
         return try {
-            ImageDecoder.decodeBitmap(source)
+            ImageDecoder.decodeBitmap(source) {
+                decoder, info, source -> decoder.isMutableRequired = true
+            }
         } catch (e: IOException) {
             e.printStackTrace()
             null
         }
     }
 
-    fun onMediaDataLoaded(key: String, data: MediaData) {
+    private fun getResumeMediaAction(action: Runnable): MediaAction {
+        return MediaAction(
+            context.getDrawable(R.drawable.lb_ic_play),
+            action,
+            context.getString(R.string.controls_media_resume)
+        )
+    }
+
+    fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
         Assert.isMainThread()
         if (mediaEntries.containsKey(key)) {
             // Otherwise this was removed already
             mediaEntries.put(key, data)
             val listenersCopy = listeners.toSet()
             listenersCopy.forEach {
-                it.onMediaDataLoaded(key, data)
+                it.onMediaDataLoaded(key, oldKey, data)
             }
         }
     }
 
     fun onNotificationRemoved(key: String) {
         Assert.isMainThread()
+        if (useMediaResumption && mediaEntries.get(key)?.resumeAction != null) {
+            Log.d(TAG, "Not removing $key because resumable")
+            // Move to resume key aka package name
+            val data = mediaEntries.remove(key)!!
+            val resumeAction = getResumeMediaAction(data.resumeAction!!)
+            val updated = data.copy(token = null, actions = listOf(resumeAction),
+                actionsToShowInCompact = listOf(0))
+            mediaEntries.put(data.packageName, updated)
+            // Notify listeners of "new" controls
+            val listenersCopy = listeners.toSet()
+            listenersCopy.forEach {
+                it.onMediaDataLoaded(data.packageName, key, updated)
+            }
+            return
+        }
         val removed = mediaEntries.remove(key)
         if (removed != null) {
             val listenersCopy = listeners.toSet()
@@ -316,19 +453,32 @@
     /**
      * Are there any media notifications active?
      */
-    fun hasActiveMedia() = mediaEntries.isNotEmpty()
+    fun hasActiveMedia() = mediaEntries.any({ isActive(it.value) })
 
-    fun hasAnyMedia(): Boolean {
-        // TODO: implement this when we implemented resumption
-        return hasActiveMedia()
+    fun isActive(data: MediaData): Boolean {
+        if (data.token == null) {
+            return false
+        }
+        val controller = mediaControllerFactory.create(data.token)
+        val state = controller?.playbackState?.state
+        return state != null && NotificationMediaManager.isActiveState(state)
     }
 
+    /**
+     * Are there any media entries, including resume controls?
+     */
+    fun hasAnyMedia() = mediaEntries.isNotEmpty()
+
     interface Listener {
 
         /**
-         * Called whenever there's new MediaData Loaded for the consumption in views
+         * Called whenever there's new MediaData Loaded for the consumption in views.
+         *
+         * oldKey is provided to check whether the view has changed keys, which can happen when a
+         * player has gone from resume state (key is package name) to active state (key is
+         * notification key) or vice versa.
          */
-        fun onMediaDataLoaded(key: String, data: MediaData) {}
+        fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {}
 
         /**
          * Called whenever a previously existing Media notification was removed
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
index 552fea6..2f521ea 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
@@ -16,11 +16,8 @@
 
 package com.android.systemui.media
 
-import android.app.Notification
 import android.content.Context
-import android.service.notification.StatusBarNotification
 import android.media.MediaRouter2Manager
-import android.media.session.MediaSession
 import android.media.session.MediaController
 import com.android.settingslib.media.LocalMediaManager
 import com.android.settingslib.media.MediaDevice
@@ -38,11 +35,16 @@
     private val localMediaManagerFactory: LocalMediaManagerFactory,
     private val mr2manager: MediaRouter2Manager,
     private val featureFlag: MediaFeatureFlag,
-    @Main private val fgExecutor: Executor
-) {
+    @Main private val fgExecutor: Executor,
+    private val mediaDataManager: MediaDataManager
+) : MediaDataManager.Listener {
     private val listeners: MutableSet<Listener> = mutableSetOf()
     private val entries: MutableMap<String, Token> = mutableMapOf()
 
+    init {
+        mediaDataManager.addListener(this)
+    }
+
     /**
      * Add a listener for changes to the media route (ie. device).
      */
@@ -53,23 +55,25 @@
      */
     fun removeListener(listener: Listener) = listeners.remove(listener)
 
-    fun onNotificationAdded(key: String, sbn: StatusBarNotification) {
-        if (featureFlag.enabled && isMediaNotification(sbn)) {
+    override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
+        if (featureFlag.enabled) {
+            if (oldKey != null && oldKey != key) {
+                val oldToken = entries.remove(oldKey)
+                oldToken?.stop()
+            }
             var tok = entries[key]
-            if (tok == null) {
-                val token = sbn.notification.extras.getParcelable(Notification.EXTRA_MEDIA_SESSION)
-                        as MediaSession.Token?
-                val controller = MediaController(context, token)
-                tok = Token(key, controller, localMediaManagerFactory.create(sbn.packageName))
+            if (tok == null && data.token != null) {
+                val controller = MediaController(context, data.token!!)
+                tok = Token(key, controller, localMediaManagerFactory.create(data.packageName))
                 entries[key] = tok
                 tok.start()
             }
         } else {
-            onNotificationRemoved(key)
+            onMediaDataRemoved(key)
         }
     }
 
-    fun onNotificationRemoved(key: String) {
+    override fun onMediaDataRemoved(key: String) {
         val token = entries.remove(key)
         token?.stop()
         token?.let {
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
index e904e93..2bd8c0c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt
@@ -50,7 +50,7 @@
         }
 
     private val listener = object : MediaDataManager.Listener {
-        override fun onMediaDataLoaded(key: String, data: MediaData) {
+        override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
             updateViewVisibility()
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt
new file mode 100644
index 0000000..6bbe0d1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaResumeListener.kt
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.media
+
+import android.app.PendingIntent
+import android.content.BroadcastReceiver
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.PackageManager
+import android.media.MediaDescription
+import android.media.session.MediaController
+import android.media.session.MediaSession
+import android.os.UserHandle
+import android.service.media.MediaBrowserService
+import android.util.Log
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.util.Utils
+import java.util.concurrent.ConcurrentLinkedQueue
+import java.util.concurrent.Executor
+import javax.inject.Inject
+import javax.inject.Singleton
+
+private const val TAG = "MediaResumeListener"
+
+private const val MEDIA_PREFERENCES = "media_control_prefs"
+private const val MEDIA_PREFERENCE_KEY = "browser_components"
+
+@Singleton
+class MediaResumeListener @Inject constructor(
+    private val context: Context,
+    private val broadcastDispatcher: BroadcastDispatcher,
+    @Background private val backgroundExecutor: Executor
+) : MediaDataManager.Listener {
+
+    private val useMediaResumption: Boolean = Utils.useMediaResumption(context)
+    private val resumeComponents: ConcurrentLinkedQueue<ComponentName> = ConcurrentLinkedQueue()
+
+    lateinit var addTrackToResumeCallback: (
+        MediaDescription,
+        Runnable,
+        MediaSession.Token,
+        String,
+        PendingIntent,
+        String
+    ) -> Unit
+    lateinit var resumeComponentFoundCallback: (String, Runnable?) -> Unit
+
+    private var mediaBrowser: ResumeMediaBrowser? = null
+
+    private val unlockReceiver = object : BroadcastReceiver() {
+        override fun onReceive(context: Context, intent: Intent) {
+            if (Intent.ACTION_USER_UNLOCKED == intent.action) {
+                loadMediaResumptionControls()
+            }
+        }
+    }
+
+    private val mediaBrowserCallback = object : ResumeMediaBrowser.Callback() {
+        override fun addTrack(
+            desc: MediaDescription,
+            component: ComponentName,
+            browser: ResumeMediaBrowser
+        ) {
+            val token = browser.token
+            val appIntent = browser.appIntent
+            val pm = context.getPackageManager()
+            var appName: CharSequence = component.packageName
+            val resumeAction = getResumeAction(component)
+            try {
+                appName = pm.getApplicationLabel(
+                        pm.getApplicationInfo(component.packageName, 0))
+            } catch (e: PackageManager.NameNotFoundException) {
+                Log.e(TAG, "Error getting package information", e)
+            }
+
+            Log.d(TAG, "Adding resume controls $desc")
+            addTrackToResumeCallback(desc, resumeAction, token, appName.toString(), appIntent,
+                component.packageName)
+        }
+    }
+
+    init {
+        if (useMediaResumption) {
+            val unlockFilter = IntentFilter()
+            unlockFilter.addAction(Intent.ACTION_USER_UNLOCKED)
+            broadcastDispatcher.registerReceiver(unlockReceiver, unlockFilter, null, UserHandle.ALL)
+            loadSavedComponents()
+        }
+    }
+
+    private fun loadSavedComponents() {
+        val userContext = context.createContextAsUser(context.getUser(), 0)
+        val prefs = userContext.getSharedPreferences(MEDIA_PREFERENCES, Context.MODE_PRIVATE)
+        val listString = prefs.getString(MEDIA_PREFERENCE_KEY, null)
+        val components = listString?.split(ResumeMediaBrowser.DELIMITER.toRegex())
+            ?.dropLastWhile { it.isEmpty() }
+        components?.forEach {
+            val info = it.split("/")
+            val packageName = info[0]
+            val className = info[1]
+            val component = ComponentName(packageName, className)
+            resumeComponents.add(component)
+        }
+        Log.d(TAG, "loaded resume components ${resumeComponents.toArray().contentToString()}")
+    }
+
+    /**
+     * Load controls for resuming media, if available
+     */
+    private fun loadMediaResumptionControls() {
+        if (!useMediaResumption) {
+            return
+        }
+
+        resumeComponents.forEach {
+            val browser = ResumeMediaBrowser(context, mediaBrowserCallback, it)
+            browser.findRecentMedia()
+        }
+        broadcastDispatcher.unregisterReceiver(unlockReceiver) // only need to load once
+    }
+
+    override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
+        if (useMediaResumption) {
+            // If this had been started from a resume state, disconnect now that it's live
+            mediaBrowser?.disconnect()
+            // If we don't have a resume action, check if we haven't already
+            if (data.resumeAction == null && !data.hasCheckedForResume) {
+                // TODO also check for a media button receiver intended for restarting (b/154127084)
+                Log.d(TAG, "Checking for service component for " + data.packageName)
+                val pm = context.packageManager
+                val serviceIntent = Intent(MediaBrowserService.SERVICE_INTERFACE)
+                val resumeInfo = pm.queryIntentServices(serviceIntent, 0)
+
+                val inf = resumeInfo?.filter {
+                    it.serviceInfo.packageName == data.packageName
+                }
+                if (inf != null && inf.size > 0) {
+                    backgroundExecutor.execute {
+                        tryUpdateResumptionList(key, inf!!.get(0).componentInfo.componentName)
+                    }
+                } else {
+                    // No service found
+                    resumeComponentFoundCallback(key, null)
+                }
+            }
+        }
+    }
+
+    /**
+     * Verify that we can connect to the given component with a MediaBrowser, and if so, add that
+     * component to the list of resumption components
+     */
+    private fun tryUpdateResumptionList(key: String, componentName: ComponentName) {
+        Log.d(TAG, "Testing if we can connect to $componentName")
+        mediaBrowser?.disconnect()
+        mediaBrowser = ResumeMediaBrowser(context,
+                object : ResumeMediaBrowser.Callback() {
+                    override fun onConnected() {
+                        Log.d(TAG, "yes we can resume with $componentName")
+                        resumeComponentFoundCallback(key, getResumeAction(componentName))
+                        updateResumptionList(componentName)
+                        mediaBrowser?.disconnect()
+                        mediaBrowser = null
+                    }
+
+                    override fun onError() {
+                        Log.e(TAG, "Cannot resume with $componentName")
+                        resumeComponentFoundCallback(key, null)
+                        mediaBrowser?.disconnect()
+                        mediaBrowser = null
+                    }
+                },
+                componentName)
+        mediaBrowser?.testConnection()
+    }
+
+    /**
+     * Add the component to the saved list of media browser services, checking for duplicates and
+     * removing older components that exceed the maximum limit
+     * @param componentName
+     */
+    private fun updateResumptionList(componentName: ComponentName) {
+        // Remove if exists
+        resumeComponents.remove(componentName)
+        // Insert at front of queue
+        resumeComponents.add(componentName)
+        // Remove old components if over the limit
+        if (resumeComponents.size > ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) {
+            resumeComponents.remove()
+        }
+
+        // Save changes
+        val sb = StringBuilder()
+        resumeComponents.forEach {
+            sb.append(it.flattenToString())
+            sb.append(ResumeMediaBrowser.DELIMITER)
+        }
+        val userContext = context.createContextAsUser(context.getUser(), 0)
+        val prefs = userContext.getSharedPreferences(MEDIA_PREFERENCES, Context.MODE_PRIVATE)
+        prefs.edit().putString(MEDIA_PREFERENCE_KEY, sb.toString()).apply()
+    }
+
+    /**
+     * Get a runnable which will resume media playback
+     */
+    private fun getResumeAction(componentName: ComponentName): Runnable {
+        return Runnable {
+            mediaBrowser?.disconnect()
+            mediaBrowser = ResumeMediaBrowser(context,
+                object : ResumeMediaBrowser.Callback() {
+                    override fun onConnected() {
+                        if (mediaBrowser?.token == null) {
+                            Log.e(TAG, "Error after connect")
+                            mediaBrowser?.disconnect()
+                            mediaBrowser = null
+                            return
+                        }
+                        Log.d(TAG, "Connected for restart $componentName")
+                        val controller = MediaController(context, mediaBrowser!!.token)
+                        val controls = controller.transportControls
+                        controls.prepare()
+                        controls.play()
+                    }
+
+                    override fun onError() {
+                        Log.e(TAG, "Resume failed for $componentName")
+                        mediaBrowser?.disconnect()
+                        mediaBrowser = null
+                    }
+                },
+                componentName)
+            mediaBrowser?.restart()
+        }
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt
index 92a1ab1..3c3f4a9 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaTimeoutListener.kt
@@ -45,7 +45,7 @@
 
     lateinit var timeoutCallback: (String, Boolean) -> Unit
 
-    override fun onMediaDataLoaded(key: String, data: MediaData) {
+    override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
         if (mediaListeners.containsKey(key)) {
             return
         }
@@ -67,27 +67,35 @@
 
         var timedOut = false
 
-        private val mediaController = mediaControllerFactory.create(data.token)
+        // Resume controls may have null token
+        private val mediaController = if (data.token != null) {
+            mediaControllerFactory.create(data.token)
+        } else {
+            null
+        }
         private var cancellation: Runnable? = null
 
         init {
-            mediaController.registerCallback(this)
+            mediaController?.registerCallback(this)
         }
 
         fun destroy() {
-            mediaController.unregisterCallback(this)
+            mediaController?.unregisterCallback(this)
         }
 
         override fun onPlaybackStateChanged(state: PlaybackState?) {
             if (DEBUG) {
                 Log.v(TAG, "onPlaybackStateChanged: $state")
             }
-            expireMediaTimeout(key, "playback state ativity - $state, $key")
 
             if (state == null || !isPlayingState(state.state)) {
                 if (DEBUG) {
                     Log.v(TAG, "schedule timeout for $key")
                 }
+                if (cancellation != null) {
+                    if (DEBUG) Log.d(TAG, "cancellation already exists, continuing.")
+                    return
+                }
                 expireMediaTimeout(key, "PLAYBACK STATE CHANGED - $state")
                 cancellation = mainExecutor.executeDelayed({
                     cancellation = null
@@ -98,6 +106,7 @@
                     timeoutCallback(key, timedOut)
                 }, PAUSED_MEDIA_TIMEOUT)
             } else {
+                expireMediaTimeout(key, "playback started - $state, $key")
                 timedOut = false
                 timeoutCallback(key, timedOut)
             }
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt
index 8ab30c7..9b9a6b4 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt
@@ -11,16 +11,12 @@
 import android.widget.LinearLayout
 import androidx.core.view.GestureDetectorCompat
 import com.android.systemui.R
-import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.qs.PageIndicator
 import com.android.systemui.statusbar.notification.VisualStabilityManager
 import com.android.systemui.util.animation.UniqueObjectHostView
 import com.android.systemui.util.animation.requiresRemeasuring
-import com.android.systemui.util.concurrency.DelayableExecutor
-import java.util.concurrent.Executor
 import javax.inject.Inject
+import javax.inject.Provider
 import javax.inject.Singleton
 
 private const val FLING_SLOP = 1000000
@@ -32,10 +28,8 @@
 @Singleton
 class MediaViewManager @Inject constructor(
     private val context: Context,
-    @Main private val foregroundExecutor: Executor,
-    @Background private val backgroundExecutor: DelayableExecutor,
+    private val mediaControlPanelFactory: Provider<MediaControlPanel>,
     private val visualStabilityManager: VisualStabilityManager,
-    private val activityStarter: ActivityStarter,
     private val mediaHostStatesManager: MediaHostStatesManager,
     mediaManager: MediaDataCombineLatest
 ) {
@@ -147,8 +141,8 @@
         visualStabilityManager.addReorderingAllowedCallback(visualStabilityCallback,
                 true /* persistent */)
         mediaManager.addListener(object : MediaDataManager.Listener {
-            override fun onMediaDataLoaded(key: String, data: MediaData) {
-                updateView(key, data)
+            override fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) {
+                updateView(key, oldKey, data)
                 updatePlayerVisibilities()
                 mediaCarousel.requiresRemeasuring = true
             }
@@ -259,11 +253,16 @@
         }
     }
 
-    private fun updateView(key: String, data: MediaData) {
+    private fun updateView(key: String, oldKey: String?, data: MediaData) {
+        // If the key was changed, update entry
+        val oldData = mediaPlayers[oldKey]
+        if (oldData != null) {
+            val oldData = mediaPlayers.remove(oldKey)
+            mediaPlayers.put(key, oldData!!)
+        }
         var existingPlayer = mediaPlayers[key]
         if (existingPlayer == null) {
-            existingPlayer = MediaControlPanel(context, foregroundExecutor, backgroundExecutor,
-                    activityStarter, mediaHostStatesManager)
+            existingPlayer = mediaControlPanelFactory.get()
             existingPlayer.attach(PlayerViewHolder.create(LayoutInflater.from(context),
                     mediaContent))
             mediaPlayers[key] = existingPlayer
@@ -286,7 +285,7 @@
                 needsReordering = true
             }
         }
-        existingPlayer.bind(data)
+        existingPlayer?.bind(data)
         updateMediaPaddings()
         updatePageIndicator()
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSMediaBrowser.java b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java
similarity index 80%
rename from packages/SystemUI/src/com/android/systemui/qs/QSMediaBrowser.java
rename to packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java
index a5b73dc..1e9a303 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSMediaBrowser.java
+++ b/packages/SystemUI/src/com/android/systemui/media/ResumeMediaBrowser.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.systemui.qs;
+package com.android.systemui.media;
 
 import android.app.PendingIntent;
 import android.content.ComponentName;
@@ -27,14 +27,17 @@
 import android.media.session.MediaSession;
 import android.os.Bundle;
 import android.service.media.MediaBrowserService;
+import android.text.TextUtils;
 import android.util.Log;
 
+import com.android.systemui.util.Utils;
+
 import java.util.List;
 
 /**
- * Media browser for managing resumption in QS media controls
+ * Media browser for managing resumption in media controls
  */
-public class QSMediaBrowser {
+public class ResumeMediaBrowser {
 
     /** Maximum number of controls to show on boot */
     public static final int MAX_RESUMPTION_CONTROLS = 5;
@@ -42,7 +45,8 @@
     /** Delimiter for saved component names */
     public static final String DELIMITER = ":";
 
-    private static final String TAG = "QSMediaBrowser";
+    private static final String TAG = "ResumeMediaBrowser";
+    private boolean mIsEnabled = false;
     private final Context mContext;
     private final Callback mCallback;
     private MediaBrowser mMediaBrowser;
@@ -54,21 +58,25 @@
      * @param callback used to report media items found
      * @param componentName Component name of the MediaBrowserService this browser will connect to
      */
-    public QSMediaBrowser(Context context, Callback callback, ComponentName componentName) {
+    public ResumeMediaBrowser(Context context, Callback callback, ComponentName componentName) {
+        mIsEnabled = Utils.useMediaResumption(context);
         mContext = context;
         mCallback = callback;
         mComponentName = componentName;
     }
 
     /**
-     * Connects to the MediaBrowserService and looks for valid media. If a media item is returned
-     * by the service, QSMediaBrowser.Callback#addTrack will be called with its MediaDescription.
-     * QSMediaBrowser.Callback#onConnected and QSMediaBrowser.Callback#onError will also be called
-     * when the initial connection is successful, or an error occurs. Note that it is possible for
-     * the service to connect but for no playable tracks to be found later.
-     * QSMediaBrowser#disconnect will be called automatically with this function.
+     * Connects to the MediaBrowserService and looks for valid media. If a media item is returned,
+     * ResumeMediaBrowser.Callback#addTrack will be called with the MediaDescription.
+     * ResumeMediaBrowser.Callback#onConnected and ResumeMediaBrowser.Callback#onError will also be
+     * called when the initial connection is successful, or an error occurs.
+     * Note that it is possible for the service to connect but for no playable tracks to be found.
+     * ResumeMediaBrowser#disconnect will be called automatically with this function.
      */
     public void findRecentMedia() {
+        if (!mIsEnabled) {
+            return;
+        }
         Log.d(TAG, "Connecting to " + mComponentName);
         disconnect();
         Bundle rootHints = new Bundle();
@@ -86,7 +94,7 @@
         public void onChildrenLoaded(String parentId,
                 List<MediaBrowser.MediaItem> children) {
             if (children.size() == 0) {
-                Log.e(TAG, "No children found for " + mComponentName);
+                Log.d(TAG, "No children found for " + mComponentName);
                 return;
             }
             // We ask apps to return a playable item as the first child when sending
@@ -94,23 +102,24 @@
             MediaBrowser.MediaItem child = children.get(0);
             MediaDescription desc = child.getDescription();
             if (child.isPlayable()) {
-                mCallback.addTrack(desc, mMediaBrowser.getServiceComponent(), QSMediaBrowser.this);
+                mCallback.addTrack(desc, mMediaBrowser.getServiceComponent(),
+                        ResumeMediaBrowser.this);
             } else {
-                Log.e(TAG, "Child found but not playable for " + mComponentName);
+                Log.d(TAG, "Child found but not playable for " + mComponentName);
             }
             disconnect();
         }
 
         @Override
         public void onError(String parentId) {
-            Log.e(TAG, "Subscribe error for " + mComponentName + ": " + parentId);
+            Log.d(TAG, "Subscribe error for " + mComponentName + ": " + parentId);
             mCallback.onError();
             disconnect();
         }
 
         @Override
         public void onError(String parentId, Bundle options) {
-            Log.e(TAG, "Subscribe error for " + mComponentName + ": " + parentId
+            Log.d(TAG, "Subscribe error for " + mComponentName + ": " + parentId
                     + ", options: " + options);
             mCallback.onError();
             disconnect();
@@ -149,7 +158,7 @@
          */
         @Override
         public void onConnectionFailed() {
-            Log.e(TAG, "Connection failed for " + mComponentName);
+            Log.d(TAG, "Connection failed for " + mComponentName);
             mCallback.onError();
             disconnect();
         }
@@ -167,11 +176,15 @@
     }
 
     /**
-     * Connects to the MediaBrowserService and starts playback. QSMediaBrowser.Callback#onError or
-     * QSMediaBrowser.Callback#onConnected will be called depending on whether it was successful.
-     * QSMediaBrowser#disconnect should be called after this to ensure the connection is closed.
+     * Connects to the MediaBrowserService and starts playback.
+     * ResumeMediaBrowser.Callback#onError or ResumeMediaBrowser.Callback#onConnected will be called
+     * depending on whether it was successful.
+     * ResumeMediaBrowser#disconnect should be called after this to ensure the connection is closed.
      */
     public void restart() {
+        if (!mIsEnabled) {
+            return;
+        }
         disconnect();
         Bundle rootHints = new Bundle();
         rootHints.putBoolean(MediaBrowserService.BrowserRoot.EXTRA_RECENT, true);
@@ -224,18 +237,21 @@
 
     /**
      * Used to test if SystemUI is allowed to connect to the given component as a MediaBrowser.
-     * QSMediaBrowser.Callback#onError or QSMediaBrowser.Callback#onConnected will be called
+     * ResumeMediaBrowser.Callback#onError or ResumeMediaBrowser.Callback#onConnected will be called
      * depending on whether it was successful.
-     * QSMediaBrowser#disconnect should be called after this to ensure the connection is closed.
+     * ResumeMediaBrowser#disconnect should be called after this to ensure the connection is closed.
      */
     public void testConnection() {
+        if (!mIsEnabled) {
+            return;
+        }
         disconnect();
         final MediaBrowser.ConnectionCallback connectionCallback =
                 new MediaBrowser.ConnectionCallback() {
                     @Override
                     public void onConnected() {
                         Log.d(TAG, "connected");
-                        if (mMediaBrowser.getRoot() == null) {
+                        if (TextUtils.isEmpty(mMediaBrowser.getRoot())) {
                             mCallback.onError();
                         } else {
                             mCallback.onConnected();
@@ -264,7 +280,7 @@
     }
 
     /**
-     * Interface to handle results from QSMediaBrowser
+     * Interface to handle results from ResumeMediaBrowser
      */
     public static class Callback {
         /**
@@ -286,7 +302,7 @@
          * @param browser reference to the browser
          */
         public void addTrack(MediaDescription track, ComponentName component,
-                QSMediaBrowser browser) {
+                ResumeMediaBrowser browser) {
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt
index 06821cd6..efc476d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/SeekBarViewModel.kt
@@ -27,8 +27,10 @@
 import androidx.annotation.WorkerThread
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.LiveData
-
-import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.util.concurrency.RepeatableExecutor
+import java.util.concurrent.Executor
+import javax.inject.Inject
 
 private const val POSITION_UPDATE_INTERVAL_MILLIS = 100L
 
@@ -65,7 +67,7 @@
 }
 
 /** ViewModel for seek bar in QS media player. */
-class SeekBarViewModel(val bgExecutor: DelayableExecutor) {
+class SeekBarViewModel @Inject constructor(@Background private val bgExecutor: RepeatableExecutor) {
 
     private var _data = Progress(false, false, null, null)
         set(value) {
@@ -89,17 +91,25 @@
     private var callback = object : MediaController.Callback() {
         override fun onPlaybackStateChanged(state: PlaybackState) {
             playbackState = state
-            if (shouldPollPlaybackPosition()) {
-                checkPlaybackPosition()
+            if (PlaybackState.STATE_NONE.equals(playbackState)) {
+                clearController()
+            } else {
+                checkIfPollingNeeded()
             }
         }
+
+        override fun onSessionDestroyed() {
+            clearController()
+        }
     }
+    private var cancel: Runnable? = null
 
     /** Listening state (QS open or closed) is used to control polling of progress. */
     var listening = true
-        set(value) {
-            if (value) {
-                checkPlaybackPosition()
+        set(value) = bgExecutor.execute {
+            if (field != value) {
+                field = value
+                checkIfPollingNeeded()
             }
         }
 
@@ -131,9 +141,7 @@
                 playbackState?.getState() == PlaybackState.STATE_NONE ||
                 (duration != null && duration <= 0)) false else true
         _data = Progress(enabled, seekAvailable, position, duration)
-        if (shouldPollPlaybackPosition()) {
-            checkPlaybackPosition()
-        }
+        checkIfPollingNeeded()
     }
 
     /**
@@ -145,6 +153,8 @@
     fun clearController() = bgExecutor.execute {
         controller = null
         playbackState = null
+        cancel?.run()
+        cancel = null
         _data = _data.copy(enabled = false)
     }
 
@@ -152,26 +162,34 @@
      * Call to clean up any resources.
      */
     @AnyThread
-    fun onDestroy() {
+    fun onDestroy() = bgExecutor.execute {
         controller = null
         playbackState = null
+        cancel?.run()
+        cancel = null
     }
 
-    @AnyThread
-    private fun checkPlaybackPosition(): Runnable = bgExecutor.executeDelayed({
+    @WorkerThread
+    private fun checkPlaybackPosition() {
         val duration = _data.duration ?: -1
         val currentPosition = playbackState?.computePosition(duration.toLong())?.toInt()
         if (currentPosition != null && _data.elapsedTime != currentPosition) {
             _data = _data.copy(elapsedTime = currentPosition)
         }
-        if (shouldPollPlaybackPosition()) {
-            checkPlaybackPosition()
-        }
-    }, POSITION_UPDATE_INTERVAL_MILLIS)
+    }
 
     @WorkerThread
-    private fun shouldPollPlaybackPosition(): Boolean {
-        return listening && playbackState?.isInMotion() ?: false
+    private fun checkIfPollingNeeded() {
+        val needed = listening && playbackState?.isInMotion() ?: false
+        if (needed) {
+            if (cancel == null) {
+                cancel = bgExecutor.executeRepeatedly(this::checkPlaybackPosition, 0L,
+                        POSITION_UPDATE_INTERVAL_MILLIS)
+            }
+        } else {
+            cancel?.run()
+            cancel = null
+        }
     }
 
     /** Gets a listener to attach to the seek bar to handle seeking. */
@@ -188,7 +206,7 @@
 
     private class SeekBarChangeListener(
         val viewModel: SeekBarViewModel,
-        val bgExecutor: DelayableExecutor
+        val bgExecutor: Executor
     ) : SeekBar.OnSeekBarChangeListener {
         override fun onProgressChanged(bar: SeekBar, progress: Int, fromUser: Boolean) {
             if (fromUser) {
diff --git a/packages/SystemUI/src/com/android/systemui/pip/PipAnimationController.java b/packages/SystemUI/src/com/android/systemui/pip/PipAnimationController.java
index 7f7e108..03b1ddc 100644
--- a/packages/SystemUI/src/com/android/systemui/pip/PipAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/pip/PipAnimationController.java
@@ -17,6 +17,7 @@
 package com.android.systemui.pip;
 
 import android.animation.Animator;
+import android.animation.RectEvaluator;
 import android.animation.ValueAnimator;
 import android.annotation.IntDef;
 import android.content.Context;
@@ -26,6 +27,7 @@
 import android.view.animation.Interpolator;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.Interpolators;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -74,15 +76,12 @@
                 || direction == TRANSITION_DIRECTION_TO_SPLIT_SCREEN;
     }
 
-    private final Interpolator mFastOutSlowInInterpolator;
     private final PipSurfaceTransactionHelper mSurfaceTransactionHelper;
 
     private PipTransitionAnimator mCurrentAnimator;
 
     @Inject
     PipAnimationController(Context context, PipSurfaceTransactionHelper helper) {
-        mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(context,
-                com.android.internal.R.interpolator.fast_out_slow_in);
         mSurfaceTransactionHelper = helper;
     }
 
@@ -104,10 +103,11 @@
     }
 
     @SuppressWarnings("unchecked")
-    PipTransitionAnimator getAnimator(SurfaceControl leash, Rect startBounds, Rect endBounds) {
+    PipTransitionAnimator getAnimator(SurfaceControl leash, Rect startBounds, Rect endBounds,
+            Rect sourceHintRect) {
         if (mCurrentAnimator == null) {
             mCurrentAnimator = setupPipTransitionAnimator(
-                    PipTransitionAnimator.ofBounds(leash, startBounds, endBounds));
+                    PipTransitionAnimator.ofBounds(leash, startBounds, endBounds, sourceHintRect));
         } else if (mCurrentAnimator.getAnimationType() == ANIM_TYPE_ALPHA
                 && mCurrentAnimator.isRunning()) {
             // If we are still animating the fade into pip, then just move the surface and ensure
@@ -122,7 +122,7 @@
         } else {
             mCurrentAnimator.cancel();
             mCurrentAnimator = setupPipTransitionAnimator(
-                    PipTransitionAnimator.ofBounds(leash, startBounds, endBounds));
+                    PipTransitionAnimator.ofBounds(leash, startBounds, endBounds, sourceHintRect));
         }
         return mCurrentAnimator;
     }
@@ -133,7 +133,7 @@
 
     private PipTransitionAnimator setupPipTransitionAnimator(PipTransitionAnimator animator) {
         animator.setSurfaceTransactionHelper(mSurfaceTransactionHelper);
-        animator.setInterpolator(mFastOutSlowInInterpolator);
+        animator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN);
         animator.setFloatValues(FRACTION_START, FRACTION_END);
         return animator;
     }
@@ -331,6 +331,7 @@
                 @Override
                 void onStartTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) {
                     getSurfaceTransactionHelper()
+                            .resetScale(tx, leash, getDestinationBounds())
                             .crop(tx, leash, getDestinationBounds())
                             .round(tx, leash, shouldApplyCornerRadius());
                     tx.show(leash);
@@ -346,35 +347,46 @@
         }
 
         static PipTransitionAnimator<Rect> ofBounds(SurfaceControl leash,
-                Rect startValue, Rect endValue) {
+                Rect startValue, Rect endValue, Rect sourceHintRect) {
+            // Just for simplicity we'll interpolate between the source rect hint insets and empty
+            // insets to calculate the window crop
+            final Rect initialStartValue = new Rect(startValue);
+            final Rect sourceHintRectInsets = sourceHintRect != null
+                    ? new Rect(sourceHintRect.left - startValue.left,
+                            sourceHintRect.top - startValue.top,
+                            startValue.right - sourceHintRect.right,
+                            startValue.bottom - sourceHintRect.bottom)
+                    : null;
+            final Rect sourceInsets = new Rect(0, 0, 0, 0);
+
             // construct new Rect instances in case they are recycled
             return new PipTransitionAnimator<Rect>(leash, ANIM_TYPE_BOUNDS,
                     endValue, new Rect(startValue), new Rect(endValue)) {
-                private final Rect mTmpRect = new Rect();
-
-                private int getCastedFractionValue(float start, float end, float fraction) {
-                    return (int) (start * (1 - fraction) + end * fraction + .5f);
-                }
+                private final RectEvaluator mRectEvaluator = new RectEvaluator(new Rect());
+                private final RectEvaluator mInsetsEvaluator = new RectEvaluator(new Rect());
 
                 @Override
                 void applySurfaceControlTransaction(SurfaceControl leash,
                         SurfaceControl.Transaction tx, float fraction) {
                     final Rect start = getStartValue();
                     final Rect end = getEndValue();
-                    mTmpRect.set(
-                            getCastedFractionValue(start.left, end.left, fraction),
-                            getCastedFractionValue(start.top, end.top, fraction),
-                            getCastedFractionValue(start.right, end.right, fraction),
-                            getCastedFractionValue(start.bottom, end.bottom, fraction));
-                    setCurrentValue(mTmpRect);
+                    Rect bounds = mRectEvaluator.evaluate(fraction, start, end);
+                    setCurrentValue(bounds);
                     if (inScaleTransition()) {
                         if (isOutPipDirection(getTransitionDirection())) {
-                            getSurfaceTransactionHelper().scale(tx, leash, end, mTmpRect);
+                            getSurfaceTransactionHelper().scale(tx, leash, end, bounds);
                         } else {
-                            getSurfaceTransactionHelper().scale(tx, leash, start, mTmpRect);
+                            getSurfaceTransactionHelper().scale(tx, leash, start, bounds);
                         }
                     } else {
-                        getSurfaceTransactionHelper().crop(tx, leash, mTmpRect);
+                        if (sourceHintRectInsets != null) {
+                            Rect insets = mInsetsEvaluator.evaluate(fraction, sourceInsets,
+                                    sourceHintRectInsets);
+                            getSurfaceTransactionHelper().scaleAndCrop(tx, leash, initialStartValue,
+                                    bounds, insets);
+                        } else {
+                            getSurfaceTransactionHelper().scale(tx, leash, start, bounds);
+                        }
                     }
                     tx.apply();
                 }
@@ -390,11 +402,11 @@
 
                 @Override
                 void onEndTransaction(SurfaceControl leash, SurfaceControl.Transaction tx) {
-                    if (!inScaleTransition()) return;
                     // NOTE: intentionally does not apply the transaction here.
                     // this end transaction should get executed synchronously with the final
                     // WindowContainerTransaction in task organizer
-                    getSurfaceTransactionHelper().resetScale(tx, leash, getDestinationBounds())
+                    getSurfaceTransactionHelper()
+                            .resetScale(tx, leash, getDestinationBounds())
                             .crop(tx, leash, getDestinationBounds());
                 }
 
diff --git a/packages/SystemUI/src/com/android/systemui/pip/PipBoundsHandler.java b/packages/SystemUI/src/com/android/systemui/pip/PipBoundsHandler.java
index 0d3a16e..2657694 100644
--- a/packages/SystemUI/src/com/android/systemui/pip/PipBoundsHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/pip/PipBoundsHandler.java
@@ -296,6 +296,14 @@
      */
     public boolean onDisplayRotationChanged(Rect outBounds, Rect oldBounds, Rect outInsetBounds,
             int displayId, int fromRotation, int toRotation, WindowContainerTransaction t) {
+        // Calculate the snap fraction of the current stack along the old movement bounds
+        final Rect postChangeStackBounds = new Rect(oldBounds);
+        final float snapFraction = getSnapFraction(postChangeStackBounds);
+
+        // Update the display layout, note that we have to do this on every rotation even if we
+        // aren't in PIP since we need to update the display layout to get the right resources
+        mDisplayLayout.rotateTo(mContext.getResources(), toRotation);
+
         // Bail early if the event is not sent to current {@link #mDisplayInfo}
         if ((displayId != mDisplayInfo.displayId) || (fromRotation == toRotation)) {
             return false;
@@ -312,13 +320,6 @@
             return false;
         }
 
-        // Calculate the snap fraction of the current stack along the old movement bounds
-        final Rect postChangeStackBounds = new Rect(oldBounds);
-        final float snapFraction = getSnapFraction(postChangeStackBounds);
-
-        // Update the display layout
-        mDisplayLayout.rotateTo(mContext.getResources(), toRotation);
-
         // Populate the new {@link #mDisplayInfo}.
         // The {@link DisplayInfo} queried from DisplayManager would be the one before rotation,
         // therefore, the width/height may require a swap first.
diff --git a/packages/SystemUI/src/com/android/systemui/pip/PipSurfaceTransactionHelper.java b/packages/SystemUI/src/com/android/systemui/pip/PipSurfaceTransactionHelper.java
index fc41d2e..65ea887 100644
--- a/packages/SystemUI/src/com/android/systemui/pip/PipSurfaceTransactionHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/pip/PipSurfaceTransactionHelper.java
@@ -44,6 +44,7 @@
     private final float[] mTmpFloat9 = new float[9];
     private final RectF mTmpSourceRectF = new RectF();
     private final RectF mTmpDestinationRectF = new RectF();
+    private final Rect mTmpDestinationRect = new Rect();
 
     @Inject
     public PipSurfaceTransactionHelper(Context context, ConfigurationController configController) {
@@ -90,7 +91,30 @@
         mTmpDestinationRectF.set(destinationBounds);
         mTmpTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL);
         tx.setMatrix(leash, mTmpTransform, mTmpFloat9)
-                .setPosition(leash, destinationBounds.left, destinationBounds.top);
+                .setPosition(leash, mTmpDestinationRectF.left, mTmpDestinationRectF.top);
+        return this;
+    }
+
+    /**
+     * Operates the scale (setMatrix) on a given transaction and leash
+     * @return same {@link PipSurfaceTransactionHelper} instance for method chaining
+     */
+    PipSurfaceTransactionHelper scaleAndCrop(SurfaceControl.Transaction tx, SurfaceControl leash,
+            Rect sourceBounds, Rect destinationBounds, Rect insets) {
+        mTmpSourceRectF.set(sourceBounds);
+        mTmpDestinationRect.set(sourceBounds);
+        mTmpDestinationRect.inset(insets);
+        // Scale by the shortest edge and offset such that the top/left of the scaled inset source
+        // rect aligns with the top/left of the destination bounds
+        final float scale = sourceBounds.width() <= sourceBounds.height()
+                ? (float) destinationBounds.width() / sourceBounds.width()
+                : (float) destinationBounds.height() / sourceBounds.height();
+        final float left = destinationBounds.left - insets.left * scale;
+        final float top = destinationBounds.top - insets.top * scale;
+        mTmpTransform.setScale(scale, scale);
+        tx.setMatrix(leash, mTmpTransform, mTmpFloat9)
+                .setWindowCrop(leash, mTmpDestinationRect)
+                .setPosition(leash, left, top);
         return this;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java b/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java
index c6f144a..42e0c56 100644
--- a/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java
+++ b/packages/SystemUI/src/com/android/systemui/pip/PipTaskOrganizer.java
@@ -143,8 +143,10 @@
             case MSG_RESIZE_ANIMATE: {
                 Rect currentBounds = (Rect) args.arg2;
                 Rect toBounds = (Rect) args.arg3;
+                Rect sourceHintRect = (Rect) args.arg4;
                 int duration = args.argi2;
-                animateResizePip(currentBounds, toBounds, args.argi1 /* direction */, duration);
+                animateResizePip(currentBounds, toBounds, sourceHintRect,
+                        args.argi1 /* direction */, duration);
                 if (updateBoundsCallback != null) {
                     updateBoundsCallback.accept(toBounds);
                 }
@@ -294,7 +296,8 @@
                 public void onTransactionReady(int id, SurfaceControl.Transaction t) {
                     t.apply();
                     scheduleAnimateResizePip(mLastReportedBounds, destinationBounds,
-                            direction, animationDurationMs, null /* updateBoundsCallback */);
+                            null /* sourceHintRect */, direction, animationDurationMs,
+                            null /* updateBoundsCallback */);
                     mInPip = false;
                 }
             });
@@ -357,7 +360,8 @@
         final Rect currentBounds = mTaskInfo.configuration.windowConfiguration.getBounds();
 
         if (mOneShotAnimationType == ANIM_TYPE_BOUNDS) {
-            scheduleAnimateResizePip(currentBounds, destinationBounds,
+            final Rect sourceHintRect = getValidSourceHintRect(info, currentBounds);
+            scheduleAnimateResizePip(currentBounds, destinationBounds, sourceHintRect,
                     TRANSITION_DIRECTION_TO_PIP, mEnterExitAnimationDuration,
                     null /* updateBoundsCallback */);
         } else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) {
@@ -368,6 +372,21 @@
         }
     }
 
+    /**
+     * Returns the source hint rect if it is valid (if provided and is contained by the current
+     * task bounds).
+     */
+    private Rect getValidSourceHintRect(ActivityManager.RunningTaskInfo info, Rect sourceBounds) {
+        final Rect sourceHintRect = info.pictureInPictureParams != null
+                && info.pictureInPictureParams.hasSourceBoundsHint()
+                ? info.pictureInPictureParams.getSourceRectHint()
+                : null;
+        if (sourceHintRect != null && sourceBounds.contains(sourceHintRect)) {
+            return sourceHintRect;
+        }
+        return null;
+    }
+
     private void enterPipWithAlphaAnimation(Rect destinationBounds, long durationMs) {
         // If we are fading the PIP in, then we should move the pip to the final location as
         // soon as possible, but set the alpha immediately since the transaction can take a
@@ -552,13 +571,13 @@
             Log.d(TAG, "skip scheduleAnimateResizePip, entering pip deferred");
             return;
         }
-        scheduleAnimateResizePip(mLastReportedBounds, toBounds,
+        scheduleAnimateResizePip(mLastReportedBounds, toBounds, null /* sourceHintRect */,
                 TRANSITION_DIRECTION_NONE, duration, updateBoundsCallback);
     }
 
     private void scheduleAnimateResizePip(Rect currentBounds, Rect destinationBounds,
-            @PipAnimationController.TransitionDirection int direction, int durationMs,
-            Consumer<Rect> updateBoundsCallback) {
+            Rect sourceHintRect, @PipAnimationController.TransitionDirection int direction,
+            int durationMs, Consumer<Rect> updateBoundsCallback) {
         if (!mInPip) {
             // can be initiated in other component, ignore if we are no longer in PIP
             return;
@@ -568,6 +587,7 @@
         args.arg1 = updateBoundsCallback;
         args.arg2 = currentBounds;
         args.arg3 = destinationBounds;
+        args.arg4 = sourceHintRect;
         args.argi1 = direction;
         args.argi2 = durationMs;
         mUpdateHandler.sendMessage(mUpdateHandler.obtainMessage(MSG_RESIZE_ANIMATE, args));
@@ -667,7 +687,8 @@
         }
         final Rect destinationBounds = new Rect(originalBounds);
         destinationBounds.offset(xOffset, yOffset);
-        animateResizePip(originalBounds, destinationBounds, TRANSITION_DIRECTION_SAME, durationMs);
+        animateResizePip(originalBounds, destinationBounds, null /* sourceHintRect */,
+                TRANSITION_DIRECTION_SAME, durationMs);
     }
 
     private void resizePip(Rect destinationBounds) {
@@ -745,7 +766,7 @@
         WindowOrganizer.applyTransaction(wct);
     }
 
-    private void animateResizePip(Rect currentBounds, Rect destinationBounds,
+    private void animateResizePip(Rect currentBounds, Rect destinationBounds, Rect sourceHintRect,
             @PipAnimationController.TransitionDirection int direction, int durationMs) {
         if (Looper.myLooper() != mUpdateHandler.getLooper()) {
             throw new RuntimeException("Callers should call scheduleAnimateResizePip() instead of "
@@ -757,7 +778,7 @@
             return;
         }
         mPipAnimationController
-                .getAnimator(mLeash, currentBounds, destinationBounds)
+                .getAnimator(mLeash, currentBounds, destinationBounds, sourceHintRect)
                 .setTransitionDirection(direction)
                 .setPipAnimationCallback(mPipAnimationCallback)
                 .setDuration(durationMs)
diff --git a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java
index c274ee9..7a9dcde 100644
--- a/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/pip/phone/PipTouchHandler.java
@@ -161,6 +161,7 @@
     private float mSavedSnapFraction = -1f;
     private boolean mSendingHoverAccessibilityEvents;
     private boolean mMovementWithinDismiss;
+    private boolean mHideMenuAfterShown = false;
     private PipAccessibilityInteractionConnection mConnection;
 
     // Touch state
@@ -677,6 +678,7 @@
                 break;
             }
             case MotionEvent.ACTION_HOVER_EXIT: {
+                mHideMenuAfterShown = true;
                 // If Touch Exploration is enabled, some a11y services (e.g. Talkback) is probably
                 // on and changing MotionEvents into HoverEvents.
                 // Let's not enable menu show/hide for a11y services.
@@ -767,6 +769,9 @@
                 mSavedSnapFraction = mMotionHelper.animateToExpandedState(expandedBounds,
                         mMovementBounds, mExpandedMovementBounds, callback);
             }
+            if (mHideMenuAfterShown) {
+                mMenuController.hideMenu();
+            }
         } else if (menuState == MENU_STATE_NONE && mMenuState == MENU_STATE_FULL) {
             // Try and restore the PiP to the closest edge, using the saved snap fraction
             // if possible
@@ -804,6 +809,7 @@
             }
         }
         mMenuState = menuState;
+        mHideMenuAfterShown = false;
         updateMovementBounds();
         // If pip menu has dismissed, we should register the A11y ActionReplacingConnection for pip
         // as well, or it can't handle a11y focus and pip menu can't perform any action.
diff --git a/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java b/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java
index 0d61449..2c76d70 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/PageIndicator.java
@@ -45,9 +45,6 @@
     }
 
     public void setNumPages(int numPages) {
-        if (numPages == getChildCount()) {
-            return;
-        }
         TypedArray array = getContext().obtainStyledAttributes(
                 new int[]{android.R.attr.colorControlActivated});
         int color = array.getColor(0, 0);
@@ -55,12 +52,12 @@
         setNumPages(numPages, color);
     }
 
-    /** Oveload of setNumPages that allows the indicator color to be specified.*/
+    /** Overload of setNumPages that allows the indicator color to be specified.*/
     public void setNumPages(int numPages, int color) {
+        setVisibility(numPages > 1 ? View.VISIBLE : View.GONE);
         if (numPages == getChildCount()) {
             return;
         }
-        setVisibility(numPages > 1 ? View.VISIBLE : View.GONE);
         if (mAnimating) {
             Log.w(TAG, "setNumPages during animation");
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
index 4008918..65d3572 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
@@ -355,10 +355,23 @@
     }
 
     public void addTile(ComponentName tile) {
+        addTile(tile, /* end */ false);
+    }
+
+    /**
+     * Adds a custom tile to the set of current tiles.
+     * @param tile the component name of the {@link android.service.quicksettings.TileService}
+     * @param end if true, the tile will be added at the end. If false, at the beginning.
+     */
+    public void addTile(ComponentName tile, boolean end) {
         String spec = CustomTile.toSpec(tile);
         if (!mTileSpecs.contains(spec)) {
             List<String> newSpecs = new ArrayList<>(mTileSpecs);
-            newSpecs.add(0, spec);
+            if (end) {
+                newSpecs.add(spec);
+            } else {
+                newSpecs.add(0, spec);
+            }
             changeTiles(mTileSpecs, newSpecs);
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
index 191d475..94b4cee 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
@@ -41,7 +41,6 @@
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.tuner.TunerService;
 import com.android.systemui.tuner.TunerService.Tunable;
-import com.android.systemui.util.Utils;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -82,8 +81,7 @@
             MediaHost mediaHost,
             UiEventLogger uiEventLogger
     ) {
-        super(context, attrs, dumpManager, broadcastDispatcher, qsLogger, mediaHost,
-                uiEventLogger);
+        super(context, attrs, dumpManager, broadcastDispatcher, qsLogger, mediaHost, uiEventLogger);
         if (mFooter != null) {
             removeView(mFooter.getView());
         }
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
index 3e268f6..2ddd6aa 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
@@ -33,9 +33,7 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.provider.Settings;
-import android.util.DisplayMetrics;
 import android.util.Log;
-import android.util.Size;
 import android.widget.Toast;
 
 import com.android.systemui.R;
@@ -247,7 +245,8 @@
         startForeground(NOTIFICATION_RECORDING_ID, notification);
     }
 
-    private Notification createSaveNotification(Uri uri) {
+    private Notification createSaveNotification(ScreenMediaRecorder.SavedRecording recording) {
+        Uri uri = recording.getUri();
         Intent viewIntent = new Intent(Intent.ACTION_VIEW)
                 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION)
                 .setDataAndType(uri, "video/mp4");
@@ -290,16 +289,7 @@
                 .addExtras(extras);
 
         // Add thumbnail if available
-        Bitmap thumbnailBitmap = null;
-        try {
-            ContentResolver resolver = getContentResolver();
-            DisplayMetrics metrics = getResources().getDisplayMetrics();
-            Size size = new Size(metrics.widthPixels, metrics.heightPixels / 2);
-            thumbnailBitmap = resolver.loadThumbnail(uri, size, null);
-        } catch (IOException e) {
-            Log.e(TAG, "Error creating thumbnail: " + e.getMessage());
-            e.printStackTrace();
-        }
+        Bitmap thumbnailBitmap = recording.getThumbnail();
         if (thumbnailBitmap != null) {
             Notification.BigPictureStyle pictureStyle = new Notification.BigPictureStyle()
                     .bigPicture(thumbnailBitmap)
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java
index 8551c88..1c7d987 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java
@@ -22,13 +22,17 @@
 import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.MIC;
 import static com.android.systemui.screenrecord.ScreenRecordingAudioSource.MIC_AND_INTERNAL;
 
+import android.annotation.Nullable;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
+import android.graphics.Bitmap;
 import android.hardware.display.DisplayManager;
 import android.hardware.display.VirtualDisplay;
+import android.media.MediaCodecInfo;
 import android.media.MediaMuxer;
 import android.media.MediaRecorder;
+import android.media.ThumbnailUtils;
 import android.media.projection.IMediaProjection;
 import android.media.projection.IMediaProjectionManager;
 import android.media.projection.MediaProjection;
@@ -40,6 +44,7 @@
 import android.provider.MediaStore;
 import android.util.DisplayMetrics;
 import android.util.Log;
+import android.util.Size;
 import android.view.Surface;
 import android.view.WindowManager;
 
@@ -125,6 +130,9 @@
         int vidBitRate = screenHeight * screenWidth * refereshRate / VIDEO_FRAME_RATE
                 * VIDEO_FRAME_RATE_TO_RESOLUTION_RATIO;
         mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
+        mMediaRecorder.setVideoEncodingProfileLevel(
+                MediaCodecInfo.CodecProfileLevel.AVCProfileHigh,
+                MediaCodecInfo.CodecProfileLevel.AVCLevel42);
         mMediaRecorder.setVideoSize(screenWidth, screenHeight);
         mMediaRecorder.setVideoFrameRate(refereshRate);
         mMediaRecorder.setVideoEncodingBitRate(vidBitRate);
@@ -206,7 +214,7 @@
     /**
      * Store recorded video
      */
-    Uri save() throws IOException {
+    protected SavedRecording save() throws IOException {
         String fileName = new SimpleDateFormat("'screen-'yyyyMMdd-HHmmss'.mp4'")
                 .format(new Date());
 
@@ -244,8 +252,38 @@
         OutputStream os = resolver.openOutputStream(itemUri, "w");
         Files.copy(mTempVideoFile.toPath(), os);
         os.close();
-        mTempVideoFile.delete();
         if (mTempAudioFile != null) mTempAudioFile.delete();
-        return itemUri;
+        DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
+        Size size = new Size(metrics.widthPixels, metrics.heightPixels);
+        SavedRecording recording = new SavedRecording(itemUri, mTempVideoFile, size);
+        mTempVideoFile.delete();
+        return recording;
+    }
+
+    /**
+    * Object representing the recording
+    */
+    public class SavedRecording {
+
+        private Uri mUri;
+        private Bitmap mThumbnailBitmap;
+
+        protected SavedRecording(Uri uri, File file, Size thumbnailSize) {
+            mUri = uri;
+            try {
+                mThumbnailBitmap = ThumbnailUtils.createVideoThumbnail(
+                        file, thumbnailSize, null);
+            } catch (IOException e) {
+                Log.e(TAG, "Error creating thumbnail", e);
+            }
+        }
+
+        public Uri getUri() {
+            return mUri;
+        }
+
+        public @Nullable Bitmap getThumbnail() {
+            return mThumbnailBitmap;
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java
index 9b1734d..a624479 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/GlobalScreenshot.java
@@ -643,6 +643,18 @@
      */
     private void showUiOnActionsReady(SavedImageData imageData) {
         logSuccessOnActionsReady(imageData);
+
+        AccessibilityManager accessibilityManager = (AccessibilityManager)
+                mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
+        long timeoutMs = accessibilityManager.getRecommendedTimeoutMillis(
+                SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS,
+                AccessibilityManager.FLAG_CONTENT_CONTROLS);
+
+        mScreenshotHandler.removeMessages(MESSAGE_CORNER_TIMEOUT);
+        mScreenshotHandler.sendMessageDelayed(
+                mScreenshotHandler.obtainMessage(MESSAGE_CORNER_TIMEOUT),
+                timeoutMs);
+
         if (imageData.uri != null) {
             mScreenshotHandler.post(() -> {
                 if (mScreenshotAnimation != null && mScreenshotAnimation.isRunning()) {
@@ -656,17 +668,6 @@
                 } else {
                     createScreenshotActionsShadeAnimation(imageData).start();
                 }
-
-                AccessibilityManager accessibilityManager = (AccessibilityManager)
-                        mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
-                long timeoutMs = accessibilityManager.getRecommendedTimeoutMillis(
-                        SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS,
-                        AccessibilityManager.FLAG_CONTENT_CONTROLS);
-
-                mScreenshotHandler.removeMessages(MESSAGE_CORNER_TIMEOUT);
-                mScreenshotHandler.sendMessageDelayed(
-                        mScreenshotHandler.obtainMessage(MESSAGE_CORNER_TIMEOUT),
-                        timeoutMs);
             });
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
index a5bab21..221174f 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/SaveImageInBackgroundTask.java
@@ -123,6 +123,7 @@
         if (isCancelled()) {
             return null;
         }
+        Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
 
         ContentResolver resolver = mContext.getContentResolver();
         Bitmap image = mParams.image;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
index 217148d..5628a24 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationMediaManager.java
@@ -52,7 +52,6 @@
 import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.media.MediaDataManager;
-import com.android.systemui.media.MediaDeviceManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.dagger.StatusBarModule;
 import com.android.systemui.statusbar.notification.NotificationEntryListener;
@@ -102,6 +101,12 @@
         PAUSED_MEDIA_STATES.add(PlaybackState.STATE_PAUSED);
         PAUSED_MEDIA_STATES.add(PlaybackState.STATE_ERROR);
     }
+    private static final HashSet<Integer> INACTIVE_MEDIA_STATES = new HashSet<>();
+    static {
+        INACTIVE_MEDIA_STATES.add(PlaybackState.STATE_NONE);
+        INACTIVE_MEDIA_STATES.add(PlaybackState.STATE_STOPPED);
+        INACTIVE_MEDIA_STATES.add(PlaybackState.STATE_ERROR);
+    }
 
     private final NotificationEntryManager mEntryManager;
     private final MediaDataManager mMediaDataManager;
@@ -190,8 +195,7 @@
             KeyguardBypassController keyguardBypassController,
             @Main DelayableExecutor mainExecutor,
             DeviceConfigProxy deviceConfig,
-            MediaDataManager mediaDataManager,
-            MediaDeviceManager mediaDeviceManager) {
+            MediaDataManager mediaDataManager) {
         mContext = context;
         mMediaArtworkProcessor = mediaArtworkProcessor;
         mKeyguardBypassController = keyguardBypassController;
@@ -212,13 +216,11 @@
             @Override
             public void onPendingEntryAdded(NotificationEntry entry) {
                 mediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn());
-                mediaDeviceManager.onNotificationAdded(entry.getKey(), entry.getSbn());
             }
 
             @Override
             public void onPreEntryUpdated(NotificationEntry entry) {
                 mediaDataManager.onNotificationAdded(entry.getKey(), entry.getSbn());
-                mediaDeviceManager.onNotificationAdded(entry.getKey(), entry.getSbn());
             }
 
             @Override
@@ -239,7 +241,6 @@
                     int reason) {
                 onNotificationRemoved(entry.getKey());
                 mediaDataManager.onNotificationRemoved(entry.getKey());
-                mediaDeviceManager.onNotificationRemoved(entry.getKey());
             }
         });
 
@@ -252,10 +253,24 @@
                 mPropertiesChangedListener);
     }
 
+    /**
+     * Check if a state should be considered actively playing
+     * @param state a PlaybackState
+     * @return true if playing
+     */
     public static boolean isPlayingState(int state) {
         return !PAUSED_MEDIA_STATES.contains(state);
     }
 
+    /**
+     * Check if a state should be considered active (playing or paused)
+     * @param state a PlaybackState
+     * @return true if active
+     */
+    public static boolean isActiveState(int state) {
+        return !INACTIVE_MEDIA_STATES.contains(state);
+    }
+
     public void setUpWithPresenter(NotificationPresenter presenter) {
         mPresenter = presenter;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java
index 3dda15b..a8c0324 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java
@@ -42,7 +42,6 @@
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
 import com.android.systemui.util.Assert;
-import com.android.systemui.util.Utils;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -150,9 +149,7 @@
         final int N = activeNotifications.size();
         for (int i = 0; i < N; i++) {
             NotificationEntry ent = activeNotifications.get(i);
-            boolean hideMedia = Utils.useQsMediaPlayer(mContext);
             if (ent.isRowDismissed() || ent.isRowRemoved()
-                    || (ent.isMediaNotification() && hideMedia)
                     || mBubbleController.isBubbleNotificationSuppressedFromShade(ent)
                     || mFgsSectionController.hasEntry(ent)) {
                 // we don't want to update removed notifications because they could
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
index c988e12..84c8db3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarDependenciesModule.java
@@ -24,7 +24,6 @@
 import com.android.systemui.bubbles.BubbleController;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.media.MediaDataManager;
-import com.android.systemui.media.MediaDeviceManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.ActionClickLogger;
 import com.android.systemui.statusbar.CommandQueue;
@@ -51,8 +50,6 @@
 import com.android.systemui.util.DeviceConfigProxy;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 
-import java.util.concurrent.Executor;
-
 import javax.inject.Singleton;
 
 import dagger.Lazy;
@@ -105,8 +102,7 @@
             KeyguardBypassController keyguardBypassController,
             @Main DelayableExecutor mainExecutor,
             DeviceConfigProxy deviceConfigProxy,
-            MediaDataManager mediaDataManager,
-            MediaDeviceManager mediaDeviceManager) {
+            MediaDataManager mediaDataManager) {
         return new NotificationMediaManager(
                 context,
                 statusBarLazy,
@@ -116,8 +112,7 @@
                 keyguardBypassController,
                 mainExecutor,
                 deviceConfigProxy,
-                mediaDataManager,
-                mediaDeviceManager);
+                mediaDataManager);
     }
 
     /** */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationFilter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationFilter.java
index 3afd623..6335a09 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationFilter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationFilter.java
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.notification;
 
+import static com.android.systemui.media.MediaDataManagerKt.isMediaNotification;
+
 import android.Manifest;
 import android.app.AppGlobals;
 import android.app.Notification;
@@ -27,6 +29,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.systemui.Dependency;
 import com.android.systemui.ForegroundServiceController;
+import com.android.systemui.media.MediaFeatureFlag;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -46,6 +49,7 @@
     private final NotificationGroupManager mGroupManager = Dependency.get(
             NotificationGroupManager.class);
     private final StatusBarStateController mStatusBarStateController;
+    private final Boolean mIsMediaFlagEnabled;
 
     private NotificationEntryManager.KeyguardEnvironment mEnvironment;
     private ShadeController mShadeController;
@@ -53,8 +57,11 @@
     private NotificationLockscreenUserManager mUserManager;
 
     @Inject
-    public NotificationFilter(StatusBarStateController statusBarStateController) {
+    public NotificationFilter(
+            StatusBarStateController statusBarStateController,
+            MediaFeatureFlag mediaFeatureFlag) {
         mStatusBarStateController = statusBarStateController;
+        mIsMediaFlagEnabled = mediaFeatureFlag.getEnabled();
     }
 
     private NotificationEntryManager.KeyguardEnvironment getEnvironment() {
@@ -133,6 +140,10 @@
                 }
             }
         }
+
+        if (mIsMediaFlagEnabled && isMediaNotification(sbn)) {
+            return true;
+        }
         return false;
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java
new file mode 100644
index 0000000..026a3ff
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection.coordinator;
+
+import static com.android.systemui.media.MediaDataManagerKt.isMediaNotification;
+
+import com.android.systemui.media.MediaFeatureFlag;
+import com.android.systemui.statusbar.notification.collection.NotifPipeline;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter;
+
+import javax.inject.Inject;
+
+/**
+ * Coordinates hiding (filtering) of media notifications.
+ */
+public class MediaCoordinator implements Coordinator {
+    private static final String TAG = "MediaCoordinator";
+
+    private final Boolean mIsMediaFeatureEnabled;
+
+    private final NotifFilter mMediaFilter = new NotifFilter(TAG) {
+        @Override
+        public boolean shouldFilterOut(NotificationEntry entry, long now) {
+            return mIsMediaFeatureEnabled && isMediaNotification(entry.getSbn());
+        }
+    };
+
+    @Inject
+    public MediaCoordinator(MediaFeatureFlag featureFlag) {
+        mIsMediaFeatureEnabled = featureFlag.getEnabled();
+    }
+
+    @Override
+    public void attach(NotifPipeline pipeline) {
+        pipeline.addFinalizeFilter(mMediaFilter);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.java
index 2b279bb..ac42964 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.java
@@ -57,7 +57,8 @@
             BubbleCoordinator bubbleCoordinator,
             HeadsUpCoordinator headsUpCoordinator,
             ConversationCoordinator conversationCoordinator,
-            PreparationCoordinator preparationCoordinator) {
+            PreparationCoordinator preparationCoordinator,
+            MediaCoordinator mediaCoordinator) {
         dumpManager.registerDumpable(TAG, this);
         mCoordinators.add(new HideLocallyDismissedNotifsCoordinator());
         mCoordinators.add(hideNotifsForOtherUsersCoordinator);
@@ -72,6 +73,7 @@
             mCoordinators.add(preparationCoordinator);
         }
         // TODO: add new Coordinators here! (b/112656837)
+        mCoordinators.add(mediaCoordinator);
 
         // TODO: add the sections in a particular ORDER (HeadsUp < People < Alerting)
         for (Coordinator c : mCoordinators) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt
index b163818..93db9cd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ChannelEditorDialogController.kt
@@ -341,7 +341,6 @@
     }
 
     private val wmFlags = (WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
-            or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
             or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
             or WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index 9925909..b0861bf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -557,9 +557,9 @@
 
     private void focusExpandButtonIfNecessary() {
         if (mFocusOnVisibilityChange) {
-            NotificationHeaderView header = getVisibleNotificationHeader();
-            if (header != null) {
-                ImageView expandButton = header.getExpandButton();
+            NotificationViewWrapper wrapper = getVisibleWrapper(mVisibleType);
+            if (wrapper != null) {
+                View expandButton = wrapper.getExpandButton();
                 if (expandButton != null) {
                     expandButton.requestAccessibilityFocus();
                 }
@@ -1348,7 +1348,9 @@
         }
         ImageView bubbleButton = layout.findViewById(com.android.internal.R.id.bubble_button);
         View actionContainer = layout.findViewById(com.android.internal.R.id.actions_container);
-        if (bubbleButton == null || actionContainer == null) {
+        LinearLayout actionContainerLayout =
+                layout.findViewById(com.android.internal.R.id.actions_container_layout);
+        if (bubbleButton == null || actionContainer == null || actionContainerLayout == null) {
             return;
         }
         boolean isPersonWithShortcut =
@@ -1374,8 +1376,16 @@
             bubbleButton.setOnClickListener(mContainingNotification.getBubbleClickListener());
             bubbleButton.setVisibility(VISIBLE);
             actionContainer.setVisibility(VISIBLE);
+
+            int paddingEnd = getResources().getDimensionPixelSize(
+                    com.android.internal.R.dimen.bubble_visible_padding_end);
+            actionContainerLayout.setPaddingRelative(0, 0, paddingEnd, 0);
         } else  {
             bubbleButton.setVisibility(GONE);
+
+            int paddingEnd = getResources().getDimensionPixelSize(
+                    com.android.internal.R.dimen.bubble_gone_padding_end);
+            actionContainerLayout.setPaddingRelative(0, 0, paddingEnd, 0);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PriorityOnboardingDialogController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PriorityOnboardingDialogController.kt
index 88c3258..c88f0bd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PriorityOnboardingDialogController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PriorityOnboardingDialogController.kt
@@ -184,7 +184,6 @@
     }
 
     private val wmFlags = (WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
-            or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
             or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
             or WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt
index 15499b8..fe70c81 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt
@@ -50,6 +50,7 @@
     private lateinit var conversationBadgeBg: View
     private lateinit var expandButton: View
     private lateinit var expandButtonContainer: View
+    private lateinit var expandButtonInnerContainer: View
     private lateinit var imageMessageContainer: ViewGroup
     private lateinit var messagingLinearLayout: MessagingLinearLayout
     private lateinit var conversationTitleView: View
@@ -69,6 +70,8 @@
             expandButton = requireViewById(com.android.internal.R.id.expand_button)
             expandButtonContainer =
                     requireViewById(com.android.internal.R.id.expand_button_container)
+            expandButtonInnerContainer =
+                    requireViewById(com.android.internal.R.id.expand_button_inner_container)
             importanceRing = requireViewById(com.android.internal.R.id.conversation_icon_badge_ring)
             appName = requireViewById(com.android.internal.R.id.app_name_text)
             conversationTitleView = requireViewById(com.android.internal.R.id.conversation_text)
@@ -134,6 +137,8 @@
         )
     }
 
+    override fun getExpandButton() = expandButtonInnerContainer
+
     override fun setShelfIconVisible(visible: Boolean) {
         if (conversationLayout.isImportantConversation) {
             if (conversationIconView.visibility != GONE) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
index f8b7831..4c9cb20 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
@@ -317,6 +317,11 @@
     }
 
     @Override
+    public View getExpandButton() {
+        return mExpandButton;
+    }
+
+    @Override
     public int getOriginalIconColor() {
         return mIcon.getOriginalIconColor();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java
index 02e537d..30080e3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapper.java
@@ -240,6 +240,13 @@
         return null;
     }
 
+    /**
+     * @return the expand button if it exists
+     */
+    public @Nullable View getExpandButton() {
+        return null;
+    }
+
     public int getOriginalIconColor() {
         return Notification.COLOR_INVALID;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
index ba7675f..0bdac39 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManager.kt
@@ -287,21 +287,17 @@
                     // Is there a section discontinuity? This usually occurs due to HUNs
                     if (prev?.entry?.bucket?.let { it > child.entry.bucket } == true) {
                         // Remove existing headers, and move the Incoming header if necessary
-                        if (alertingHeaderTarget != -1) {
-                            if (showHeaders && incomingHeaderTarget != -1) {
-                                incomingHeaderTarget = alertingHeaderTarget
-                            }
-                            alertingHeaderTarget = -1
+                        incomingHeaderTarget = when {
+                            !showHeaders -> -1
+                            incomingHeaderTarget != -1 -> incomingHeaderTarget
+                            peopleHeaderTarget != -1 -> peopleHeaderTarget
+                            alertingHeaderTarget != -1 -> alertingHeaderTarget
+                            gentleHeaderTarget != -1 -> gentleHeaderTarget
+                            else -> 0
                         }
-                        if (peopleHeaderTarget != -1) {
-                            if (showHeaders && incomingHeaderTarget != -1) {
-                                incomingHeaderTarget = peopleHeaderTarget
-                            }
-                            peopleHeaderTarget = -1
-                        }
-                        if (showHeaders && incomingHeaderTarget == -1) {
-                            incomingHeaderTarget = 0
-                        }
+                        peopleHeaderTarget = -1
+                        alertingHeaderTarget = -1
+                        gentleHeaderTarget = -1
                         // Walk backwards changing all previous notifications to the Incoming
                         // section
                         for (j in i - 1 downTo lastIncomingIndex + 1) {
@@ -323,6 +319,9 @@
                                 peopleHeaderTarget = i
                                 // Offset the target if there are other headers before this that
                                 // will be moved.
+                                if (currentIncomingHeaderIdx != -1 && incomingHeaderTarget == -1) {
+                                    peopleHeaderTarget--
+                                }
                                 if (currentPeopleHeaderIdx != -1) {
                                     peopleHeaderTarget--
                                 }
@@ -340,6 +339,13 @@
                                 alertingHeaderTarget = i
                                 // Offset the target if there are other headers before this that
                                 // will be moved.
+                                if (currentIncomingHeaderIdx != -1 && incomingHeaderTarget == -1) {
+                                    alertingHeaderTarget--
+                                }
+                                if (currentPeopleHeaderIdx != -1 && peopleHeaderTarget == -1) {
+                                    // People header will be removed
+                                    alertingHeaderTarget--
+                                }
                                 if (currentAlertingHeaderIdx != -1) {
                                     alertingHeaderTarget--
                                 }
@@ -354,6 +360,17 @@
                                 gentleHeaderTarget = i
                                 // Offset the target if there are other headers before this that
                                 // will be moved.
+                                if (currentIncomingHeaderIdx != -1 && incomingHeaderTarget == -1) {
+                                    gentleHeaderTarget--
+                                }
+                                if (currentPeopleHeaderIdx != -1 && peopleHeaderTarget == -1) {
+                                    // People header will be removed
+                                    gentleHeaderTarget--
+                                }
+                                if (currentAlertingHeaderIdx != -1 && alertingHeaderTarget == -1) {
+                                    // Alerting header will be removed
+                                    gentleHeaderTarget--
+                                }
                                 if (currentGentleHeaderIdx != -1) {
                                     gentleHeaderTarget--
                                 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java
index 7951541..fc8c8db 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java
@@ -274,7 +274,7 @@
             }
             if (value != 0) {
                 if (mSpec.startsWith(CustomTile.PREFIX)) {
-                    mHost.addTile(CustomTile.getComponentFromSpec(mSpec));
+                    mHost.addTile(CustomTile.getComponentFromSpec(mSpec), /* end */ true);
                 } else {
                     mHost.addTile(mSpec);
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index e0e52001..1bc42d1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -2253,6 +2253,8 @@
                 updateHideIconsForBouncer(false /* animate */);
             }
         }
+
+        updateBubblesVisibility();
     }
 
     @Override
@@ -2268,6 +2270,8 @@
         }
         mLightBarController.onStatusBarAppearanceChanged(appearanceRegions, barModeChanged,
                 mStatusBarMode, navbarColorManagedByIme);
+
+        updateBubblesVisibility();
     }
 
     @Override
@@ -2311,6 +2315,7 @@
         final int barMode = barMode(mTransientShown, mAppearance);
         if (updateBarMode(barMode)) {
             mLightBarController.onStatusBarModeChanged(barMode);
+            updateBubblesVisibility();
         }
     }
 
@@ -2395,6 +2400,14 @@
         mNotificationPanelViewController.setQsScrimEnabled(scrimEnabled);
     }
 
+    /** Temporarily hides Bubbles if the status bar is hidden. */
+    private void updateBubblesVisibility() {
+        mBubbleController.onStatusBarVisibilityChanged(
+                mStatusBarMode != MODE_LIGHTS_OUT
+                        && mStatusBarMode != MODE_LIGHTS_OUT_TRANSPARENT
+                        && !mStatusBarWindowHidden);
+    }
+
     void checkBarMode(@TransitionMode int mode, @WindowVisibleState int windowState,
             BarTransitions transitions) {
         final boolean anim = !mNoAnimationOnNextBarModeChange && mDeviceInteractive
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java
index b4de3cd..18a7add 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/MobileSignalController.java
@@ -640,8 +640,7 @@
                         + " dataState=" + state.getDataRegistrationState());
             }
             mServiceState = state;
-            // onDisplayInfoChanged is invoked directly after onServiceStateChanged, so not calling
-            // updateTelephony() to prevent icon flickering in case of overrides.
+            updateTelephony();
         }
 
         @Override
@@ -651,12 +650,6 @@
                         + " type=" + networkType);
             }
             mDataState = state;
-            if (networkType != mTelephonyDisplayInfo.getNetworkType()) {
-                Log.d(mTag, "onDataConnectionStateChanged:"
-                        + " network type change and reset displayInfo. type=" + networkType);
-                mTelephonyDisplayInfo = new TelephonyDisplayInfo(networkType,
-                        TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NONE);
-            }
             updateTelephony();
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/util/DismissCircleView.java b/packages/SystemUI/src/com/android/systemui/util/DismissCircleView.java
index 6c3538c..a31ea7c 100644
--- a/packages/SystemUI/src/com/android/systemui/util/DismissCircleView.java
+++ b/packages/SystemUI/src/com/android/systemui/util/DismissCircleView.java
@@ -40,7 +40,7 @@
 
         setBackground(res.getDrawable(R.drawable.dismiss_circle_background));
 
-        mIconView.setImageDrawable(res.getDrawable(R.drawable.dismiss_target_x));
+        mIconView.setImageDrawable(res.getDrawable(R.drawable.ic_close_white));
         addView(mIconView);
 
         setViewSizes();
diff --git a/packages/SystemUI/src/com/android/systemui/util/Utils.java b/packages/SystemUI/src/com/android/systemui/util/Utils.java
index b1792d0..5c9db54 100644
--- a/packages/SystemUI/src/com/android/systemui/util/Utils.java
+++ b/packages/SystemUI/src/com/android/systemui/util/Utils.java
@@ -133,4 +133,13 @@
                 Settings.Global.SHOW_MEDIA_ON_QUICK_SETTINGS, 1);
         return flag > 0;
     }
+
+    /**
+     * Allow media resumption controls. Requires {@link #useQsMediaPlayer(Context)} to be enabled.
+     * Off by default, but can be enabled by setting to 1
+     */
+    public static boolean useMediaResumption(Context context) {
+        int flag = Settings.System.getInt(context.getContentResolver(), "qs_media_resumption", 0);
+        return useQsMediaPlayer(context) && flag > 0;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index ce032e2..3455ff4 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -212,7 +212,6 @@
         mWindow.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND
                 | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR);
         mWindow.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
-                | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
                 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
                 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
                 | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubblePersistentRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubblePersistentRepositoryTest.kt
index f468192..2aed75e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubblePersistentRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubblePersistentRepositoryTest.kt
@@ -29,9 +29,9 @@
 class BubblePersistentRepositoryTest : SysuiTestCase() {
 
     private val bubbles = listOf(
-            BubbleEntity(0, "com.example.messenger", "shortcut-1", "key-1"),
-            BubbleEntity(10, "com.example.chat", "alice and bob", "key-2"),
-            BubbleEntity(0, "com.example.messenger", "shortcut-2", "key-3")
+            BubbleEntity(0, "com.example.messenger", "shortcut-1", "key-1", 120, 0),
+            BubbleEntity(10, "com.example.chat", "alice and bob", "key-2", 0, 16537428),
+            BubbleEntity(0, "com.example.messenger", "shortcut-2", "key-3", 120, 0)
     )
     private lateinit var repository: BubblePersistentRepository
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubbleVolatileRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubbleVolatileRepositoryTest.kt
index 2bb6bb8..f9d611c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubbleVolatileRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubbleVolatileRepositoryTest.kt
@@ -37,9 +37,9 @@
     private val user0 = UserHandle.of(0)
     private val user10 = UserHandle.of(10)
 
-    private val bubble1 = BubbleEntity(0, PKG_MESSENGER, "shortcut-1", "k1")
-    private val bubble2 = BubbleEntity(10, PKG_CHAT, "alice and bob", "k2")
-    private val bubble3 = BubbleEntity(0, PKG_MESSENGER, "shortcut-2", "k3")
+    private val bubble1 = BubbleEntity(0, PKG_MESSENGER, "shortcut-1", "k1", 120, 0)
+    private val bubble2 = BubbleEntity(10, PKG_CHAT, "alice and bob", "k2", 0, 16537428)
+    private val bubble3 = BubbleEntity(0, PKG_MESSENGER, "shortcut-2", "k3", 120, 0)
 
     private val bubbles = listOf(bubble1, bubble2, bubble3)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubbleXmlHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubbleXmlHelperTest.kt
index 79701ec..4946787 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubbleXmlHelperTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/storage/BubbleXmlHelperTest.kt
@@ -31,17 +31,17 @@
 class BubbleXmlHelperTest : SysuiTestCase() {
 
     private val bubbles = listOf(
-        BubbleEntity(0, "com.example.messenger", "shortcut-1", "k1"),
-        BubbleEntity(10, "com.example.chat", "alice and bob", "k2"),
-        BubbleEntity(0, "com.example.messenger", "shortcut-2", "k3")
+        BubbleEntity(0, "com.example.messenger", "shortcut-1", "k1", 120, 0),
+        BubbleEntity(10, "com.example.chat", "alice and bob", "k2", 0, 16537428),
+        BubbleEntity(0, "com.example.messenger", "shortcut-2", "k3", 120, 0)
     )
 
     @Test
     fun testWriteXml() {
         val expectedEntries = """
-            <bb uid="0" pkg="com.example.messenger" sid="shortcut-1" key="k1" />
-            <bb uid="10" pkg="com.example.chat" sid="alice and bob" key="k2" />
-            <bb uid="0" pkg="com.example.messenger" sid="shortcut-2" key="k3" />
+            <bb uid="0" pkg="com.example.messenger" sid="shortcut-1" key="k1" h="120" hid="0" />
+            <bb uid="10" pkg="com.example.chat" sid="alice and bob" key="k2" h="0" hid="16537428" />
+            <bb uid="0" pkg="com.example.messenger" sid="shortcut-2" key="k3" h="120" hid="0" />
         """.trimIndent()
         ByteArrayOutputStream().use {
             writeXml(it, bubbles)
@@ -56,9 +56,9 @@
         val src = """
             <?xml version='1.0' encoding='utf-8' standalone='yes' ?>
             <bs>
-            <bb uid="0" pkg="com.example.messenger" sid="shortcut-1" key="k1" />
-            <bb uid="10" pkg="com.example.chat" sid="alice and bob" key="k2" />
-            <bb uid="0" pkg="com.example.messenger" sid="shortcut-2" key="k3" />
+            <bb uid="0" pkg="com.example.messenger" sid="shortcut-1" key="k1" h="120" hid="0" />
+            <bb uid="10" pkg="com.example.chat" sid="alice and bob" key="k2" h="0" hid="16537428" />
+            <bb uid="0" pkg="com.example.messenger" sid="shortcut-2" key="k3" h="120" hid="0" />
             </bs>
         """.trimIndent()
         val actual = readXml(ByteArrayInputStream(src.toByteArray(Charsets.UTF_8)))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogTest.java
index 3254633..329af2b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/GlobalActionsDialogTest.java
@@ -19,6 +19,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -244,7 +245,7 @@
     }
 
     @Test
-    public void testCreateActionItems_maxThree() {
+    public void testCreateActionItems_maxThree_noOverflow() {
         mGlobalActionsDialog = spy(mGlobalActionsDialog);
         // allow 3 items to be shown
         doReturn(3).when(mGlobalActionsDialog).getMaxShownPowerItems();
@@ -254,13 +255,129 @@
                 GlobalActionsDialog.GLOBAL_ACTION_KEY_EMERGENCY,
                 GlobalActionsDialog.GLOBAL_ACTION_KEY_POWER,
                 GlobalActionsDialog.GLOBAL_ACTION_KEY_RESTART,
+        };
+        doReturn(actions).when(mGlobalActionsDialog).getDefaultActions();
+        mGlobalActionsDialog.createActionItems();
+
+        assertEquals(3, mGlobalActionsDialog.mItems.size());
+        assertEquals(0, mGlobalActionsDialog.mOverflowItems.size());
+        assertEquals(0, mGlobalActionsDialog.mPowerItems.size());
+    }
+
+    @Test
+    public void testCreateActionItems_maxThree_condensePower() {
+        mGlobalActionsDialog = spy(mGlobalActionsDialog);
+        // allow 3 items to be shown
+        doReturn(3).when(mGlobalActionsDialog).getMaxShownPowerItems();
+        // ensure items are not blocked by keyguard or device provisioning
+        doReturn(true).when(mGlobalActionsDialog).shouldShowAction(any());
+        // make sure lockdown action will be shown
+        doReturn(true).when(mGlobalActionsDialog).shouldDisplayLockdown(any());
+        String[] actions = {
+                GlobalActionsDialog.GLOBAL_ACTION_KEY_EMERGENCY,
+                GlobalActionsDialog.GLOBAL_ACTION_KEY_POWER,
+                GlobalActionsDialog.GLOBAL_ACTION_KEY_RESTART,
+                GlobalActionsDialog.GLOBAL_ACTION_KEY_LOCKDOWN,
+        };
+        doReturn(actions).when(mGlobalActionsDialog).getDefaultActions();
+        mGlobalActionsDialog.createActionItems();
+
+        assertEquals(3, mGlobalActionsDialog.mItems.size());
+        assertEquals(0, mGlobalActionsDialog.mOverflowItems.size());
+        assertEquals(2, mGlobalActionsDialog.mPowerItems.size());
+
+        // PowerOptionsAction should appear immediately after the Emergency action
+
+        GlobalActionsDialog.Action firstItem = mGlobalActionsDialog.mItems.get(0);
+        GlobalActionsDialog.Action secondItem = mGlobalActionsDialog.mItems.get(1);
+
+        assertTrue(firstItem instanceof GlobalActionsDialog.EmergencyAction);
+        assertTrue(secondItem instanceof GlobalActionsDialog.PowerOptionsAction);
+    }
+
+    @Test
+    public void testCreateActionItems_maxThree_condensePower_noEmergency() {
+        mGlobalActionsDialog = spy(mGlobalActionsDialog);
+        // allow 3 items to be shown
+        doReturn(3).when(mGlobalActionsDialog).getMaxShownPowerItems();
+        // make sure lockdown action will be shown
+        doReturn(true).when(mGlobalActionsDialog).shouldDisplayLockdown(any());
+        // ensure items are not blocked by keyguard or device provisioning
+        doReturn(true).when(mGlobalActionsDialog).shouldShowAction(any());
+        String[] actions = {
+                GlobalActionsDialog.GLOBAL_ACTION_KEY_POWER,
+                GlobalActionsDialog.GLOBAL_ACTION_KEY_RESTART,
                 GlobalActionsDialog.GLOBAL_ACTION_KEY_SCREENSHOT,
+                GlobalActionsDialog.GLOBAL_ACTION_KEY_LOCKDOWN,
+        };
+        doReturn(actions).when(mGlobalActionsDialog).getDefaultActions();
+        mGlobalActionsDialog.createActionItems();
+
+        assertEquals(3, mGlobalActionsDialog.mItems.size());
+        assertEquals(0, mGlobalActionsDialog.mOverflowItems.size());
+        assertEquals(2, mGlobalActionsDialog.mPowerItems.size());
+
+        // When Emergency isn't used, PowerOptionsAction should be first
+
+        GlobalActionsDialog.Action firstItem = mGlobalActionsDialog.mItems.get(0);
+        GlobalActionsDialog.Action secondItem = mGlobalActionsDialog.mItems.get(1);
+
+        assertTrue(firstItem instanceof GlobalActionsDialog.PowerOptionsAction);
+        assertTrue(secondItem instanceof GlobalActionsDialog.ScreenshotAction);
+    }
+
+    @Test
+    public void testCreateActionItems_maxFour_condensePower() {
+        mGlobalActionsDialog = spy(mGlobalActionsDialog);
+        // allow 3 items to be shown
+        doReturn(4).when(mGlobalActionsDialog).getMaxShownPowerItems();
+        // make sure lockdown action will be shown
+        doReturn(true).when(mGlobalActionsDialog).shouldDisplayLockdown(any());
+        // ensure items are not blocked by keyguard or device provisioning
+        doReturn(true).when(mGlobalActionsDialog).shouldShowAction(any());
+        String[] actions = {
+                GlobalActionsDialog.GLOBAL_ACTION_KEY_EMERGENCY,
+                GlobalActionsDialog.GLOBAL_ACTION_KEY_POWER,
+                GlobalActionsDialog.GLOBAL_ACTION_KEY_RESTART,
+                GlobalActionsDialog.GLOBAL_ACTION_KEY_LOCKDOWN,
+                GlobalActionsDialog.GLOBAL_ACTION_KEY_SCREENSHOT
+        };
+        doReturn(actions).when(mGlobalActionsDialog).getDefaultActions();
+        mGlobalActionsDialog.createActionItems();
+
+        assertEquals(4, mGlobalActionsDialog.mItems.size());
+        assertEquals(0, mGlobalActionsDialog.mOverflowItems.size());
+        assertEquals(2, mGlobalActionsDialog.mPowerItems.size());
+
+        // with four items, make sure power still shows up immediately after Emergency
+        GlobalActionsDialog.Action firstItem = mGlobalActionsDialog.mItems.get(0);
+        GlobalActionsDialog.Action secondItem = mGlobalActionsDialog.mItems.get(1);
+
+        assertTrue(firstItem instanceof GlobalActionsDialog.EmergencyAction);
+        assertTrue(secondItem instanceof GlobalActionsDialog.PowerOptionsAction);
+    }
+
+    @Test
+    public void testCreateActionItems_maxThree_doNotCondensePower() {
+        mGlobalActionsDialog = spy(mGlobalActionsDialog);
+        // allow 3 items to be shown
+        doReturn(3).when(mGlobalActionsDialog).getMaxShownPowerItems();
+        // make sure lockdown action will be shown
+        doReturn(true).when(mGlobalActionsDialog).shouldDisplayLockdown(any());
+        // ensure items are not blocked by keyguard or device provisioning
+        doReturn(true).when(mGlobalActionsDialog).shouldShowAction(any());
+        String[] actions = {
+                GlobalActionsDialog.GLOBAL_ACTION_KEY_EMERGENCY,
+                GlobalActionsDialog.GLOBAL_ACTION_KEY_POWER,
+                GlobalActionsDialog.GLOBAL_ACTION_KEY_SCREENSHOT,
+                GlobalActionsDialog.GLOBAL_ACTION_KEY_LOCKDOWN,
         };
         doReturn(actions).when(mGlobalActionsDialog).getDefaultActions();
         mGlobalActionsDialog.createActionItems();
 
         assertEquals(3, mGlobalActionsDialog.mItems.size());
         assertEquals(1, mGlobalActionsDialog.mOverflowItems.size());
+        assertEquals(0, mGlobalActionsDialog.mPowerItems.size());
     }
 
     @Test
@@ -270,11 +387,13 @@
         doReturn(Integer.MAX_VALUE).when(mGlobalActionsDialog).getMaxShownPowerItems();
         // ensure items are not blocked by keyguard or device provisioning
         doReturn(true).when(mGlobalActionsDialog).shouldShowAction(any());
+        // make sure lockdown action will be shown
+        doReturn(true).when(mGlobalActionsDialog).shouldDisplayLockdown(any());
         String[] actions = {
                 GlobalActionsDialog.GLOBAL_ACTION_KEY_EMERGENCY,
                 GlobalActionsDialog.GLOBAL_ACTION_KEY_POWER,
                 GlobalActionsDialog.GLOBAL_ACTION_KEY_RESTART,
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_SCREENSHOT,
+                GlobalActionsDialog.GLOBAL_ACTION_KEY_LOCKDOWN,
         };
         doReturn(actions).when(mGlobalActionsDialog).getDefaultActions();
         mGlobalActionsDialog.createActionItems();
@@ -288,10 +407,12 @@
         mGlobalActionsDialog = spy(mGlobalActionsDialog);
         // allow only 3 items to be shown
         doReturn(3).when(mGlobalActionsDialog).getMaxShownPowerItems();
+        // make sure lockdown action will NOT be shown
+        doReturn(false).when(mGlobalActionsDialog).shouldDisplayLockdown(any());
         String[] actions = {
                 GlobalActionsDialog.GLOBAL_ACTION_KEY_EMERGENCY,
-                // screenshot blocked because device not provisioned
-                GlobalActionsDialog.GLOBAL_ACTION_KEY_SCREENSHOT,
+                // lockdown action not allowed
+                GlobalActionsDialog.GLOBAL_ACTION_KEY_LOCKDOWN,
                 GlobalActionsDialog.GLOBAL_ACTION_KEY_POWER,
                 GlobalActionsDialog.GLOBAL_ACTION_KEY_RESTART,
         };
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
index 9d2b6f4..737ced6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
@@ -31,6 +31,7 @@
 import android.widget.ImageView
 import android.widget.SeekBar
 import android.widget.TextView
+import androidx.lifecycle.LiveData
 import androidx.test.filters.SmallTest
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
@@ -41,6 +42,7 @@
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
@@ -48,6 +50,7 @@
 import org.mockito.Mockito.mock
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when` as whenever
+import org.mockito.junit.MockitoJUnit
 
 private const val KEY = "TEST_KEY"
 private const val APP = "APP"
@@ -67,13 +70,14 @@
 
     private lateinit var player: MediaControlPanel
 
-    private lateinit var fgExecutor: FakeExecutor
     private lateinit var bgExecutor: FakeExecutor
     @Mock private lateinit var activityStarter: ActivityStarter
 
     @Mock private lateinit var holder: PlayerViewHolder
     @Mock private lateinit var view: TransitionLayout
     @Mock private lateinit var mediaHostStatesManager: MediaHostStatesManager
+    @Mock private lateinit var seekBarViewModel: SeekBarViewModel
+    @Mock private lateinit var seekBarData: LiveData<SeekBarViewModel.Progress>
     private lateinit var appIcon: ImageView
     private lateinit var appName: TextView
     private lateinit var albumView: ImageView
@@ -95,20 +99,17 @@
     private val device = MediaDeviceData(true, null, DEVICE_NAME)
     private val disabledDevice = MediaDeviceData(false, null, null)
 
+    @JvmField @Rule val mockito = MockitoJUnit.rule()
+
     @Before
     fun setUp() {
-        fgExecutor = FakeExecutor(FakeSystemClock())
         bgExecutor = FakeExecutor(FakeSystemClock())
 
-        activityStarter = mock(ActivityStarter::class.java)
-        mediaHostStatesManager = mock(MediaHostStatesManager::class.java)
-
-        player = MediaControlPanel(context, fgExecutor, bgExecutor, activityStarter,
-                mediaHostStatesManager)
+        player = MediaControlPanel(context, bgExecutor, activityStarter, mediaHostStatesManager,
+                seekBarViewModel)
+        whenever(seekBarViewModel.progress).thenReturn(seekBarData)
 
         // Mock out a view holder for the player to attach to.
-        holder = mock(PlayerViewHolder::class.java)
-        view = mock(TransitionLayout::class.java)
         whenever(holder.player).thenReturn(view)
         appIcon = ImageView(context)
         whenever(holder.appIcon).thenReturn(appIcon)
@@ -171,7 +172,7 @@
     @Test
     fun bindWhenUnattached() {
         val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
-                emptyList(), PACKAGE, null, null, device)
+                emptyList(), PACKAGE, null, null, device, null)
         player.bind(state)
         assertThat(player.isPlaying()).isFalse()
     }
@@ -180,7 +181,7 @@
     fun bindText() {
         player.attach(holder)
         val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
-                emptyList(), PACKAGE, session.getSessionToken(), null, device)
+                emptyList(), PACKAGE, session.getSessionToken(), null, device, null)
         player.bind(state)
         assertThat(appName.getText()).isEqualTo(APP)
         assertThat(titleText.getText()).isEqualTo(TITLE)
@@ -191,7 +192,7 @@
     fun bindBackgroundColor() {
         player.attach(holder)
         val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
-                emptyList(), PACKAGE, session.getSessionToken(), null, device)
+                emptyList(), PACKAGE, session.getSessionToken(), null, device, null)
         player.bind(state)
         val list = ArgumentCaptor.forClass(ColorStateList::class.java)
         verify(view).setBackgroundTintList(list.capture())
@@ -202,7 +203,7 @@
     fun bindDevice() {
         player.attach(holder)
         val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
-                emptyList(), PACKAGE, session.getSessionToken(), null, device)
+                emptyList(), PACKAGE, session.getSessionToken(), null, device, null)
         player.bind(state)
         assertThat(seamlessText.getText()).isEqualTo(DEVICE_NAME)
         assertThat(seamless.isEnabled()).isTrue()
@@ -212,7 +213,7 @@
     fun bindDisabledDevice() {
         player.attach(holder)
         val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
-                emptyList(), PACKAGE, session.getSessionToken(), null, disabledDevice)
+                emptyList(), PACKAGE, session.getSessionToken(), null, disabledDevice, null)
         player.bind(state)
         assertThat(seamless.isEnabled()).isFalse()
         assertThat(seamlessText.getText()).isEqualTo(context.getResources().getString(
@@ -223,7 +224,7 @@
     fun bindNullDevice() {
         player.attach(holder)
         val state = MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
-                emptyList(), PACKAGE, session.getSessionToken(), null, null)
+                emptyList(), PACKAGE, session.getSessionToken(), null, null, null)
         player.bind(state)
         assertThat(seamless.isEnabled()).isTrue()
         assertThat(seamlessText.getText()).isEqualTo(context.getResources().getString(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java
index 48e3b0a..bed5c9e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataCombineLatestTest.java
@@ -79,16 +79,16 @@
         mManager.addListener(mListener);
 
         mMediaData = new MediaData(true, BG_COLOR, APP, null, ARTIST, TITLE, null,
-                new ArrayList<>(), new ArrayList<>(), PACKAGE, null, null, null, KEY);
+                new ArrayList<>(), new ArrayList<>(), PACKAGE, null, null, null, null, KEY, false);
         mDeviceData = new MediaDeviceData(true, null, DEVICE_NAME);
     }
 
     @Test
     public void eventNotEmittedWithoutDevice() {
         // WHEN data source emits an event without device data
-        mDataListener.onMediaDataLoaded(KEY, mMediaData);
+        mDataListener.onMediaDataLoaded(KEY, null, mMediaData);
         // THEN an event isn't emitted
-        verify(mListener, never()).onMediaDataLoaded(eq(KEY), any());
+        verify(mListener, never()).onMediaDataLoaded(eq(KEY), any(), any());
     }
 
     @Test
@@ -96,7 +96,7 @@
         // WHEN device source emits an event without media data
         mDeviceListener.onMediaDeviceChanged(KEY, mDeviceData);
         // THEN an event isn't emitted
-        verify(mListener, never()).onMediaDataLoaded(eq(KEY), any());
+        verify(mListener, never()).onMediaDataLoaded(eq(KEY), any(), any());
     }
 
     @Test
@@ -104,22 +104,22 @@
         // GIVEN that a device event has already been received
         mDeviceListener.onMediaDeviceChanged(KEY, mDeviceData);
         // WHEN media event is received
-        mDataListener.onMediaDataLoaded(KEY, mMediaData);
+        mDataListener.onMediaDataLoaded(KEY, null, mMediaData);
         // THEN the listener receives a combined event
         ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
-        verify(mListener).onMediaDataLoaded(eq(KEY), captor.capture());
+        verify(mListener).onMediaDataLoaded(eq(KEY), any(), captor.capture());
         assertThat(captor.getValue().getDevice()).isNotNull();
     }
 
     @Test
     public void emitEventAfterMediaFirst() {
         // GIVEN that media event has already been received
-        mDataListener.onMediaDataLoaded(KEY, mMediaData);
+        mDataListener.onMediaDataLoaded(KEY, null, mMediaData);
         // WHEN device event is received
         mDeviceListener.onMediaDeviceChanged(KEY, mDeviceData);
         // THEN the listener receives a combined event
         ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
-        verify(mListener).onMediaDataLoaded(eq(KEY), captor.capture());
+        verify(mListener).onMediaDataLoaded(eq(KEY), any(), captor.capture());
         assertThat(captor.getValue().getDevice()).isNotNull();
     }
 
@@ -133,7 +133,7 @@
 
     @Test
     public void mediaDataRemovedAfterMediaEvent() {
-        mDataListener.onMediaDataLoaded(KEY, mMediaData);
+        mDataListener.onMediaDataLoaded(KEY, null, mMediaData);
         mDataListener.onMediaDataRemoved(KEY);
         verify(mListener).onMediaDataRemoved(eq(KEY));
     }
@@ -145,6 +145,18 @@
         verify(mListener).onMediaDataRemoved(eq(KEY));
     }
 
+    @Test
+    public void mediaDataKeyUpdated() {
+        // GIVEN that device and media events have already been received
+        mDataListener.onMediaDataLoaded(KEY, null, mMediaData);
+        mDeviceListener.onMediaDeviceChanged(KEY, mDeviceData);
+        // WHEN the key is changed
+        mDataListener.onMediaDataLoaded("NEW_KEY", KEY, mMediaData);
+        // THEN the listener gets a load event with the correct keys
+        ArgumentCaptor<MediaData> captor = ArgumentCaptor.forClass(MediaData.class);
+        verify(mListener).onMediaDataLoaded(eq("NEW_KEY"), any(), captor.capture());
+    }
+
     private MediaDataManager.Listener captureDataListener() {
         ArgumentCaptor<MediaDataManager.Listener> captor = ArgumentCaptor.forClass(
                 MediaDataManager.Listener.class);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
index c0aef8a..3a3140f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDeviceManagerTest.kt
@@ -23,8 +23,6 @@
 import android.media.RoutingSessionInfo
 import android.media.session.MediaSession
 import android.media.session.PlaybackState
-import android.os.Process
-import android.service.notification.StatusBarNotification
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import androidx.test.filters.SmallTest
@@ -67,6 +65,7 @@
 public class MediaDeviceManagerTest : SysuiTestCase() {
 
     private lateinit var manager: MediaDeviceManager
+    @Mock private lateinit var mediaDataManager: MediaDataManager
     @Mock private lateinit var lmmFactory: LocalMediaManagerFactory
     @Mock private lateinit var lmm: LocalMediaManager
     @Mock private lateinit var mr2: MediaRouter2Manager
@@ -80,13 +79,14 @@
     private lateinit var metadataBuilder: MediaMetadata.Builder
     private lateinit var playbackBuilder: PlaybackState.Builder
     private lateinit var notifBuilder: Notification.Builder
-    private lateinit var sbn: StatusBarNotification
+    private lateinit var mediaData: MediaData
     @JvmField @Rule val mockito = MockitoJUnit.rule()
 
     @Before
     fun setUp() {
         fakeExecutor = FakeExecutor(FakeSystemClock())
-        manager = MediaDeviceManager(context, lmmFactory, mr2, featureFlag, fakeExecutor)
+        manager = MediaDeviceManager(context, lmmFactory, mr2, featureFlag, fakeExecutor,
+            mediaDataManager)
         manager.addListener(listener)
 
         // Configure mocks.
@@ -117,8 +117,8 @@
             setSmallIcon(android.R.drawable.ic_media_pause)
             setStyle(Notification.MediaStyle().setMediaSession(session.getSessionToken()))
         }
-        sbn = StatusBarNotification(PACKAGE, PACKAGE, 0, "TAG", Process.myUid(), 0, 0,
-                notifBuilder.build(), Process.myUserHandle(), 0)
+        mediaData = MediaData(true, 0, PACKAGE, null, null, SESSION_TITLE, null,
+            emptyList(), emptyList(), PACKAGE, session.sessionToken, null, null, null)
     }
 
     @After
@@ -128,33 +128,33 @@
 
     @Test
     fun removeUnknown() {
-        manager.onNotificationRemoved("unknown")
+        manager.onMediaDataRemoved("unknown")
     }
 
     @Test
     fun addNotification() {
-        manager.onNotificationAdded(KEY, sbn)
+        manager.onMediaDataLoaded(KEY, null, mediaData)
         verify(lmmFactory).create(PACKAGE)
     }
 
     @Test
     fun featureDisabled() {
         whenever(featureFlag.enabled).thenReturn(false)
-        manager.onNotificationAdded(KEY, sbn)
+        manager.onMediaDataLoaded(KEY, null, mediaData)
         verify(lmmFactory, never()).create(PACKAGE)
     }
 
     @Test
     fun addAndRemoveNotification() {
-        manager.onNotificationAdded(KEY, sbn)
-        manager.onNotificationRemoved(KEY)
+        manager.onMediaDataLoaded(KEY, null, mediaData)
+        manager.onMediaDataRemoved(KEY)
         verify(lmm).unregisterCallback(any())
     }
 
     @Test
     fun deviceEventOnAddNotification() {
         // WHEN a notification is added
-        manager.onNotificationAdded(KEY, sbn)
+        manager.onMediaDataLoaded(KEY, null, mediaData)
         val deviceCallback = captureCallback()
         // THEN the update is dispatched to the listener
         val data = captureDeviceData(KEY)
@@ -165,7 +165,7 @@
 
     @Test
     fun deviceListUpdate() {
-        manager.onNotificationAdded(KEY, sbn)
+        manager.onMediaDataLoaded(KEY, null, mediaData)
         val deviceCallback = captureCallback()
         // WHEN the device list changes
         deviceCallback.onDeviceListUpdate(mutableListOf(device))
@@ -179,7 +179,7 @@
 
     @Test
     fun selectedDeviceStateChanged() {
-        manager.onNotificationAdded(KEY, sbn)
+        manager.onMediaDataLoaded(KEY, null, mediaData)
         val deviceCallback = captureCallback()
         // WHEN the selected device changes state
         deviceCallback.onSelectedDeviceStateChanged(device, 1)
@@ -193,9 +193,9 @@
 
     @Test
     fun listenerReceivesKeyRemoved() {
-        manager.onNotificationAdded(KEY, sbn)
+        manager.onMediaDataLoaded(KEY, null, mediaData)
         // WHEN the notification is removed
-        manager.onNotificationRemoved(KEY)
+        manager.onMediaDataRemoved(KEY)
         // THEN the listener receives key removed event
         verify(listener).onKeyRemoved(eq(KEY))
     }
@@ -205,7 +205,7 @@
         // GIVEN that MR2Manager returns null for routing session
         whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null)
         // WHEN a notification is added
-        manager.onNotificationAdded(KEY, sbn)
+        manager.onMediaDataLoaded(KEY, null, mediaData)
         // THEN the device is disabled
         val data = captureDeviceData(KEY)
         assertThat(data.enabled).isFalse()
@@ -216,7 +216,7 @@
     @Test
     fun deviceDisabledWhenMR2ReturnsNullRouteInfoOnDeviceChanged() {
         // GIVEN a notif is added
-        manager.onNotificationAdded(KEY, sbn)
+        manager.onMediaDataLoaded(KEY, null, mediaData)
         reset(listener)
         // AND MR2Manager returns null for routing session
         whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null)
@@ -234,7 +234,7 @@
     @Test
     fun deviceDisabledWhenMR2ReturnsNullRouteInfoOnDeviceListUpdate() {
         // GIVEN a notif is added
-        manager.onNotificationAdded(KEY, sbn)
+        manager.onMediaDataLoaded(KEY, null, mediaData)
         reset(listener)
         // GIVEN that MR2Manager returns null for routing session
         whenever(mr2.getRoutingSessionForMediaController(any())).thenReturn(null)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt
index c21343c..7d44327 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaTimeoutListenerTest.kt
@@ -16,7 +16,9 @@
 
 package com.android.systemui.media
 
+import android.media.MediaMetadata
 import android.media.session.MediaController
+import android.media.session.MediaSession
 import android.media.session.PlaybackState
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
@@ -41,6 +43,10 @@
 import org.mockito.junit.MockitoJUnit
 
 private const val KEY = "KEY"
+private const val PACKAGE = "PKG"
+private const val SESSION_KEY = "SESSION_KEY"
+private const val SESSION_ARTIST = "SESSION_ARTIST"
+private const val SESSION_TITLE = "SESSION_TITLE"
 
 private fun <T> eq(value: T): T = Mockito.eq(value) ?: value
 private fun <T> anyObject(): T {
@@ -54,12 +60,15 @@
     @Mock private lateinit var mediaControllerFactory: MediaControllerFactory
     @Mock private lateinit var mediaController: MediaController
     @Mock private lateinit var executor: DelayableExecutor
-    @Mock private lateinit var mediaData: MediaData
     @Mock private lateinit var timeoutCallback: (String, Boolean) -> Unit
     @Mock private lateinit var cancellationRunnable: Runnable
     @Captor private lateinit var timeoutCaptor: ArgumentCaptor<Runnable>
     @Captor private lateinit var mediaCallbackCaptor: ArgumentCaptor<MediaController.Callback>
     @JvmField @Rule val mockito = MockitoJUnit.rule()
+    private lateinit var metadataBuilder: MediaMetadata.Builder
+    private lateinit var playbackBuilder: PlaybackState.Builder
+    private lateinit var session: MediaSession
+    private lateinit var mediaData: MediaData
     private lateinit var mediaTimeoutListener: MediaTimeoutListener
 
     @Before
@@ -68,22 +77,39 @@
         `when`(executor.executeDelayed(any(), anyLong())).thenReturn(cancellationRunnable)
         mediaTimeoutListener = MediaTimeoutListener(mediaControllerFactory, executor)
         mediaTimeoutListener.timeoutCallback = timeoutCallback
+
+        // Create a media session and notification for testing.
+        metadataBuilder = MediaMetadata.Builder().apply {
+            putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST)
+            putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE)
+        }
+        playbackBuilder = PlaybackState.Builder().apply {
+            setState(PlaybackState.STATE_PAUSED, 6000L, 1f)
+            setActions(PlaybackState.ACTION_PLAY)
+        }
+        session = MediaSession(context, SESSION_KEY).apply {
+            setMetadata(metadataBuilder.build())
+            setPlaybackState(playbackBuilder.build())
+        }
+        session.setActive(true)
+        mediaData = MediaData(true, 0, PACKAGE, null, null, SESSION_TITLE, null,
+            emptyList(), emptyList(), PACKAGE, session.sessionToken, null, null, null)
     }
 
     @Test
     fun testOnMediaDataLoaded_registersPlaybackListener() {
-        mediaTimeoutListener.onMediaDataLoaded(KEY, mediaData)
+        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
         verify(mediaController).registerCallback(capture(mediaCallbackCaptor))
 
         // Ignores is same key
         clearInvocations(mediaController)
-        mediaTimeoutListener.onMediaDataLoaded(KEY, mediaData)
+        mediaTimeoutListener.onMediaDataLoaded(KEY, KEY, mediaData)
         verify(mediaController, never()).registerCallback(anyObject())
     }
 
     @Test
     fun testOnMediaDataRemoved_unregistersPlaybackListener() {
-        mediaTimeoutListener.onMediaDataLoaded(KEY, mediaData)
+        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
         mediaTimeoutListener.onMediaDataRemoved(KEY)
         verify(mediaController).unregisterCallback(anyObject())
 
@@ -105,7 +131,7 @@
 
     @Test
     fun testOnPlaybackStateChanged_cancelsTimeout_whenResumed() {
-        // Assuming we're have a pending timeout
+        // Assuming we have a pending timeout
         testOnPlaybackStateChanged_schedulesTimeout_whenPaused()
 
         mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder()
@@ -114,6 +140,17 @@
     }
 
     @Test
+    fun testOnPlaybackStateChanged_reusesTimeout_whenNotPlaying() {
+        // Assuming we have a pending timeout
+        testOnPlaybackStateChanged_schedulesTimeout_whenPaused()
+
+        clearInvocations(cancellationRunnable)
+        mediaCallbackCaptor.value.onPlaybackStateChanged(PlaybackState.Builder()
+                .setState(PlaybackState.STATE_STOPPED, 0L, 0f).build())
+        verify(cancellationRunnable, never()).run()
+    }
+
+    @Test
     fun testTimeoutCallback_invokedIfTimeout() {
         // Assuming we're have a pending timeout
         testOnPlaybackStateChanged_schedulesTimeout_whenPaused()
@@ -124,7 +161,7 @@
 
     @Test
     fun testIsTimedOut() {
-        mediaTimeoutListener.onMediaDataLoaded(KEY, mediaData)
+        mediaTimeoutListener.onMediaDataLoaded(KEY, null, mediaData)
         assertThat(mediaTimeoutListener.isTimedOut(KEY)).isFalse()
     }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarViewModelTest.kt
index 19e15b3..24e9bd8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/SeekBarViewModelTest.kt
@@ -29,6 +29,7 @@
 
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.concurrency.FakeRepeatableExecutor
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
 
@@ -71,7 +72,7 @@
     @Before
     fun setUp() {
         fakeExecutor = FakeExecutor(FakeSystemClock())
-        viewModel = SeekBarViewModel(fakeExecutor)
+        viewModel = SeekBarViewModel(FakeRepeatableExecutor(fakeExecutor))
         mockController = mock(MediaController::class.java)
         whenever(mockController.sessionToken).thenReturn(token1)
         mockTransport = mock(MediaController.TransportControls::class.java)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/pip/PipAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/pip/PipAnimationControllerTest.java
index b7a2633..536cae4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/pip/PipAnimationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/pip/PipAnimationControllerTest.java
@@ -82,7 +82,7 @@
     @Test
     public void getAnimator_withBounds_returnBoundsAnimator() {
         final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController
-                .getAnimator(mLeash, new Rect(), new Rect());
+                .getAnimator(mLeash, new Rect(), new Rect(), null);
 
         assertEquals("Expect ANIM_TYPE_BOUNDS animation",
                 animator.getAnimationType(), PipAnimationController.ANIM_TYPE_BOUNDS);
@@ -94,12 +94,12 @@
         final Rect endValue1 = new Rect(100, 100, 200, 200);
         final Rect endValue2 = new Rect(200, 200, 300, 300);
         final PipAnimationController.PipTransitionAnimator oldAnimator = mPipAnimationController
-                .getAnimator(mLeash, startValue, endValue1);
+                .getAnimator(mLeash, startValue, endValue1, null);
         oldAnimator.setSurfaceControlTransactionFactory(DummySurfaceControlTx::new);
         oldAnimator.start();
 
         final PipAnimationController.PipTransitionAnimator newAnimator = mPipAnimationController
-                .getAnimator(mLeash, startValue, endValue2);
+                .getAnimator(mLeash, startValue, endValue2, null);
 
         assertEquals("getAnimator with same type returns same animator",
                 oldAnimator, newAnimator);
@@ -129,7 +129,7 @@
         final Rect endValue1 = new Rect(100, 100, 200, 200);
         final Rect endValue2 = new Rect(200, 200, 300, 300);
         final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController
-                .getAnimator(mLeash, startValue, endValue1);
+                .getAnimator(mLeash, startValue, endValue1, null);
 
         animator.updateEndValue(endValue2);
 
@@ -141,7 +141,7 @@
         final Rect startValue = new Rect(0, 0, 100, 100);
         final Rect endValue = new Rect(100, 100, 200, 200);
         final PipAnimationController.PipTransitionAnimator animator = mPipAnimationController
-                .getAnimator(mLeash, startValue, endValue);
+                .getAnimator(mLeash, startValue, endValue, null);
         animator.setSurfaceControlTransactionFactory(DummySurfaceControlTx::new);
 
         animator.setPipAnimationCallback(mPipAnimationCallback);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
index 1147739..5d4ef55 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
@@ -219,13 +219,43 @@
     public void testNoRepeatedSpecs_customTile() {
         mQSTileHost.onTuningChanged(QSTileHost.TILES_SETTING, CUSTOM_TILE_SPEC);
 
-        mQSTileHost.addTile(CUSTOM_TILE);
+        mQSTileHost.addTile(CUSTOM_TILE, /* end */ false);
 
         assertEquals(1, mQSTileHost.mTileSpecs.size());
         assertEquals(CUSTOM_TILE_SPEC, mQSTileHost.mTileSpecs.get(0));
     }
 
     @Test
+    public void testAddedAtBeginningOnDefault_customTile() {
+        mQSTileHost.onTuningChanged(QSTileHost.TILES_SETTING, "spec1"); // seed
+
+        mQSTileHost.addTile(CUSTOM_TILE);
+
+        assertEquals(2, mQSTileHost.mTileSpecs.size());
+        assertEquals(CUSTOM_TILE_SPEC, mQSTileHost.mTileSpecs.get(0));
+    }
+
+    @Test
+    public void testAddedAtBeginning_customTile() {
+        mQSTileHost.onTuningChanged(QSTileHost.TILES_SETTING, "spec1"); // seed
+
+        mQSTileHost.addTile(CUSTOM_TILE, /* end */ false);
+
+        assertEquals(2, mQSTileHost.mTileSpecs.size());
+        assertEquals(CUSTOM_TILE_SPEC, mQSTileHost.mTileSpecs.get(0));
+    }
+
+    @Test
+    public void testAddedAtEnd_customTile() {
+        mQSTileHost.onTuningChanged(QSTileHost.TILES_SETTING, "spec1"); // seed
+
+        mQSTileHost.addTile(CUSTOM_TILE, /* end */ true);
+
+        assertEquals(2, mQSTileHost.mTileSpecs.size());
+        assertEquals(CUSTOM_TILE_SPEC, mQSTileHost.mTileSpecs.get(1));
+    }
+
+    @Test
     public void testLoadTileSpec_repeated() {
         List<String> specs = QSTileHost.loadTileSpecs(mContext, "spec1,spec1,spec2");
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationFilterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationFilterTest.java
index 595ba89..5a81d36 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationFilterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationFilterTest.java
@@ -27,8 +27,10 @@
 
 import android.Manifest;
 import android.app.Notification;
+import android.app.Notification.MediaStyle;
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
+import android.media.session.MediaSession;
 import android.os.Bundle;
 import android.service.notification.StatusBarNotification;
 import android.testing.AndroidTestingRunner;
@@ -40,6 +42,7 @@
 
 import com.android.systemui.ForegroundServiceController;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.media.MediaFeatureFlag;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
 import com.android.systemui.statusbar.notification.NotificationEntryManager.KeyguardEnvironment;
@@ -51,6 +54,7 @@
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
 import com.android.systemui.statusbar.phone.ShadeController;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -73,10 +77,16 @@
     ForegroundServiceController mFsc;
     @Mock
     KeyguardEnvironment mEnvironment;
+    @Mock
+    MediaFeatureFlag mMediaFeatureFlag;
+    @Mock
+    StatusBarStateController mStatusBarStateController;
     private final IPackageManager mMockPackageManager = mock(IPackageManager.class);
 
     private NotificationFilter mNotificationFilter;
     private ExpandableNotificationRow mRow;
+    private NotificationEntry mMediaEntry;
+    private MediaSession mMediaSession;
 
     @Before
     public void setUp() throws Exception {
@@ -84,6 +94,12 @@
         MockitoAnnotations.initMocks(this);
         when(mMockStatusBarNotification.getUid()).thenReturn(UID_NORMAL);
 
+        mMediaSession = new MediaSession(mContext, "TEST_MEDIA_SESSION");
+        NotificationEntryBuilder builder = new NotificationEntryBuilder();
+        builder.modifyNotification(mContext).setStyle(
+                new MediaStyle().setMediaSession(mMediaSession.getSessionToken()));
+        mMediaEntry = builder.build();
+
         when(mMockPackageManager.checkUidPermission(
                 eq(Manifest.permission.NOTIFICATION_DURING_SETUP),
                 eq(UID_NORMAL)))
@@ -107,7 +123,12 @@
                 mDependency,
                 TestableLooper.get(this));
         mRow = testHelper.createRow();
-        mNotificationFilter = new NotificationFilter(mock(StatusBarStateController.class));
+        mNotificationFilter = new NotificationFilter(mStatusBarStateController, mMediaFeatureFlag);
+    }
+
+    @After
+    public void tearDown() {
+        mMediaSession.release();
     }
 
     @Test
@@ -218,6 +239,56 @@
         assertFalse(mNotificationFilter.shouldFilterOut(entry));
     }
 
+    @Test
+    public void shouldFilterOtherNotificationWhenDisabled() {
+        // GIVEN that the media feature is disabled
+        when(mMediaFeatureFlag.getEnabled()).thenReturn(false);
+        NotificationFilter filter = new NotificationFilter(mStatusBarStateController,
+                mMediaFeatureFlag);
+        // WHEN the media filter is asked about an entry
+        NotificationEntry otherEntry = new NotificationEntryBuilder().build();
+        final boolean shouldFilter = filter.shouldFilterOut(otherEntry);
+        // THEN it shouldn't be filtered
+        assertFalse(shouldFilter);
+    }
+
+    @Test
+    public void shouldFilterOtherNotificationWhenEnabled() {
+        // GIVEN that the media feature is enabled
+        when(mMediaFeatureFlag.getEnabled()).thenReturn(true);
+        NotificationFilter filter = new NotificationFilter(mStatusBarStateController,
+                mMediaFeatureFlag);
+        // WHEN the media filter is asked about an entry
+        NotificationEntry otherEntry = new NotificationEntryBuilder().build();
+        final boolean shouldFilter = filter.shouldFilterOut(otherEntry);
+        // THEN it shouldn't be filtered
+        assertFalse(shouldFilter);
+    }
+
+    @Test
+    public void shouldFilterMediaNotificationWhenDisabled() {
+        // GIVEN that the media feature is disabled
+        when(mMediaFeatureFlag.getEnabled()).thenReturn(false);
+        NotificationFilter filter = new NotificationFilter(mStatusBarStateController,
+                mMediaFeatureFlag);
+        // WHEN the media filter is asked about a media entry
+        final boolean shouldFilter = filter.shouldFilterOut(mMediaEntry);
+        // THEN it shouldn't be filtered
+        assertFalse(shouldFilter);
+    }
+
+    @Test
+    public void shouldFilterMediaNotificationWhenEnabled() {
+        // GIVEN that the media feature is enabled
+        when(mMediaFeatureFlag.getEnabled()).thenReturn(true);
+        NotificationFilter filter = new NotificationFilter(mStatusBarStateController,
+                mMediaFeatureFlag);
+        // WHEN the media filter is asked about a media entry
+        final boolean shouldFilter = filter.shouldFilterOut(mMediaEntry);
+        // THEN it should be filtered
+        assertTrue(shouldFilter);
+    }
+
     private void initStatusBarNotification(boolean allowDuringSetup) {
         Bundle bundle = new Bundle();
         bundle.putBoolean(Notification.EXTRA_ALLOW_DURING_SETUP, allowDuringSetup);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java
new file mode 100644
index 0000000..c5dc2b4
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinatorTest.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection.coordinator;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.Notification.MediaStyle;
+import android.media.session.MediaSession;
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.media.MediaFeatureFlag;
+import com.android.systemui.statusbar.notification.collection.NotifPipeline;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public final class MediaCoordinatorTest extends SysuiTestCase {
+
+    private MediaSession mMediaSession;
+    private NotificationEntry mOtherEntry;
+    private NotificationEntry mMediaEntry;
+
+    @Mock private NotifPipeline mNotifPipeline;
+    @Mock private MediaFeatureFlag mMediaFeatureFlag;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mOtherEntry = new NotificationEntryBuilder().build();
+        mMediaSession = new MediaSession(mContext, "TEST_MEDIA_SESSION");
+        NotificationEntryBuilder builder = new NotificationEntryBuilder();
+        builder.modifyNotification(mContext).setStyle(
+                new MediaStyle().setMediaSession(mMediaSession.getSessionToken()));
+        mMediaEntry = builder.build();
+    }
+
+    @After
+    public void tearDown() {
+        mMediaSession.release();
+    }
+
+    @Test
+    public void shouldFilterOtherNotificationWhenDisabled() {
+        // GIVEN that the media feature is disabled
+        when(mMediaFeatureFlag.getEnabled()).thenReturn(false);
+        MediaCoordinator coordinator = new MediaCoordinator(mMediaFeatureFlag);
+        // WHEN the media filter is asked about an entry
+        NotifFilter filter = captureFilter(coordinator);
+        final boolean shouldFilter = filter.shouldFilterOut(mOtherEntry, 0);
+        // THEN it shouldn't be filtered
+        assertThat(shouldFilter).isFalse();
+    }
+
+    @Test
+    public void shouldFilterOtherNotificationWhenEnabled() {
+        // GIVEN that the media feature is enabled
+        when(mMediaFeatureFlag.getEnabled()).thenReturn(true);
+        MediaCoordinator coordinator = new MediaCoordinator(mMediaFeatureFlag);
+        // WHEN the media filter is asked about an entry
+        NotifFilter filter = captureFilter(coordinator);
+        final boolean shouldFilter = filter.shouldFilterOut(mOtherEntry, 0);
+        // THEN it shouldn't be filtered
+        assertThat(shouldFilter).isFalse();
+    }
+
+    @Test
+    public void shouldFilterMediaNotificationWhenDisabled() {
+        // GIVEN that the media feature is disabled
+        when(mMediaFeatureFlag.getEnabled()).thenReturn(false);
+        MediaCoordinator coordinator = new MediaCoordinator(mMediaFeatureFlag);
+        // WHEN the media filter is asked about a media entry
+        NotifFilter filter = captureFilter(coordinator);
+        final boolean shouldFilter = filter.shouldFilterOut(mMediaEntry, 0);
+        // THEN it shouldn't be filtered
+        assertThat(shouldFilter).isFalse();
+    }
+
+    @Test
+    public void shouldFilterMediaNotificationWhenEnabled() {
+        // GIVEN that the media feature is enabled
+        when(mMediaFeatureFlag.getEnabled()).thenReturn(true);
+        MediaCoordinator coordinator = new MediaCoordinator(mMediaFeatureFlag);
+        // WHEN the media filter is asked about a media entry
+        NotifFilter filter = captureFilter(coordinator);
+        final boolean shouldFilter = filter.shouldFilterOut(mMediaEntry, 0);
+        // THEN it should be filtered
+        assertThat(shouldFilter).isTrue();
+    }
+
+    private NotifFilter captureFilter(MediaCoordinator coordinator) {
+        ArgumentCaptor<NotifFilter> filterCaptor = ArgumentCaptor.forClass(NotifFilter.class);
+        coordinator.attach(mNotifPipeline);
+        verify(mNotifPipeline).addFinalizeFilter(filterCaptor.capture());
+        return filterCaptor.getValue();
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java
index b018b59..ed4f8b3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java
@@ -16,7 +16,6 @@
 
 package com.android.systemui.statusbar.notification.row;
 
-import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
@@ -30,11 +29,13 @@
 import android.util.ArraySet;
 import android.view.NotificationHeaderView;
 import android.view.View;
+import android.view.ViewPropertyAnimator;
 
 import androidx.test.annotation.UiThreadTest;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.internal.widget.NotificationExpandButton;
 import com.android.systemui.SysuiTestCase;
 
 import org.junit.Before;
@@ -98,4 +99,42 @@
         verify(mockExpanded, times(1)).setVisibility(View.VISIBLE);
         verify(mockHeadsUp, times(1)).setVisibility(View.VISIBLE);
     }
+
+    @Test
+    @UiThreadTest
+    public void testExpandButtonFocusIsCalled() {
+        View mockContractedEB = mock(NotificationExpandButton.class);
+        View mockContracted = mock(NotificationHeaderView.class);
+        when(mockContracted.animate()).thenReturn(mock(ViewPropertyAnimator.class));
+        when(mockContracted.findViewById(com.android.internal.R.id.expand_button)).thenReturn(
+                mockContractedEB);
+
+        View mockExpandedEB = mock(NotificationExpandButton.class);
+        View mockExpanded = mock(NotificationHeaderView.class);
+        when(mockExpanded.animate()).thenReturn(mock(ViewPropertyAnimator.class));
+        when(mockExpanded.findViewById(com.android.internal.R.id.expand_button)).thenReturn(
+                mockExpandedEB);
+
+        View mockHeadsUpEB = mock(NotificationExpandButton.class);
+        View mockHeadsUp = mock(NotificationHeaderView.class);
+        when(mockHeadsUp.animate()).thenReturn(mock(ViewPropertyAnimator.class));
+        when(mockHeadsUp.findViewById(com.android.internal.R.id.expand_button)).thenReturn(
+                mockHeadsUpEB);
+
+        // Set up all 3 child forms
+        mView.setContractedChild(mockContracted);
+        mView.setExpandedChild(mockExpanded);
+        mView.setHeadsUpChild(mockHeadsUp);
+
+        // This is required to call requestAccessibilityFocus()
+        mView.setFocusOnVisibilityChange();
+
+        // The following will initialize the view and switch from not visible to expanded.
+        // (heads-up is actually an alternate form of contracted, hence this enters expanded state)
+        mView.setHeadsUp(true);
+
+        verify(mockContractedEB, times(0)).requestAccessibilityFocus();
+        verify(mockExpandedEB, times(1)).requestAccessibilityFocus();
+        verify(mockHeadsUpEB, times(0)).requestAccessibilityFocus();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java
index 3dc941a..c55391a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationSectionsManagerTest.java
@@ -403,11 +403,11 @@
         enablePeopleFiltering();
 
         setupMockStack(
-                PERSON.headsUp(),
-                INCOMING_HEADER,
-                ALERTING.headsUp(),
-                PEOPLE_HEADER,
-                PERSON
+                PERSON.headsUp(),   // personHeaderTarget = 0
+                INCOMING_HEADER,    // currentIncomingHeaderIdx = 1
+                ALERTING.headsUp(), // alertingHeaderTarget = 1
+                PEOPLE_HEADER,      // currentPeopleHeaderIdx = 3
+                PERSON              //
         );
         mSectionsManager.updateSectionBoundaries();
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java
index 1a6921a..05cdd80 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java
@@ -172,11 +172,12 @@
     }
 
     @Test
-    public void testSettingTileAddedComponent_onChanged() {
+    public void testSettingTileAddedComponentAtEnd_onChanged() {
         changeValue(TEST_SETTING_COMPONENT, 1);
         waitForIdleSync();
         verify(mAutoAddTracker).setTileAdded(TEST_CUSTOM_SPEC);
-        verify(mQsTileHost).addTile(ComponentName.unflattenFromString(TEST_COMPONENT));
+        verify(mQsTileHost).addTile(ComponentName.unflattenFromString(TEST_COMPONENT)
+            , /* end */ true);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/FakeRepeatableExecutor.java b/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/FakeRepeatableExecutor.java
new file mode 100644
index 0000000..477f615
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/FakeRepeatableExecutor.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.concurrency;
+
+/**
+ * A fake to use in tests.
+ */
+public class FakeRepeatableExecutor extends RepeatableExecutorImpl {
+
+    /**
+     * Initializes a fake RepeatableExecutor from a fake executor.
+     *
+     * Use the fake executor to actually process tasks.
+     *
+     * @param executor fake executor.
+     */
+    public FakeRepeatableExecutor(FakeExecutor executor) {
+        super(executor);
+    }
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java
index 468e93a..d15c60b 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityWindowManager.java
@@ -725,7 +725,8 @@
                 case WindowManager.LayoutParams.TYPE_SYSTEM_ERROR:
                 case WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY:
                 case WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY:
-                case WindowManager.LayoutParams.TYPE_SCREENSHOT: {
+                case WindowManager.LayoutParams.TYPE_SCREENSHOT:
+                case WindowManager.LayoutParams.TYPE_TRUSTED_APPLICATION_OVERLAY: {
                     return AccessibilityWindowInfo.TYPE_SYSTEM;
                 }
 
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
index 42e859f..089861b 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java
@@ -192,7 +192,7 @@
     public AutofillManagerService(Context context) {
         super(context,
                 new SecureSettingsServiceNameResolver(context, Settings.Secure.AUTOFILL_SERVICE),
-                UserManager.DISALLOW_AUTOFILL);
+                UserManager.DISALLOW_AUTOFILL, PACKAGE_UPDATE_POLICY_REFRESH_EAGER);
         mUi = new AutoFillUI(ActivityThread.currentActivityThread().getSystemUiContext());
         mAm = LocalServices.getService(ActivityManagerInternal.class);
 
diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java
index b5aec8e..8eb401a 100644
--- a/services/core/java/com/android/server/StorageManagerService.java
+++ b/services/core/java/com/android/server/StorageManagerService.java
@@ -4657,9 +4657,8 @@
         private void killAppForOpChange(int code, int uid, String packageName) {
             final IActivityManager am = ActivityManager.getService();
             try {
-                am.killApplication(packageName,
-                        UserHandle.getAppId(uid),
-                        UserHandle.USER_ALL, AppOpsManager.opToName(code) + " changed.");
+                am.killUid(UserHandle.getAppId(uid), UserHandle.USER_ALL,
+                        AppOpsManager.opToName(code) + " changed.");
             } catch (RemoteException e) {
             }
         }
@@ -4681,7 +4680,12 @@
                                 // results in a bad UX, especially since the gid only gives access
                                 // to unreliable volumes, USB OTGs that are rarely mounted. The app
                                 // will get the external_storage gid on next organic restart.
-                                killAppForOpChange(code, uid, packageName);
+                                if (packageName != null) {
+                                    killAppForOpChange(code, uid, packageName);
+                                } else {
+                                    // TODO(b/158283222) this can happen, figure out if we need
+                                    // to kill in this case as well.
+                                }
                             }
                             return;
                         case OP_LEGACY_STORAGE:
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index a33ad7d..37ab1df 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -16,6 +16,11 @@
 
 package com.android.server.appop;
 
+import static android.app.ActivityManager.DEBUG_PROCESS_CAPABILITY_FOREGROUND_CAMERA;
+import static android.app.ActivityManager.DEBUG_PROCESS_CAPABILITY_FOREGROUND_CAMERA_Q;
+import static android.app.ActivityManager.DEBUG_PROCESS_CAPABILITY_FOREGROUND_LOCATION;
+import static android.app.ActivityManager.DEBUG_PROCESS_CAPABILITY_FOREGROUND_MICROPHONE;
+import static android.app.ActivityManager.DEBUG_PROCESS_CAPABILITY_FOREGROUND_MICROPHONE_Q;
 import static android.app.ActivityManager.PROCESS_CAPABILITY_FOREGROUND_CAMERA;
 import static android.app.ActivityManager.PROCESS_CAPABILITY_FOREGROUND_LOCATION;
 import static android.app.ActivityManager.PROCESS_CAPABILITY_FOREGROUND_MICROPHONE;
@@ -66,11 +71,6 @@
 import static android.content.pm.PermissionInfo.PROTECTION_DANGEROUS;
 import static android.content.pm.PermissionInfo.PROTECTION_FLAG_APPOP;
 
-import static android.app.ActivityManager.DEBUG_PROCESS_CAPABILITY_FOREGROUND_CAMERA;
-import static android.app.ActivityManager.DEBUG_PROCESS_CAPABILITY_FOREGROUND_CAMERA_Q;
-import static android.app.ActivityManager.DEBUG_PROCESS_CAPABILITY_FOREGROUND_LOCATION;
-import static android.app.ActivityManager.DEBUG_PROCESS_CAPABILITY_FOREGROUND_MICROPHONE;
-import static android.app.ActivityManager.DEBUG_PROCESS_CAPABILITY_FOREGROUND_MICROPHONE_Q;
 import static com.android.server.appop.AppOpsService.ModeCallback.ALL_OPS;
 
 import static java.lang.Long.max;
@@ -1776,8 +1776,7 @@
         mHandler.postDelayed(new Runnable() {
             @Override
             public void run() {
-                List<String> packageNames = getPackageNamesForSampling();
-                resamplePackageAndAppOpLocked(packageNames);
+                List<String> packageNames = getPackageListAndResample();
                 initializeRarelyUsedPackagesList(new ArraySet<>(packageNames));
             }
         }, RARELY_USED_PACKAGES_INITIALIZATION_DELAY_MILLIS);
@@ -5967,11 +5966,13 @@
         mContext.enforcePermission(android.Manifest.permission.GET_APP_OPS_STATS,
                 Binder.getCallingPid(), Binder.getCallingUid(), null);
         RuntimeAppOpAccessMessage result;
-        List<String> packageNames = getPackageNamesForSampling();
         synchronized (this) {
             result = mCollectedRuntimePermissionMessage;
-            resamplePackageAndAppOpLocked(packageNames);
+            mCollectedRuntimePermissionMessage = null;
         }
+        mHandler.sendMessage(PooledLambda.obtainMessage(
+                AppOpsService::getPackageListAndResample,
+                this));
         return result;
     }
 
@@ -5995,6 +5996,15 @@
         }
     }
 
+    /** Obtains package list and resamples package and appop to watch. */
+    private List<String> getPackageListAndResample() {
+        List<String> packageNames = getPackageNamesForSampling();
+        synchronized (this) {
+            resamplePackageAndAppOpLocked(packageNames);
+        }
+        return packageNames;
+    }
+
     /** Resamples package and appop to watch from the list provided. */
     private void resamplePackageAndAppOpLocked(@NonNull List<String> packageNames) {
         if (!packageNames.isEmpty()) {
@@ -6010,7 +6020,6 @@
         mSampledAppOpCode = ThreadLocalRandom.current().nextInt(_NUM_OP);
         mAcceptableLeftDistance = _NUM_OP;
         mSampledPackage = packageName;
-        mCollectedRuntimePermissionMessage = null;
     }
 
     /**
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 9de95ab..b9669c74 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -40,6 +40,7 @@
 import android.hardware.hdmi.HdmiDeviceInfo;
 import android.hardware.hdmi.HdmiHotplugEvent;
 import android.hardware.hdmi.HdmiPortInfo;
+import android.hardware.hdmi.IHdmiCecVolumeControlFeatureListener;
 import android.hardware.hdmi.IHdmiControlCallback;
 import android.hardware.hdmi.IHdmiControlService;
 import android.hardware.hdmi.IHdmiControlStatusChangeListener;
@@ -63,6 +64,7 @@
 import android.os.IBinder;
 import android.os.Looper;
 import android.os.PowerManager;
+import android.os.RemoteCallbackList;
 import android.os.RemoteException;
 import android.os.SystemClock;
 import android.os.SystemProperties;
@@ -268,6 +270,11 @@
     private final ArrayList<HdmiControlStatusChangeListenerRecord>
             mHdmiControlStatusChangeListenerRecords = new ArrayList<>();
 
+    // List of records for HDMI control volume control status change listener for death monitoring.
+    @GuardedBy("mLock")
+    private final RemoteCallbackList<IHdmiCecVolumeControlFeatureListener>
+            mHdmiCecVolumeControlFeatureListenerRecords = new RemoteCallbackList<>();
+
     // List of records for hotplug event listener to handle the the caller killed in action.
     @GuardedBy("mLock")
     private final ArrayList<HotplugEventListenerRecord> mHotplugEventListenerRecords =
@@ -1814,6 +1821,21 @@
         }
 
         @Override
+        public void addHdmiCecVolumeControlFeatureListener(
+                final IHdmiCecVolumeControlFeatureListener listener) {
+            enforceAccessPermission();
+            HdmiControlService.this.addHdmiCecVolumeControlFeatureListener(listener);
+        }
+
+        @Override
+        public void removeHdmiCecVolumeControlFeatureListener(
+                final IHdmiCecVolumeControlFeatureListener listener) {
+            enforceAccessPermission();
+            HdmiControlService.this.removeHdmiControlVolumeControlStatusChangeListener(listener);
+        }
+
+
+        @Override
         public void addHotplugEventListener(final IHdmiHotplugEventListener listener) {
             enforceAccessPermission();
             HdmiControlService.this.addHotplugEventListener(listener);
@@ -2409,6 +2431,33 @@
         }
     }
 
+    @VisibleForTesting
+    void addHdmiCecVolumeControlFeatureListener(
+            final IHdmiCecVolumeControlFeatureListener listener) {
+        mHdmiCecVolumeControlFeatureListenerRecords.register(listener);
+
+        runOnServiceThread(new Runnable() {
+            @Override
+            public void run() {
+                // Return the current status of mHdmiCecVolumeControlEnabled;
+                synchronized (mLock) {
+                    try {
+                        listener.onHdmiCecVolumeControlFeature(mHdmiCecVolumeControlEnabled);
+                    } catch (RemoteException e) {
+                        Slog.e(TAG, "Failed to report HdmiControlVolumeControlStatusChange: "
+                                + mHdmiCecVolumeControlEnabled, e);
+                    }
+                }
+            }
+        });
+    }
+
+    @VisibleForTesting
+    void removeHdmiControlVolumeControlStatusChangeListener(
+            final IHdmiCecVolumeControlFeatureListener listener) {
+        mHdmiCecVolumeControlFeatureListenerRecords.unregister(listener);
+    }
+
     private void addHotplugEventListener(final IHdmiHotplugEventListener listener) {
         final HotplugEventListenerRecord record = new HotplugEventListenerRecord(listener);
         try {
@@ -2682,6 +2731,19 @@
         }
     }
 
+    private void announceHdmiCecVolumeControlFeatureChange(boolean isEnabled) {
+        assertRunOnServiceThread();
+        mHdmiCecVolumeControlFeatureListenerRecords.broadcast(listener -> {
+            try {
+                listener.onHdmiCecVolumeControlFeature(isEnabled);
+            } catch (RemoteException e) {
+                Slog.e(TAG,
+                        "Failed to report HdmiControlVolumeControlStatusChange: "
+                                + isEnabled);
+            }
+        });
+    }
+
     public HdmiCecLocalDeviceTv tv() {
         return (HdmiCecLocalDeviceTv) mCecController.getLocalDevice(HdmiDeviceInfo.DEVICE_TV);
     }
@@ -3026,6 +3088,7 @@
                         isHdmiCecVolumeControlEnabled);
             }
         }
+        announceHdmiCecVolumeControlFeatureChange(isHdmiCecVolumeControlEnabled);
     }
 
     boolean isHdmiCecVolumeControlEnabled() {
diff --git a/services/core/java/com/android/server/hdmi/SystemAudioStatusAction.java b/services/core/java/com/android/server/hdmi/SystemAudioStatusAction.java
index 13f0f4ae..5d913d1 100644
--- a/services/core/java/com/android/server/hdmi/SystemAudioStatusAction.java
+++ b/services/core/java/com/android/server/hdmi/SystemAudioStatusAction.java
@@ -64,12 +64,6 @@
     }
 
     private void handleSendGiveAudioStatusFailure() {
-        // Inform to all application that the audio status (volume, mute) of
-        // the audio amplifier is unknown.
-        tv().setAudioStatus(false, Constants.UNKNOWN_VOLUME);
-
-        sendUserControlPressedAndReleased(mAvrAddress,
-                HdmiCecKeycode.getMuteKey(!tv().isSystemAudioActivated()));
 
         // Still return SUCCESS to callback.
         finishWithCallback(HdmiControlManager.RESULT_SUCCESS);
diff --git a/services/core/java/com/android/server/notification/BadgeExtractor.java b/services/core/java/com/android/server/notification/BadgeExtractor.java
index af8baa5..d323d80 100644
--- a/services/core/java/com/android/server/notification/BadgeExtractor.java
+++ b/services/core/java/com/android/server/notification/BadgeExtractor.java
@@ -19,6 +19,7 @@
 
 import android.content.Context;
 import android.util.Slog;
+import android.app.Notification;
 
 /**
  * Determines whether a badge should be shown for this notification
@@ -61,6 +62,10 @@
             record.setShowBadge(false);
         }
 
+        Notification.BubbleMetadata metadata = record.getNotification().getBubbleMetadata();
+        if (metadata != null && metadata.isNotificationSuppressed()) {
+            record.setShowBadge(false);
+        }
         return null;
     }
 
diff --git a/services/core/java/com/android/server/om/IdmapManager.java b/services/core/java/com/android/server/om/IdmapManager.java
index 59735eb..d6b1b27 100644
--- a/services/core/java/com/android/server/om/IdmapManager.java
+++ b/services/core/java/com/android/server/om/IdmapManager.java
@@ -65,7 +65,7 @@
      * modified.
      */
     boolean createIdmap(@NonNull final PackageInfo targetPackage,
-            @NonNull final PackageInfo overlayPackage, int additionalPolicies, int userId) {
+            @NonNull final PackageInfo overlayPackage, int userId) {
         if (DEBUG) {
             Slog.d(TAG, "create idmap for " + targetPackage.packageName + " and "
                     + overlayPackage.packageName);
@@ -73,14 +73,13 @@
         final String targetPath = targetPackage.applicationInfo.getBaseCodePath();
         final String overlayPath = overlayPackage.applicationInfo.getBaseCodePath();
         try {
+            int policies = calculateFulfilledPolicies(targetPackage, overlayPackage, userId);
             boolean enforce = enforceOverlayable(overlayPackage);
-            int policies = calculateFulfilledPolicies(targetPackage, overlayPackage, userId)
-                    | additionalPolicies;
             if (mIdmapDaemon.verifyIdmap(targetPath, overlayPath, policies, enforce, userId)) {
                 return false;
             }
-            return mIdmapDaemon.createIdmap(targetPath, overlayPath, policies, enforce, userId)
-                    != null;
+            return mIdmapDaemon.createIdmap(targetPath, overlayPath, policies,
+                    enforce, userId) != null;
         } catch (Exception e) {
             Slog.w(TAG, "failed to generate idmap for " + targetPath + " and "
                     + overlayPath + ": " + e.getMessage());
diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java
index 3c5e476..3968153 100644
--- a/services/core/java/com/android/server/om/OverlayManagerService.java
+++ b/services/core/java/com/android/server/om/OverlayManagerService.java
@@ -45,7 +45,6 @@
 import android.content.pm.PackageManagerInternal;
 import android.content.pm.UserInfo;
 import android.content.res.ApkAssets;
-import android.content.res.Resources;
 import android.net.Uri;
 import android.os.Binder;
 import android.os.Environment;
@@ -63,7 +62,6 @@
 import android.util.Slog;
 import android.util.SparseArray;
 
-import com.android.internal.R;
 import com.android.internal.content.om.OverlayConfig;
 import com.android.server.FgThread;
 import com.android.server.IoThread;
@@ -252,8 +250,7 @@
             mSettings = new OverlayManagerSettings();
             mImpl = new OverlayManagerServiceImpl(mPackageManager, im, mSettings,
                     OverlayConfig.getSystemInstance(), getDefaultOverlayPackages(),
-                    new OverlayChangeListener(), getOverlayableConfigurator(),
-                    getOverlayableConfiguratorTargets());
+                    new OverlayChangeListener());
             mActorEnforcer = new OverlayActorEnforcer(mPackageManager);
 
             final IntentFilter packageFilter = new IntentFilter();
@@ -336,28 +333,6 @@
         return defaultPackages.toArray(new String[defaultPackages.size()]);
     }
 
-
-    /**
-     * Retrieves the package name that is recognized as an actor for the packages specified by
-     * {@link #getOverlayableConfiguratorTargets()}.
-     */
-    @Nullable
-    private String getOverlayableConfigurator() {
-        return TextUtils.nullIfEmpty(Resources.getSystem()
-                .getString(R.string.config_overlayableConfigurator));
-    }
-
-    /**
-     * Retrieves the target packages that recognize the {@link #getOverlayableConfigurator} as an
-     * actor for itself. Overlays targeting one of the specified targets that are signed with the
-     * same signature as the overlayable configurator will be granted the "actor" policy.
-     */
-    @Nullable
-    private String[] getOverlayableConfiguratorTargets() {
-        return Resources.getSystem().getStringArray(
-                R.array.config_overlayableConfiguratorTargets);
-    }
-
     private final class PackageReceiver extends BroadcastReceiver {
         @Override
         public void onReceive(@NonNull final Context context, @NonNull final Intent intent) {
diff --git a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
index 879ad4f..05a4a38 100644
--- a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
+++ b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
@@ -31,7 +31,6 @@
 import android.content.om.OverlayInfo;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageInfo;
-import android.os.OverlayablePolicy;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -74,9 +73,6 @@
     private final String[] mDefaultOverlays;
     private final OverlayChangeListener mListener;
 
-    private final String mOverlayableConfigurator;
-    private final String[] mOverlayableConfiguratorTargets;
-
     /**
      * Helper method to merge the overlay manager's (as read from overlays.xml)
      * and package manager's (as parsed from AndroidManifest.xml files) views
@@ -119,17 +115,13 @@
             @NonNull final OverlayManagerSettings settings,
             @NonNull final OverlayConfig overlayConfig,
             @NonNull final String[] defaultOverlays,
-            @NonNull final OverlayChangeListener listener,
-            @Nullable final String overlayableConfigurator,
-            @Nullable final String[] overlayableConfiguratorTargets) {
+            @NonNull final OverlayChangeListener listener) {
         mPackageManager = packageManager;
         mIdmapManager = idmapManager;
         mSettings = settings;
         mOverlayConfig = overlayConfig;
         mDefaultOverlays = defaultOverlays;
         mListener = listener;
-        mOverlayableConfigurator = overlayableConfigurator;
-        mOverlayableConfiguratorTargets = overlayableConfiguratorTargets;
     }
 
     /**
@@ -714,25 +706,7 @@
         if (targetPackage != null && overlayPackage != null
                 && !("android".equals(targetPackageName)
                     && !isPackageConfiguredMutable(overlayPackageName))) {
-
-            int additionalPolicies = 0;
-            if (TextUtils.nullIfEmpty(mOverlayableConfigurator) != null
-                    && ArrayUtils.contains(mOverlayableConfiguratorTargets, targetPackageName)
-                    && isPackageConfiguredMutable(overlayPackageName)
-                    && mPackageManager.signaturesMatching(mOverlayableConfigurator,
-                            overlayPackageName, userId)) {
-                // The overlay targets a package that has the overlayable configurator configured as
-                // its actor. The overlay and this actor are signed with the same signature, so
-                // the overlay fulfills the actor policy.
-                modified |= mSettings.setHasConfiguratorActorPolicy(overlayPackageName, userId,
-                        true);
-                additionalPolicies |= OverlayablePolicy.ACTOR_SIGNATURE;
-            } else if (mSettings.hasConfiguratorActorPolicy(overlayPackageName, userId)) {
-                additionalPolicies |= OverlayablePolicy.ACTOR_SIGNATURE;
-            }
-
-            modified |= mIdmapManager.createIdmap(targetPackage, overlayPackage, additionalPolicies,
-                    userId);
+            modified |= mIdmapManager.createIdmap(targetPackage, overlayPackage, userId);
         }
 
         if (overlayPackage != null) {
diff --git a/services/core/java/com/android/server/om/OverlayManagerSettings.java b/services/core/java/com/android/server/om/OverlayManagerSettings.java
index f8226fa..3d520bf 100644
--- a/services/core/java/com/android/server/om/OverlayManagerSettings.java
+++ b/services/core/java/com/android/server/om/OverlayManagerSettings.java
@@ -73,7 +73,7 @@
         remove(packageName, userId);
         insert(new SettingsItem(packageName, userId, targetPackageName, targetOverlayableName,
                 baseCodePath, OverlayInfo.STATE_UNKNOWN, isEnabled, isMutable, priority,
-                overlayCategory, false /* hasConfiguratorActorPolicy */));
+                overlayCategory));
     }
 
     /**
@@ -160,26 +160,6 @@
         return mItems.get(idx).setState(state);
     }
 
-    boolean hasConfiguratorActorPolicy(@NonNull final String packageName, final int userId) {
-        final int idx = select(packageName, userId);
-        if (idx < 0) {
-            throw new BadKeyException(packageName, userId);
-        }
-        return mItems.get(idx).hasConfiguratorActorPolicy();
-    }
-
-    /**
-     * Returns true if the settings were modified, false if they remain the same.
-     */
-    boolean setHasConfiguratorActorPolicy(@NonNull final String packageName, final int userId,
-            boolean hasPolicy) {
-        final int idx = select(packageName, userId);
-        if (idx < 0) {
-            throw new BadKeyException(packageName, userId);
-        }
-        return mItems.get(idx).setHasConfiguratorActorPolicy(hasPolicy);
-    }
-
     List<OverlayInfo> getOverlaysForTarget(@NonNull final String targetPackageName,
             final int userId) {
         // Immutable RROs targeting "android" are loaded from AssetManager, and so they should be
@@ -343,17 +323,16 @@
         pw.println(item.mPackageName + ":" + item.getUserId() + " {");
         pw.increaseIndent();
 
-        pw.println("mPackageName................: " + item.mPackageName);
-        pw.println("mUserId.....................: " + item.getUserId());
-        pw.println("mTargetPackageName..........: " + item.getTargetPackageName());
-        pw.println("mTargetOverlayableName......: " + item.getTargetOverlayableName());
-        pw.println("mBaseCodePath...............: " + item.getBaseCodePath());
-        pw.println("mState......................: " + OverlayInfo.stateToString(item.getState()));
-        pw.println("mIsEnabled..................: " + item.isEnabled());
-        pw.println("mIsMutable..................: " + item.isMutable());
-        pw.println("mPriority...................: " + item.mPriority);
-        pw.println("mCategory...................: " + item.mCategory);
-        pw.println("mHasConfiguratorActorPolicy.: " + item.hasConfiguratorActorPolicy());
+        pw.println("mPackageName...........: " + item.mPackageName);
+        pw.println("mUserId................: " + item.getUserId());
+        pw.println("mTargetPackageName.....: " + item.getTargetPackageName());
+        pw.println("mTargetOverlayableName.: " + item.getTargetOverlayableName());
+        pw.println("mBaseCodePath..........: " + item.getBaseCodePath());
+        pw.println("mState.................: " + OverlayInfo.stateToString(item.getState()));
+        pw.println("mIsEnabled.............: " + item.isEnabled());
+        pw.println("mIsMutable.............: " + item.isMutable());
+        pw.println("mPriority..............: " + item.mPriority);
+        pw.println("mCategory..............: " + item.mCategory);
 
         pw.decreaseIndent();
         pw.println("}");
@@ -392,9 +371,6 @@
             case "category":
                 pw.println(item.mCategory);
                 break;
-            case "hasconfiguratoractorpolicy":
-                pw.println(item.mHasConfiguratorActorPolicy);
-                break;
         }
     }
 
@@ -422,8 +398,6 @@
         private static final String ATTR_CATEGORY = "category";
         private static final String ATTR_USER_ID = "userId";
         private static final String ATTR_VERSION = "version";
-        private static final String ATTR_HAS_CONFIGURATOR_ACTOR_POLICY =
-                "hasConfiguratorActorPolicy";
 
         @VisibleForTesting
         static final int CURRENT_VERSION = 4;
@@ -461,6 +435,10 @@
                     // Throw an exception which will cause the overlay file to be ignored
                     // and overwritten.
                     throw new XmlPullParserException("old version " + oldVersion + "; ignoring");
+                case 3:
+                    // Upgrading from version 3 to 4 is not a breaking change so do not ignore the
+                    // overlay file.
+                    return;
                 default:
                     throw new XmlPullParserException("unrecognized version " + oldVersion);
             }
@@ -480,12 +458,9 @@
             final boolean isStatic = XmlUtils.readBooleanAttribute(parser, ATTR_IS_STATIC);
             final int priority = XmlUtils.readIntAttribute(parser, ATTR_PRIORITY);
             final String category = XmlUtils.readStringAttribute(parser, ATTR_CATEGORY);
-            final boolean hasConfiguratorActorPolicy = XmlUtils.readBooleanAttribute(parser,
-                    ATTR_HAS_CONFIGURATOR_ACTOR_POLICY);
 
             return new SettingsItem(packageName, userId, targetPackageName, targetOverlayableName,
-                    baseCodePath, state, isEnabled, !isStatic, priority, category,
-                    hasConfiguratorActorPolicy);
+                    baseCodePath, state, isEnabled, !isStatic, priority, category);
         }
 
         public static void persist(@NonNull final ArrayList<SettingsItem> table,
@@ -520,8 +495,6 @@
             XmlUtils.writeBooleanAttribute(xml, ATTR_IS_STATIC, !item.mIsMutable);
             XmlUtils.writeIntAttribute(xml, ATTR_PRIORITY, item.mPriority);
             XmlUtils.writeStringAttribute(xml, ATTR_CATEGORY, item.mCategory);
-            XmlUtils.writeBooleanAttribute(xml, ATTR_HAS_CONFIGURATOR_ACTOR_POLICY,
-                    item.mHasConfiguratorActorPolicy);
             xml.endTag(null, TAG_ITEM);
         }
     }
@@ -538,14 +511,12 @@
         private boolean mIsMutable;
         private int mPriority;
         private String mCategory;
-        private boolean mHasConfiguratorActorPolicy;
 
         SettingsItem(@NonNull final String packageName, final int userId,
                 @NonNull final String targetPackageName,
                 @Nullable final String targetOverlayableName, @NonNull final String baseCodePath,
                 final @OverlayInfo.State int state, final boolean isEnabled,
-                final boolean isMutable, final int priority,  @Nullable String category,
-                final boolean hasConfiguratorActorPolicy) {
+                final boolean isMutable, final int priority,  @Nullable String category) {
             mPackageName = packageName;
             mUserId = userId;
             mTargetPackageName = targetPackageName;
@@ -557,7 +528,6 @@
             mCache = null;
             mIsMutable = isMutable;
             mPriority = priority;
-            mHasConfiguratorActorPolicy = hasConfiguratorActorPolicy;
         }
 
         private String getTargetPackageName() {
@@ -648,18 +618,6 @@
         private int getPriority() {
             return mPriority;
         }
-
-        private boolean hasConfiguratorActorPolicy() {
-            return mHasConfiguratorActorPolicy;
-        }
-
-        private boolean setHasConfiguratorActorPolicy(boolean hasPolicy) {
-            if (mHasConfiguratorActorPolicy != hasPolicy) {
-                mHasConfiguratorActorPolicy = hasPolicy;
-                return true;
-            }
-            return false;
-        }
     }
 
     private int select(@NonNull final String packageName, final int userId) {
diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java
index 49c7819..2f963b7 100644
--- a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java
+++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerModule.java
@@ -20,11 +20,16 @@
 import android.annotation.Nullable;
 import android.hardware.soundtrigger.V2_1.ISoundTriggerHwCallback;
 import android.hardware.soundtrigger.V2_2.ISoundTriggerHw;
+import android.media.audio.common.AudioConfig;
+import android.media.audio.common.AudioOffloadInfo;
 import android.media.soundtrigger_middleware.ISoundTriggerCallback;
 import android.media.soundtrigger_middleware.ISoundTriggerModule;
 import android.media.soundtrigger_middleware.ModelParameterRange;
+import android.media.soundtrigger_middleware.PhraseRecognitionEvent;
+import android.media.soundtrigger_middleware.PhraseRecognitionExtra;
 import android.media.soundtrigger_middleware.PhraseSoundModel;
 import android.media.soundtrigger_middleware.RecognitionConfig;
+import android.media.soundtrigger_middleware.RecognitionEvent;
 import android.media.soundtrigger_middleware.SoundModel;
 import android.media.soundtrigger_middleware.SoundModelType;
 import android.media.soundtrigger_middleware.SoundTriggerModuleProperties;
@@ -540,20 +545,20 @@
                     switch (mModelType) {
                         case SoundModelType.GENERIC: {
                             android.media.soundtrigger_middleware.RecognitionEvent event =
-                                    new android.media.soundtrigger_middleware.RecognitionEvent();
+                                    newEmptyRecognitionEvent();
                             event.status =
                                     android.media.soundtrigger_middleware.RecognitionStatus.ABORTED;
+                            event.type = SoundModelType.GENERIC;
                             mCallback.onRecognition(mHandle, event);
                         }
                         break;
 
                         case SoundModelType.KEYPHRASE: {
                             android.media.soundtrigger_middleware.PhraseRecognitionEvent event =
-                                    new android.media.soundtrigger_middleware.PhraseRecognitionEvent();
-                            event.common =
-                                    new android.media.soundtrigger_middleware.RecognitionEvent();
+                                    newEmptyPhraseRecognitionEvent();
                             event.common.status =
                                     android.media.soundtrigger_middleware.RecognitionStatus.ABORTED;
+                            event.common.type = SoundModelType.KEYPHRASE;
                             mCallback.onPhraseRecognition(mHandle, event);
                         }
                         break;
@@ -614,4 +619,35 @@
             }
         }
     }
+
+    /**
+     * Creates a default-initialized recognition event.
+     *
+     * Object fields are default constructed.
+     * Array fields are initialized to 0 length.
+     *
+     * @return The event.
+     */
+    private static RecognitionEvent newEmptyRecognitionEvent() {
+        RecognitionEvent result = new RecognitionEvent();
+        result.audioConfig = new AudioConfig();
+        result.audioConfig.offloadInfo = new AudioOffloadInfo();
+        result.data = new byte[0];
+        return result;
+    }
+
+    /**
+     * Creates a default-initialized phrase recognition event.
+     *
+     * Object fields are default constructed.
+     * Array fields are initialized to 0 length.
+     *
+     * @return The event.
+     */
+    private static PhraseRecognitionEvent newEmptyPhraseRecognitionEvent() {
+        PhraseRecognitionEvent result = new PhraseRecognitionEvent();
+        result.common = newEmptyRecognitionEvent();
+        result.phraseExtras = new PhraseRecognitionExtra[0];
+        return result;
+    }
 }
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 3189705..48609e1 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -3164,6 +3164,11 @@
     }
 
     @Override
+    public SurfaceControl.Builder makeAnimationLeash() {
+        return super.makeAnimationLeash().setMetadata(METADATA_TASK_ID, mTaskId);
+    }
+
+    @Override
     public SurfaceControl getAnimationLeashParent() {
         if (WindowManagerService.sHierarchicalAnimations) {
             return super.getAnimationLeashParent();
diff --git a/services/core/java/com/android/server/wm/TaskSnapshotSurface.java b/services/core/java/com/android/server/wm/TaskSnapshotSurface.java
index 3925570..e26f1e1 100644
--- a/services/core/java/com/android/server/wm/TaskSnapshotSurface.java
+++ b/services/core/java/com/android/server/wm/TaskSnapshotSurface.java
@@ -391,6 +391,7 @@
             frame = null;
             mTmpSnapshotSize.set(0, 0, buffer.getWidth(), buffer.getHeight());
             mTmpDstFrame.set(mFrame);
+            mTmpDstFrame.offsetTo(0, 0);
         }
 
         // Scale the mismatch dimensions to fill the task bounds
diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java
index 42c2193..c025236 100644
--- a/services/core/java/com/android/server/wm/WindowStateAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java
@@ -418,25 +418,25 @@
         if (!mDestroyPreservedSurfaceUponRedraw) {
             return;
         }
-        if (mSurfaceController != null) {
-            if (mPendingDestroySurface != null) {
-                // If we are preserving a surface but we aren't relaunching that means
-                // we are just doing an in-place switch. In that case any SurfaceFlinger side
-                // child layers need to be reparented to the new surface to make this
-                // transparent to the app.
-                if (mWin.mActivityRecord == null || mWin.mActivityRecord.isRelaunching() == false) {
-                    mPostDrawTransaction.reparentChildren(
-                        mPendingDestroySurface.getClientViewRootSurface(),
-                        mSurfaceController.mSurfaceControl).apply();
-                }
-            }
+
+        // If we are preserving a surface but we aren't relaunching that means
+        // we are just doing an in-place switch. In that case any SurfaceFlinger side
+        // child layers need to be reparented to the new surface to make this
+        // transparent to the app.
+        // If the children are detached, we don't want to reparent them to the new surface.
+        // Instead let the children get removed when the old surface is deleted.
+        if (mSurfaceController != null && mPendingDestroySurface != null && !mChildrenDetached
+                && (mWin.mActivityRecord == null || !mWin.mActivityRecord.isRelaunching())) {
+            mPostDrawTransaction.reparentChildren(
+                    mPendingDestroySurface.getClientViewRootSurface(),
+                    mSurfaceController.mSurfaceControl).apply();
         }
 
         destroyDeferredSurfaceLocked();
         mDestroyPreservedSurfaceUponRedraw = false;
     }
 
-    void markPreservedSurfaceForDestroy() {
+    private void markPreservedSurfaceForDestroy() {
         if (mDestroyPreservedSurfaceUponRedraw
                 && !mService.mDestroyPreservedSurface.contains(mWin)) {
             mService.mDestroyPreservedSurface.add(mWin);
@@ -1363,9 +1363,13 @@
         if (mPendingDestroySurface != null && mDestroyPreservedSurfaceUponRedraw) {
             final SurfaceControl pendingSurfaceControl = mPendingDestroySurface.mSurfaceControl;
             mPostDrawTransaction.reparent(pendingSurfaceControl, null);
-            mPostDrawTransaction.reparentChildren(
-                mPendingDestroySurface.getClientViewRootSurface(),
-                mSurfaceController.mSurfaceControl);
+            // If the children are detached, we don't want to reparent them to the new surface.
+            // Instead let the children get removed when the old surface is deleted.
+            if (!mChildrenDetached) {
+                mPostDrawTransaction.reparentChildren(
+                        mPendingDestroySurface.getClientViewRootSurface(),
+                        mSurfaceController.mSurfaceControl);
+            }
         }
 
         SurfaceControl.mergeToGlobalTransaction(mPostDrawTransaction);
@@ -1593,6 +1597,12 @@
             mSurfaceController.detachChildren();
         }
         mChildrenDetached = true;
+        // If the children are detached, it means the app is exiting. We don't want to tear the
+        // content down too early, otherwise we could end up with a flicker. By preserving the
+        // current surface, we ensure the content remains on screen until the window is completely
+        // removed. It also ensures that the old surface is cleaned up when started again since it
+        // forces mSurfaceController to be set to null.
+        preserveSurfaceLocked();
     }
 
     void setOffsetPositionForStackResize(boolean offsetPositionForStackResize) {
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
index 145e1ec..949bcfe 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java
@@ -184,6 +184,32 @@
     }
 
     @Test
+    public void handleOnStandby_ScreenOff_NotActiveSource() {
+        mHdmiCecLocalDevicePlayback.setIsActiveSource(false);
+        mHdmiCecLocalDevicePlayback.setAutoDeviceOff(true);
+        mHdmiCecLocalDevicePlayback.onStandby(false, HdmiControlService.STANDBY_SCREEN_OFF);
+        mTestLooper.dispatchAll();
+
+        HdmiCecMessage standbyMessage = HdmiCecMessageBuilder.buildStandby(
+                mHdmiCecLocalDevicePlayback.mAddress, ADDR_TV);
+
+        assertThat(mNativeWrapper.getResultMessages()).doesNotContain(standbyMessage);
+    }
+
+    @Test
+    public void handleOnStandby_ScreenOff_ActiveSource() {
+        mHdmiCecLocalDevicePlayback.setIsActiveSource(true);
+        mHdmiCecLocalDevicePlayback.setAutoDeviceOff(true);
+        mHdmiCecLocalDevicePlayback.onStandby(false, HdmiControlService.STANDBY_SCREEN_OFF);
+        mTestLooper.dispatchAll();
+
+        HdmiCecMessage standbyMessage = HdmiCecMessageBuilder.buildStandby(
+                mHdmiCecLocalDevicePlayback.mAddress, ADDR_TV);
+
+        assertThat(mNativeWrapper.getResultMessages()).contains(standbyMessage);
+    }
+
+    @Test
     public void sendVolumeKeyEvent_up_volumeEnabled() {
         mHdmiControlService.setHdmiCecVolumeControlEnabled(true);
         mHdmiCecLocalDevicePlayback.sendVolumeKeyEvent(KeyEvent.KEYCODE_VOLUME_UP, true);
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
index 7af7a23..c34b8e1 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiControlServiceTest.java
@@ -33,6 +33,7 @@
 import android.content.ContextWrapper;
 import android.hardware.hdmi.HdmiControlManager;
 import android.hardware.hdmi.HdmiPortInfo;
+import android.hardware.hdmi.IHdmiCecVolumeControlFeatureListener;
 import android.os.IPowerManager;
 import android.os.IThermalService;
 import android.os.Looper;
@@ -261,4 +262,89 @@
         mHdmiControlService.setHdmiCecVolumeControlEnabled(true);
         assertThat(mHdmiControlService.isHdmiCecVolumeControlEnabled()).isTrue();
     }
+
+    @Test
+    public void addHdmiCecVolumeControlFeatureListener_emitsCurrentState_enabled() {
+        mHdmiControlService.setHdmiCecVolumeControlEnabled(true);
+        VolumeControlFeatureCallback callback = new VolumeControlFeatureCallback();
+
+        mHdmiControlService.addHdmiCecVolumeControlFeatureListener(callback);
+        mTestLooper.dispatchAll();
+
+        assertThat(callback.mCallbackReceived).isTrue();
+        assertThat(callback.mVolumeControlEnabled).isTrue();
+    }
+
+    @Test
+    public void addHdmiCecVolumeControlFeatureListener_emitsCurrentState_disabled() {
+        mHdmiControlService.setHdmiCecVolumeControlEnabled(false);
+        VolumeControlFeatureCallback callback = new VolumeControlFeatureCallback();
+
+        mHdmiControlService.addHdmiCecVolumeControlFeatureListener(callback);
+        mTestLooper.dispatchAll();
+
+        assertThat(callback.mCallbackReceived).isTrue();
+        assertThat(callback.mVolumeControlEnabled).isFalse();
+    }
+
+    @Test
+    public void addHdmiCecVolumeControlFeatureListener_notifiesStateUpdate() {
+        mHdmiControlService.setHdmiCecVolumeControlEnabled(false);
+        VolumeControlFeatureCallback callback = new VolumeControlFeatureCallback();
+
+        mHdmiControlService.addHdmiCecVolumeControlFeatureListener(callback);
+
+        mHdmiControlService.setHdmiCecVolumeControlEnabled(true);
+        mTestLooper.dispatchAll();
+
+        assertThat(callback.mCallbackReceived).isTrue();
+        assertThat(callback.mVolumeControlEnabled).isTrue();
+    }
+
+    @Test
+    public void addHdmiCecVolumeControlFeatureListener_honorsUnregistration() {
+        mHdmiControlService.setHdmiCecVolumeControlEnabled(false);
+        VolumeControlFeatureCallback callback = new VolumeControlFeatureCallback();
+
+        mHdmiControlService.addHdmiCecVolumeControlFeatureListener(callback);
+        mTestLooper.dispatchAll();
+
+        mHdmiControlService.removeHdmiControlVolumeControlStatusChangeListener(callback);
+        mHdmiControlService.setHdmiCecVolumeControlEnabled(true);
+        mTestLooper.dispatchAll();
+
+        assertThat(callback.mCallbackReceived).isTrue();
+        assertThat(callback.mVolumeControlEnabled).isFalse();
+    }
+
+    @Test
+    public void addHdmiCecVolumeControlFeatureListener_notifiesStateUpdate_multiple() {
+        mHdmiControlService.setHdmiCecVolumeControlEnabled(false);
+        VolumeControlFeatureCallback callback1 = new VolumeControlFeatureCallback();
+        VolumeControlFeatureCallback callback2 = new VolumeControlFeatureCallback();
+
+        mHdmiControlService.addHdmiCecVolumeControlFeatureListener(callback1);
+        mHdmiControlService.addHdmiCecVolumeControlFeatureListener(callback2);
+
+
+        mHdmiControlService.setHdmiCecVolumeControlEnabled(true);
+        mTestLooper.dispatchAll();
+
+        assertThat(callback1.mCallbackReceived).isTrue();
+        assertThat(callback2.mCallbackReceived).isTrue();
+        assertThat(callback1.mVolumeControlEnabled).isTrue();
+        assertThat(callback2.mVolumeControlEnabled).isTrue();
+    }
+
+    private static class VolumeControlFeatureCallback extends
+            IHdmiCecVolumeControlFeatureListener.Stub {
+        boolean mCallbackReceived = false;
+        boolean mVolumeControlEnabled = false;
+
+        @Override
+        public void onHdmiCecVolumeControlFeature(boolean enabled) throws RemoteException {
+            this.mCallbackReceived = true;
+            this.mVolumeControlEnabled = enabled;
+        }
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTests.java b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTests.java
index 8774ab0..f4c5506 100644
--- a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTests.java
+++ b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTests.java
@@ -26,7 +26,6 @@
 import static org.junit.Assert.assertTrue;
 
 import android.content.om.OverlayInfo;
-import android.os.OverlayablePolicy;
 
 import androidx.test.runner.AndroidJUnit4;
 
@@ -205,138 +204,4 @@
         impl.setEnabled(OVERLAY, true, USER);
         assertEquals(0, listener.count);
     }
-
-    @Test
-    public void testConfigurator() {
-        mOverlayableConfigurator = "actor";
-        mOverlayableConfiguratorTargets = new String[]{TARGET};
-        reinitializeImpl();
-
-        installNewPackage(target("actor").setCertificate("one"), USER);
-        installNewPackage(target(TARGET).addOverlayable("TestResources").setCertificate("two"),
-                USER);
-
-        DummyDeviceState.PackageBuilder overlay = overlay(OVERLAY, TARGET, "TestResources")
-                .setCertificate("one");
-        installNewPackage(overlay, USER);
-
-        DummyIdmapDaemon.IdmapHeader idmap = getIdmapDaemon().getIdmap(overlay.build().apkPath);
-        assertNotNull(idmap);
-        assertEquals(OverlayablePolicy.ACTOR_SIGNATURE,
-                idmap.policies & OverlayablePolicy.ACTOR_SIGNATURE);
-    }
-
-    @Test
-    public void testConfiguratorWithoutOverlayable() {
-        mOverlayableConfigurator = "actor";
-        mOverlayableConfiguratorTargets = new String[]{TARGET};
-        reinitializeImpl();
-
-        installNewPackage(target("actor").setCertificate("one"), USER);
-        installNewPackage(target(TARGET).setCertificate("two"), USER);
-
-        DummyDeviceState.PackageBuilder overlay = overlay(OVERLAY, TARGET).setCertificate("one");
-        installNewPackage(overlay, USER);
-
-        DummyIdmapDaemon.IdmapHeader idmap = getIdmapDaemon().getIdmap(overlay.build().apkPath);
-        assertNotNull(idmap);
-        assertEquals(OverlayablePolicy.ACTOR_SIGNATURE,
-                idmap.policies & OverlayablePolicy.ACTOR_SIGNATURE);
-    }
-
-    @Test
-    public void testConfiguratorDifferentTargets() {
-        // The target package is not listed in the configurator target list, so the actor policy
-        // should not be granted.
-        mOverlayableConfigurator = "actor";
-        mOverlayableConfiguratorTargets = new String[]{"somethingElse"};
-        reinitializeImpl();
-
-        installNewPackage(target("actor").setCertificate("one"), USER);
-        installNewPackage(target(TARGET).setCertificate("two"), USER);
-
-        DummyDeviceState.PackageBuilder overlay = overlay(OVERLAY, TARGET).setCertificate("one");
-        installNewPackage(overlay, USER);
-
-        DummyIdmapDaemon.IdmapHeader idmap = getIdmapDaemon().getIdmap(overlay.build().apkPath);
-        assertNotNull(idmap);
-        assertEquals(0, idmap.policies & OverlayablePolicy.ACTOR_SIGNATURE);
-    }
-
-    @Test
-    public void testConfiguratorDifferentSignatures() {
-        mOverlayableConfigurator = "actor";
-        mOverlayableConfiguratorTargets = new String[]{TARGET};
-        reinitializeImpl();
-
-        installNewPackage(target("actor").setCertificate("one"), USER);
-        installNewPackage(target(TARGET).addOverlayable("TestResources").setCertificate("two"),
-                USER);
-
-        DummyDeviceState.PackageBuilder overlay = overlay(OVERLAY, TARGET, "TestResources")
-                .setCertificate("two");
-        installNewPackage(overlay, USER);
-
-        DummyIdmapDaemon.IdmapHeader idmap = getIdmapDaemon().getIdmap(overlay.build().apkPath);
-        assertNotNull(idmap);
-        assertEquals(0, idmap.policies & OverlayablePolicy.ACTOR_SIGNATURE);
-    }
-
-    @Test
-    public void testConfiguratorWithoutOverlayableDifferentSignatures() {
-        mOverlayableConfigurator = "actor";
-        mOverlayableConfiguratorTargets = new String[]{TARGET};
-        reinitializeImpl();
-
-        installNewPackage(target("actor").setCertificate("one"), USER);
-        installNewPackage(target(TARGET).setCertificate("two"), USER);
-
-        DummyDeviceState.PackageBuilder overlay = overlay(OVERLAY, TARGET).setCertificate("two");
-        installNewPackage(overlay, USER);
-
-        DummyIdmapDaemon.IdmapHeader idmap = getIdmapDaemon().getIdmap(overlay.build().apkPath);
-        assertNotNull(idmap);
-        assertEquals(0, idmap.policies & OverlayablePolicy.ACTOR_SIGNATURE);
-    }
-
-    @Test
-    public void testConfiguratorChanges() {
-        mOverlayableConfigurator = "actor";
-        mOverlayableConfiguratorTargets = new String[]{TARGET};
-        reinitializeImpl();
-
-        installNewPackage(target("actor").setCertificate("one"), USER);
-        installNewPackage(target(TARGET).addOverlayable("TestResources").setCertificate("two"),
-                USER);
-
-        DummyDeviceState.PackageBuilder overlay = overlay(OVERLAY, TARGET, "TestResources")
-                .setCertificate("one");
-        installNewPackage(overlay, USER);
-
-        DummyIdmapDaemon.IdmapHeader idmap = getIdmapDaemon().getIdmap(overlay.build().apkPath);
-        assertNotNull(idmap);
-        assertEquals(OverlayablePolicy.ACTOR_SIGNATURE,
-                idmap.policies & OverlayablePolicy.ACTOR_SIGNATURE);
-
-        // Change the configurator to a different package. The overlay should still be granted the
-        // actor policy.
-        mOverlayableConfigurator = "differentActor";
-        OverlayManagerServiceImpl impl = reinitializeImpl();
-        impl.updateOverlaysForUser(USER);
-
-        idmap = getIdmapDaemon().getIdmap(overlay.build().apkPath);
-        assertNotNull(idmap);
-        assertEquals(OverlayablePolicy.ACTOR_SIGNATURE,
-                idmap.policies & OverlayablePolicy.ACTOR_SIGNATURE);
-
-        // Reset the setting persisting that the overlay once fulfilled the actor policy implicitly
-        // through the configurator. The overlay should lose the actor policy.
-        impl = reinitializeImpl();
-        getSettings().setHasConfiguratorActorPolicy(OVERLAY, USER, false);
-        impl.updateOverlaysForUser(USER);
-
-        idmap = getIdmapDaemon().getIdmap(overlay.build().apkPath);
-        assertNotNull(idmap);
-        assertEquals(0, idmap.policies & OverlayablePolicy.ACTOR_SIGNATURE);
-    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTestsBase.java b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTestsBase.java
index 52a5890..733310b 100644
--- a/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTestsBase.java
+++ b/services/tests/servicestests/src/com/android/server/om/OverlayManagerServiceImplTestsBase.java
@@ -52,9 +52,6 @@
     private DummyPackageManagerHelper mPackageManager;
     private DummyIdmapDaemon mIdmapDaemon;
     private OverlayConfig mOverlayConfig;
-    private OverlayManagerSettings mSettings;
-    String mOverlayableConfigurator;
-    String[] mOverlayableConfiguratorTargets;
 
     @Before
     public void setUp() {
@@ -62,26 +59,20 @@
         mListener = new DummyListener();
         mPackageManager = new DummyPackageManagerHelper(mState);
         mIdmapDaemon = new DummyIdmapDaemon(mState);
-        mSettings = new OverlayManagerSettings();
         mOverlayConfig = mock(OverlayConfig.class);
         when(mOverlayConfig.getPriority(any())).thenReturn(OverlayConfig.DEFAULT_PRIORITY);
         when(mOverlayConfig.isEnabled(any())).thenReturn(false);
         when(mOverlayConfig.isMutable(any())).thenReturn(true);
-        mOverlayableConfigurator = null;
-        mOverlayableConfiguratorTargets = null;
         reinitializeImpl();
     }
 
-    OverlayManagerServiceImpl reinitializeImpl() {
+    void reinitializeImpl() {
         mImpl = new OverlayManagerServiceImpl(mPackageManager,
                 new IdmapManager(mIdmapDaemon, mPackageManager),
-                mSettings,
+                new OverlayManagerSettings(),
                 mOverlayConfig,
                 new String[0],
-                mListener,
-                mOverlayableConfigurator,
-                mOverlayableConfiguratorTargets);
-        return mImpl;
+                mListener);
     }
 
     OverlayManagerServiceImpl getImpl() {
@@ -92,14 +83,6 @@
         return mListener;
     }
 
-    DummyIdmapDaemon getIdmapDaemon() {
-        return mIdmapDaemon;
-    }
-
-    OverlayManagerSettings getSettings() {
-        return mSettings;
-    }
-
     void assertState(@State int expected, final String overlayPackageName, int userId) {
         final OverlayInfo info = mImpl.getOverlayInfo(overlayPackageName, userId);
         if (info == null) {
diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayManagerSettingsTests.java b/services/tests/servicestests/src/com/android/server/om/OverlayManagerSettingsTests.java
index e2cedb5..146f60a 100644
--- a/services/tests/servicestests/src/com/android/server/om/OverlayManagerSettingsTests.java
+++ b/services/tests/servicestests/src/com/android/server/om/OverlayManagerSettingsTests.java
@@ -367,8 +367,7 @@
                 + "      isEnabled='false'\n"
                 + "      category='dummy-category'\n"
                 + "      isStatic='false'\n"
-                + "      priority='0'"
-                + "      hasConfiguratorActorPolicy='true' />\n"
+                + "      priority='0' />\n"
                 + "</overlays>\n";
         ByteArrayInputStream is = new ByteArrayInputStream(xml.getBytes("utf-8"));
 
@@ -381,7 +380,6 @@
         assertEquals(1234, oi.userId);
         assertEquals(STATE_DISABLED, oi.state);
         assertFalse(mSettings.getEnabled("com.dummy.overlay", 1234));
-        assertTrue(mSettings.hasConfiguratorActorPolicy("com.dummy.overlay", 1234));
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingEquivalenceTest.kt b/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingEquivalenceTest.kt
index 5412bb5..74b4d12 100644
--- a/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingEquivalenceTest.kt
+++ b/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingEquivalenceTest.kt
@@ -18,8 +18,8 @@
 
 import android.content.pm.PackageManager
 import android.platform.test.annotations.Presubmit
+import androidx.test.filters.LargeTest
 import com.google.common.truth.Expect
-import com.google.common.truth.Truth.assertWithMessage
 import org.junit.Rule
 import org.junit.Test
 
@@ -52,6 +52,7 @@
         }
     }
 
+    @LargeTest
     @Test
     fun packageInfoEquality() {
         val flags = PackageManager.GET_ACTIVITIES or
@@ -65,7 +66,9 @@
                 PackageManager.GET_SERVICES or
                 PackageManager.GET_SHARED_LIBRARY_FILES or
                 PackageManager.GET_SIGNATURES or
-                PackageManager.GET_SIGNING_CERTIFICATES
+                PackageManager.GET_SIGNING_CERTIFICATES or
+                PackageManager.MATCH_DIRECT_BOOT_UNAWARE or
+                PackageManager.MATCH_DIRECT_BOOT_AWARE
         val oldPackageInfo = oldPackages.asSequence().map { oldPackageInfo(it, flags) }
         val newPackageInfo = newPackages.asSequence().map { newPackageInfo(it, flags) }
 
@@ -77,11 +80,79 @@
             } else {
                 "$firstName | $secondName"
             }
-            expect.withMessage("${it.first?.applicationInfo?.sourceDir} $packageName")
-                    .that(it.first?.dumpToString())
-                    .isEqualTo(it.second?.dumpToString())
+
+            // Main components are asserted independently to separate the failures. Otherwise the
+            // comparison would include every component in one massive string.
+
+            val prefix = "${it.first?.applicationInfo?.sourceDir} $packageName"
+
+            expect.withMessage("$prefix PackageInfo")
+                    .that(it.second?.dumpToString())
+                    .isEqualTo(it.first?.dumpToString())
+
+            expect.withMessage("$prefix ApplicationInfo")
+                    .that(it.second?.applicationInfo?.dumpToString())
+                    .isEqualTo(it.first?.applicationInfo?.dumpToString())
+
+            val firstActivityNames = it.first?.activities?.map { it.name } ?: emptyList()
+            val secondActivityNames = it.second?.activities?.map { it.name } ?: emptyList()
+            expect.withMessage("$prefix activities")
+                    .that(secondActivityNames)
+                    .containsExactlyElementsIn(firstActivityNames)
+                    .inOrder()
+
+            if (!it.first?.activities.isNullOrEmpty() && !it.second?.activities.isNullOrEmpty()) {
+                it.first?.activities?.zip(it.second?.activities!!)?.forEach {
+                    expect.withMessage("$prefix ${it.first.name}")
+                            .that(it.second.dumpToString())
+                            .isEqualTo(it.first.dumpToString())
+                }
+            }
+
+            val firstReceiverNames = it.first?.receivers?.map { it.name } ?: emptyList()
+            val secondReceiverNames = it.second?.receivers?.map { it.name } ?: emptyList()
+            expect.withMessage("$prefix receivers")
+                    .that(secondReceiverNames)
+                    .containsExactlyElementsIn(firstReceiverNames)
+                    .inOrder()
+
+            if (!it.first?.receivers.isNullOrEmpty() && !it.second?.receivers.isNullOrEmpty()) {
+                it.first?.receivers?.zip(it.second?.receivers!!)?.forEach {
+                    expect.withMessage("$prefix ${it.first.name}")
+                            .that(it.second.dumpToString())
+                            .isEqualTo(it.first.dumpToString())
+                }
+            }
+
+            val firstProviderNames = it.first?.providers?.map { it.name } ?: emptyList()
+            val secondProviderNames = it.second?.providers?.map { it.name } ?: emptyList()
+            expect.withMessage("$prefix providers")
+                    .that(secondProviderNames)
+                    .containsExactlyElementsIn(firstProviderNames)
+                    .inOrder()
+
+            if (!it.first?.providers.isNullOrEmpty() && !it.second?.providers.isNullOrEmpty()) {
+                it.first?.providers?.zip(it.second?.providers!!)?.forEach {
+                    expect.withMessage("$prefix ${it.first.name}")
+                            .that(it.second.dumpToString())
+                            .isEqualTo(it.first.dumpToString())
+                }
+            }
+
+            val firstServiceNames = it.first?.services?.map { it.name } ?: emptyList()
+            val secondServiceNames = it.second?.services?.map { it.name } ?: emptyList()
+            expect.withMessage("$prefix services")
+                    .that(secondServiceNames)
+                    .containsExactlyElementsIn(firstServiceNames)
+                    .inOrder()
+
+            if (!it.first?.services.isNullOrEmpty() && !it.second?.services.isNullOrEmpty()) {
+                it.first?.services?.zip(it.second?.services!!)?.forEach {
+                    expect.withMessage("$prefix ${it.first.name}")
+                            .that(it.second.dumpToString())
+                            .isEqualTo(it.first.dumpToString())
+                }
+            }
         }
     }
 }
-
-
diff --git a/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingTestBase.kt b/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingTestBase.kt
index 0f028f0..420ff19 100644
--- a/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingTestBase.kt
+++ b/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingTestBase.kt
@@ -19,6 +19,7 @@
 import android.content.Context
 import android.content.pm.ActivityInfo
 import android.content.pm.ApplicationInfo
+import android.content.pm.ComponentInfo
 import android.content.pm.ConfigurationInfo
 import android.content.pm.FeatureInfo
 import android.content.pm.InstrumentationInfo
@@ -27,6 +28,8 @@
 import android.content.pm.PackageUserState
 import android.content.pm.PermissionInfo
 import android.content.pm.ProviderInfo
+import android.content.pm.ServiceInfo
+import android.os.Bundle
 import android.os.Debug
 import android.os.Environment
 import android.util.SparseArray
@@ -38,8 +41,10 @@
 import com.android.server.testutils.mockThrowOnUnmocked
 import com.android.server.testutils.whenever
 import org.junit.BeforeClass
-import org.mockito.Mockito
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyBoolean
 import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.anyString
 import org.mockito.Mockito.mock
 import java.io.File
 
@@ -47,7 +52,7 @@
 
     companion object {
 
-        private const val VERIFY_ALL_APKS = false
+        private const val VERIFY_ALL_APKS = true
 
         /** For auditing memory usage differences */
         private const val DUMP_HPROF_TO_EXTERNAL = false
@@ -81,10 +86,14 @@
                             .filter { file -> file.name.endsWith(".apk") }
                             .toList()
                 }
+                .distinct()
 
         private val dummyUserState = mock(PackageUserState::class.java).apply {
             installed = true
-            Mockito.`when`(isAvailable(anyInt())).thenReturn(true)
+            whenever(isAvailable(anyInt())) { true }
+            whenever(isMatch(any<ComponentInfo>(), anyInt())) { true }
+            whenever(isMatch(anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(),
+                    anyString(), anyInt())) { true }
         }
 
         lateinit var oldPackages: List<PackageParser.Package>
@@ -145,6 +154,7 @@
         private fun mockPkgSetting(aPkg: AndroidPackage) = mockThrowOnUnmocked<PackageSetting> {
             this.pkg = aPkg
             whenever(pkgState) { PackageStateUnserialized() }
+            whenever(readUserState(anyInt())) { dummyUserState }
         }
     }
 
@@ -156,19 +166,10 @@
     // The following methods prepend "this." because @hide APIs can cause an IDE to auto-import
     // the R.attr constant instead of referencing the field in an attempt to fix the error.
 
-    /**
-     * Known exclusions:
-     *   - [ApplicationInfo.credentialProtectedDataDir]
-     *   - [ApplicationInfo.dataDir]
-     *   - [ApplicationInfo.deviceProtectedDataDir]
-     *   - [ApplicationInfo.processName]
-     *   - [ApplicationInfo.publicSourceDir]
-     *   - [ApplicationInfo.scanPublicSourceDir]
-     *   - [ApplicationInfo.scanSourceDir]
-     *   - [ApplicationInfo.sourceDir]
-     * These attributes used to be assigned post-package-parsing as part of another component,
-     * but are now adjusted directly inside [PackageImpl].
-     */
+    // It's difficult to comment out a line in a triple quoted string, so this is used instead
+    // to ignore specific fields. A comment is required to explain why a field was ignored.
+    private fun Any?.ignored(comment: String): String = "IGNORED"
+
     protected fun ApplicationInfo.dumpToString() = """
             appComponentFactory=${this.appComponentFactory}
             backupAgentName=${this.backupAgentName}
@@ -179,22 +180,31 @@
             compatibleWidthLimitDp=${this.compatibleWidthLimitDp}
             compileSdkVersion=${this.compileSdkVersion}
             compileSdkVersionCodename=${this.compileSdkVersionCodename}
+            credentialProtectedDataDir=${this.credentialProtectedDataDir
+            .ignored("Deferred pre-R, but assigned immediately in R")}
+            crossProfile=${this.crossProfile.ignored("Added in R")}
+            dataDir=${this.dataDir.ignored("Deferred pre-R, but assigned immediately in R")}
             descriptionRes=${this.descriptionRes}
+            deviceProtectedDataDir=${this.deviceProtectedDataDir
+            .ignored("Deferred pre-R, but assigned immediately in R")}
             enabled=${this.enabled}
             enabledSetting=${this.enabledSetting}
             flags=${Integer.toBinaryString(this.flags)}
             fullBackupContent=${this.fullBackupContent}
+            gwpAsanMode=${this.gwpAsanMode.ignored("Added in R")}
             hiddenUntilInstalled=${this.hiddenUntilInstalled}
             icon=${this.icon}
             iconRes=${this.iconRes}
             installLocation=${this.installLocation}
+            labelRes=${this.labelRes}
             largestWidthLimitDp=${this.largestWidthLimitDp}
             logo=${this.logo}
             longVersionCode=${this.longVersionCode}
+            ${"".ignored("mHiddenApiPolicy is a private field")}
             manageSpaceActivityName=${this.manageSpaceActivityName}
-            maxAspectRatio.compareTo(that.maxAspectRatio)=${this.maxAspectRatio}
-            metaData=${this.metaData}
-            minAspectRatio.compareTo(that.minAspectRatio)=${this.minAspectRatio}
+            maxAspectRatio=${this.maxAspectRatio}
+            metaData=${this.metaData.dumpToString()}
+            minAspectRatio=${this.minAspectRatio}
             minSdkVersion=${this.minSdkVersion}
             name=${this.name}
             nativeLibraryDir=${this.nativeLibraryDir}
@@ -206,18 +216,27 @@
             permission=${this.permission}
             primaryCpuAbi=${this.primaryCpuAbi}
             privateFlags=${Integer.toBinaryString(this.privateFlags)}
+            processName=${this.processName.ignored("Deferred pre-R, but assigned immediately in R")}
+            publicSourceDir=${this.publicSourceDir
+            .ignored("Deferred pre-R, but assigned immediately in R")}
             requiresSmallestWidthDp=${this.requiresSmallestWidthDp}
             resourceDirs=${this.resourceDirs?.contentToString()}
             roundIconRes=${this.roundIconRes}
-            secondaryCpuAbi=${this.secondaryCpuAbi}
-            secondaryNativeLibraryDir=${this.secondaryNativeLibraryDir}
+            scanPublicSourceDir=${this.scanPublicSourceDir
+            .ignored("Deferred pre-R, but assigned immediately in R")}
+            scanSourceDir=${this.scanSourceDir
+            .ignored("Deferred pre-R, but assigned immediately in R")}
             seInfo=${this.seInfo}
             seInfoUser=${this.seInfoUser}
+            secondaryCpuAbi=${this.secondaryCpuAbi}
+            secondaryNativeLibraryDir=${this.secondaryNativeLibraryDir}
             sharedLibraryFiles=${this.sharedLibraryFiles?.contentToString()}
             sharedLibraryInfos=${this.sharedLibraryInfos}
             showUserIcon=${this.showUserIcon}
+            sourceDir=${this.sourceDir
+            .ignored("Deferred pre-R, but assigned immediately in R")}
             splitClassLoaderNames=${this.splitClassLoaderNames?.contentToString()}
-            splitDependencies=${this.splitDependencies}
+            splitDependencies=${this.splitDependencies.dumpToString()}
             splitNames=${this.splitNames?.contentToString()}
             splitPublicSourceDirs=${this.splitPublicSourceDirs?.contentToString()}
             splitSourceDirs=${this.splitSourceDirs?.contentToString()}
@@ -226,8 +245,8 @@
             targetSdkVersion=${this.targetSdkVersion}
             taskAffinity=${this.taskAffinity}
             theme=${this.theme}
-            uid=${this.uid}
             uiOptions=${this.uiOptions}
+            uid=${this.uid}
             versionCode=${this.versionCode}
             volumeUuid=${this.volumeUuid}
             zygotePreloadName=${this.zygotePreloadName}
@@ -241,19 +260,27 @@
             """.trimIndent()
 
     protected fun InstrumentationInfo.dumpToString() = """
+            banner=${this.banner}
             credentialProtectedDataDir=${this.credentialProtectedDataDir}
             dataDir=${this.dataDir}
             deviceProtectedDataDir=${this.deviceProtectedDataDir}
             functionalTest=${this.functionalTest}
             handleProfiling=${this.handleProfiling}
+            icon=${this.icon}
+            labelRes=${this.labelRes}
+            logo=${this.logo}
+            metaData=${this.metaData}
+            name=${this.name}
             nativeLibraryDir=${this.nativeLibraryDir}
+            nonLocalizedLabel=${this.nonLocalizedLabel}
+            packageName=${this.packageName}
             primaryCpuAbi=${this.primaryCpuAbi}
             publicSourceDir=${this.publicSourceDir}
             secondaryCpuAbi=${this.secondaryCpuAbi}
             secondaryNativeLibraryDir=${this.secondaryNativeLibraryDir}
+            showUserIcon=${this.showUserIcon}
             sourceDir=${this.sourceDir}
-            splitDependencies=${this.splitDependencies.sequence()
-            .map { it.first to it.second?.contentToString() }.joinToString()}
+            splitDependencies=${this.splitDependencies.dumpToString()}
             splitNames=${this.splitNames?.contentToString()}
             splitPublicSourceDirs=${this.splitPublicSourceDirs?.contentToString()}
             splitSourceDirs=${this.splitSourceDirs?.contentToString()}
@@ -262,25 +289,40 @@
             """.trimIndent()
 
     protected fun ActivityInfo.dumpToString() = """
+            banner=${this.banner}
             colorMode=${this.colorMode}
             configChanges=${this.configChanges}
+            descriptionRes=${this.descriptionRes}
+            directBootAware=${this.directBootAware}
             documentLaunchMode=${this.documentLaunchMode}
+            enabled=${this.enabled}
+            exported=${this.exported}
             flags=${Integer.toBinaryString(this.flags)}
+            icon=${this.icon}
+            labelRes=${this.labelRes}
             launchMode=${this.launchMode}
             launchToken=${this.launchToken}
             lockTaskLaunchMode=${this.lockTaskLaunchMode}
+            logo=${this.logo}
             maxAspectRatio=${this.maxAspectRatio}
             maxRecents=${this.maxRecents}
+            metaData=${this.metaData.dumpToString()}
             minAspectRatio=${this.minAspectRatio}
+            name=${this.name}
+            nonLocalizedLabel=${this.nonLocalizedLabel}
+            packageName=${this.packageName}
             parentActivityName=${this.parentActivityName}
             permission=${this.permission}
-            persistableMode=${this.persistableMode}
-            privateFlags=${Integer.toBinaryString(this.privateFlags)}
+            persistableMode=${this.persistableMode.ignored("Could be dropped pre-R, fixed in R")}
+            privateFlags=${this.privateFlags}
+            processName=${this.processName.ignored("Deferred pre-R, but assigned immediately in R")}
             requestedVrComponent=${this.requestedVrComponent}
             resizeMode=${this.resizeMode}
             rotationAnimation=${this.rotationAnimation}
             screenOrientation=${this.screenOrientation}
+            showUserIcon=${this.showUserIcon}
             softInputMode=${this.softInputMode}
+            splitName=${this.splitName}
             targetActivity=${this.targetActivity}
             taskAffinity=${this.taskAffinity}
             theme=${this.theme}
@@ -300,30 +342,77 @@
 
     protected fun PermissionInfo.dumpToString() = """
             backgroundPermission=${this.backgroundPermission}
+            banner=${this.banner}
             descriptionRes=${this.descriptionRes}
             flags=${Integer.toBinaryString(this.flags)}
             group=${this.group}
+            icon=${this.icon}
+            labelRes=${this.labelRes}
+            logo=${this.logo}
+            metaData=${this.metaData.dumpToString()}
+            name=${this.name}
             nonLocalizedDescription=${this.nonLocalizedDescription}
+            nonLocalizedLabel=${this.nonLocalizedLabel}
+            packageName=${this.packageName}
             protectionLevel=${this.protectionLevel}
             requestRes=${this.requestRes}
+            showUserIcon=${this.showUserIcon}
             """.trimIndent()
 
     protected fun ProviderInfo.dumpToString() = """
+            applicationInfo=${this.applicationInfo.ignored("Already checked")}
             authority=${this.authority}
+            banner=${this.banner}
+            descriptionRes=${this.descriptionRes}
+            directBootAware=${this.directBootAware}
+            enabled=${this.enabled}
+            exported=${this.exported}
             flags=${Integer.toBinaryString(this.flags)}
             forceUriPermissions=${this.forceUriPermissions}
             grantUriPermissions=${this.grantUriPermissions}
+            icon=${this.icon}
             initOrder=${this.initOrder}
             isSyncable=${this.isSyncable}
+            labelRes=${this.labelRes}
+            logo=${this.logo}
+            metaData=${this.metaData.dumpToString()}
             multiprocess=${this.multiprocess}
+            name=${this.name}
+            nonLocalizedLabel=${this.nonLocalizedLabel}
+            packageName=${this.packageName}
             pathPermissions=${this.pathPermissions?.joinToString {
         "readPermission=${it.readPermission}\nwritePermission=${it.writePermission}"
     }}
+            processName=${this.processName.ignored("Deferred pre-R, but assigned immediately in R")}
             readPermission=${this.readPermission}
+            showUserIcon=${this.showUserIcon}
+            splitName=${this.splitName}
             uriPermissionPatterns=${this.uriPermissionPatterns?.contentToString()}
             writePermission=${this.writePermission}
             """.trimIndent()
 
+    protected fun ServiceInfo.dumpToString() = """
+            applicationInfo=${this.applicationInfo.ignored("Already checked")}
+            banner=${this.banner}
+            descriptionRes=${this.descriptionRes}
+            directBootAware=${this.directBootAware}
+            enabled=${this.enabled}
+            exported=${this.exported}
+            flags=${Integer.toBinaryString(this.flags)}
+            icon=${this.icon}
+            labelRes=${this.labelRes}
+            logo=${this.logo}
+            mForegroundServiceType"${this.mForegroundServiceType}
+            metaData=${this.metaData.dumpToString()}
+            name=${this.name}
+            nonLocalizedLabel=${this.nonLocalizedLabel}
+            packageName=${this.packageName}
+            permission=${this.permission}
+            processName=${this.processName.ignored("Deferred pre-R, but assigned immediately in R")}
+            showUserIcon=${this.showUserIcon}
+            splitName=${this.splitName}
+            """.trimIndent()
+
     protected fun ConfigurationInfo.dumpToString() = """
             reqGlEsVersion=${this.reqGlEsVersion}
             reqInputFeatures=${this.reqInputFeatures}
@@ -333,8 +422,10 @@
             """.trimIndent()
 
     protected fun PackageInfo.dumpToString() = """
-            activities=${this.activities?.joinToString { it.dumpToString() }}
-            applicationInfo=${this.applicationInfo.dumpToString()}
+            activities=${this.activities?.joinToString { it.dumpToString() }
+            .ignored("Checked separately in test")}
+            applicationInfo=${this.applicationInfo.dumpToString()
+            .ignored("Checked separately in test")}
             baseRevisionCode=${this.baseRevisionCode}
             compileSdkVersion=${this.compileSdkVersion}
             compileSdkVersionCodename=${this.compileSdkVersionCodename}
@@ -356,15 +447,18 @@
             overlayTarget=${this.overlayTarget}
             packageName=${this.packageName}
             permissions=${this.permissions?.joinToString { it.dumpToString() }}
-            providers=${this.providers?.joinToString { it.dumpToString() }}
-            receivers=${this.receivers?.joinToString { it.dumpToString() }}
+            providers=${this.providers?.joinToString { it.dumpToString() }
+            .ignored("Checked separately in test")}
+            receivers=${this.receivers?.joinToString { it.dumpToString() }
+            .ignored("Checked separately in test")}
             reqFeatures=${this.reqFeatures?.joinToString { it.dumpToString() }}
             requestedPermissions=${this.requestedPermissions?.contentToString()}
             requestedPermissionsFlags=${this.requestedPermissionsFlags?.contentToString()}
             requiredAccountType=${this.requiredAccountType}
             requiredForAllUsers=${this.requiredForAllUsers}
             restrictedAccountType=${this.restrictedAccountType}
-            services=${this.services?.contentToString()}
+            services=${this.services?.joinToString { it.dumpToString() }
+            .ignored("Checked separately in test")}
             sharedUserId=${this.sharedUserId}
             sharedUserLabel=${this.sharedUserLabel}
             signatures=${this.signatures?.joinToString { it.toCharsString() }}
@@ -378,11 +472,17 @@
             versionName=${this.versionName}
             """.trimIndent()
 
-    @Suppress("unused")
-    private fun <T> SparseArray<T>.sequence(): Sequence<Pair<Int, T>> {
-        var index = 0
-        return generateSequence {
-            index++.takeIf { it < size() }?.let { keyAt(it) to valueAt(index) }
+    private fun Bundle?.dumpToString() = this?.keySet()?.associateWith { get(it) }?.toString()
+
+    private fun <T> SparseArray<T>?.dumpToString(): String {
+        if (this == null) {
+            return "EMPTY"
         }
+
+        val list = mutableListOf<Pair<Int, T>>()
+        for (index in (0 until size())) {
+            list += keyAt(index) to valueAt(index)
+        }
+        return list.toString()
     }
 }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/BadgeExtractorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/BadgeExtractorTest.java
index e1f3913..c9c31bf 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/BadgeExtractorTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/BadgeExtractorTest.java
@@ -29,6 +29,10 @@
 import android.app.Notification;
 import android.app.Notification.Builder;
 import android.app.NotificationChannel;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.graphics.drawable.Icon;
+
 import android.os.UserHandle;
 import android.service.notification.StatusBarNotification;
 import android.test.suitebuilder.annotation.SmallTest;
@@ -79,6 +83,37 @@
         return r;
     }
 
+    private NotificationRecord getNotificationRecordWithBubble(boolean suppressNotif) {
+        NotificationChannel channel = new NotificationChannel("a", "a", IMPORTANCE_UNSPECIFIED);
+        channel.setShowBadge(/* showBadge */ true);
+        when(mConfig.getNotificationChannel(mPkg, mUid, "a", false)).thenReturn(channel);
+
+        Notification.BubbleMetadata metadata = new Notification.BubbleMetadata.Builder(
+                PendingIntent.getActivity(mContext, 0, new Intent(), 0),
+                        Icon.createWithResource("", 0)).build();
+
+        int flags = metadata.getFlags();
+        if (suppressNotif) {
+            flags |= Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION;
+        } else {
+            flags &= ~Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION;
+        }
+        metadata.setFlags(flags);
+
+        final Builder builder = new Builder(getContext())
+                .setContentTitle("foo")
+                .setSmallIcon(android.R.drawable.sym_def_app_icon)
+                .setPriority(Notification.PRIORITY_HIGH)
+                .setDefaults(Notification.DEFAULT_SOUND)
+                .setBubbleMetadata(metadata);
+
+        Notification n = builder.build();
+        StatusBarNotification sbn = new StatusBarNotification(mPkg, mPkg, mId, mTag, mUid,
+                mPid, n, mUser, null, System.currentTimeMillis());
+        NotificationRecord r = new NotificationRecord(getContext(), sbn, channel);
+        return r;
+    }
+
     //
     // Tests
     //
@@ -154,6 +189,20 @@
     }
 
     @Test
+    public void testHideNotifOverridesYes() throws Exception {
+        BadgeExtractor extractor = new BadgeExtractor();
+        extractor.setConfig(mConfig);
+
+        when(mConfig.badgingEnabled(mUser)).thenReturn(true);
+        when(mConfig.canShowBadge(mPkg, mUid)).thenReturn(true);
+        NotificationRecord r = getNotificationRecordWithBubble(/* suppressNotif */ true);
+
+        extractor.process(r);
+
+        assertFalse(r.canShowBadge());
+    }
+
+    @Test
     public void testDndOverridesYes() {
         BadgeExtractor extractor = new BadgeExtractor();
         extractor.setConfig(mConfig);
diff --git a/services/tests/wmtests/AndroidManifest.xml b/services/tests/wmtests/AndroidManifest.xml
index f382fba..30df0d4 100644
--- a/services/tests/wmtests/AndroidManifest.xml
+++ b/services/tests/wmtests/AndroidManifest.xml
@@ -62,6 +62,7 @@
                   android:resumeWhilePausing="true"/>
         <activity android:name="com.android.server.wm.ScreenDecorWindowTests$TestActivity"
                   android:showWhenLocked="true" android:allowEmbedded="true"/>
+        <activity android:name="com.android.server.wm.ActivityLeakTests$DetectLeakActivity" />
     </application>
 
     <instrumentation
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityLeakTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityLeakTests.java
new file mode 100644
index 0000000..bd6ac58
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityLeakTests.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static org.junit.Assert.assertFalse;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Debug;
+import android.os.StrictMode;
+import android.os.strictmode.InstanceCountViolation;
+import android.util.Log;
+
+import org.junit.After;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Tests for Activity leaks.
+ *
+ * Build/Install/Run:
+ *     atest WmTests:ActivityLeakTests
+ */
+public class ActivityLeakTests {
+
+    private final Instrumentation mInstrumentation = getInstrumentation();
+    private final Context mContext = mInstrumentation.getTargetContext();
+    private final List<Activity> mStartedActivityList = new ArrayList<>();
+
+    @After
+    public void tearDown() {
+        mInstrumentation.runOnMainSync(() -> {
+            // Reset strict mode.
+            StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().build());
+        });
+        for (Activity activity : mStartedActivityList) {
+            if (!activity.isDestroyed()) {
+                activity.finish();
+            }
+        }
+        mStartedActivityList.clear();
+    }
+
+    @Test
+    public void testActivityLeak() {
+        final Bundle intentExtras = new Bundle();
+        intentExtras.putBoolean(DetectLeakActivity.ENABLE_STRICT_MODE, true);
+        final DetectLeakActivity activity = (DetectLeakActivity) startActivity(
+                DetectLeakActivity.class, 0 /* flags */, intentExtras);
+        mStartedActivityList.add(activity);
+
+        activity.finish();
+
+        assertFalse("Leak found on activity", activity.isLeakedAfterDestroy());
+    }
+
+    @Test
+    public void testActivityLeakForTwoInstances() {
+        final Bundle intentExtras = new Bundle();
+
+        // Launch an activity, then enable strict mode
+        intentExtras.putBoolean(DetectLeakActivity.ENABLE_STRICT_MODE, true);
+        final DetectLeakActivity activity1 = (DetectLeakActivity) startActivity(
+                DetectLeakActivity.class, 0 /* flags */, intentExtras);
+        mStartedActivityList.add(activity1);
+
+        // Launch second activity instance.
+        intentExtras.putBoolean(DetectLeakActivity.ENABLE_STRICT_MODE, false);
+        final DetectLeakActivity activity2 = (DetectLeakActivity) startActivity(
+                DetectLeakActivity.class,
+                FLAG_ACTIVITY_MULTIPLE_TASK | FLAG_ACTIVITY_NEW_DOCUMENT, intentExtras);
+        mStartedActivityList.add(activity2);
+
+        // Destroy the activity
+        activity1.finish();
+        assertFalse("Leak found on activity 1", activity1.isLeakedAfterDestroy());
+
+        activity2.finish();
+        assertFalse("Leak found on activity 2", activity2.isLeakedAfterDestroy());
+    }
+
+    private Activity startActivity(Class<?> cls, int flags, Bundle extras) {
+        final Intent intent = new Intent(mContext, cls);
+        intent.addFlags(flags | FLAG_ACTIVITY_NEW_TASK);
+        if (extras != null) {
+            intent.putExtras(extras);
+        }
+        return mInstrumentation.startActivitySync(intent);
+    }
+
+    public static class DetectLeakActivity extends Activity {
+
+        private static final String TAG = "DetectLeakActivity";
+
+        public static final String ENABLE_STRICT_MODE = "enable_strict_mode";
+
+        private volatile boolean mWasDestroyed;
+        private volatile boolean mIsLeaked;
+
+        @Override
+        protected void onCreate(Bundle savedInstanceState) {
+            super.onCreate(savedInstanceState);
+            if (getIntent().getBooleanExtra(ENABLE_STRICT_MODE, false)) {
+                enableStrictMode();
+            }
+        }
+
+        @Override
+        protected void onDestroy() {
+            super.onDestroy();
+            getWindow().getDecorView().post(() -> {
+                synchronized (this) {
+                    mWasDestroyed = true;
+                    notifyAll();
+                }
+            });
+        }
+
+        public boolean isLeakedAfterDestroy() {
+            synchronized (this) {
+                while (!mWasDestroyed && !mIsLeaked) {
+                    try {
+                        wait(5000 /* timeoutMs */);
+                    } catch (InterruptedException ignored) {
+                    }
+                }
+            }
+            return mIsLeaked;
+        }
+
+        private void enableStrictMode() {
+            StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
+                    .detectActivityLeaks()
+                    .penaltyLog()
+                    .penaltyListener(Runnable::run, violation -> {
+                        if (!(violation instanceof InstanceCountViolation)) {
+                            return;
+                        }
+                        synchronized (this) {
+                            mIsLeaked = true;
+                            notifyAll();
+                        }
+                        Log.w(TAG, violation.toString() + ", " + dumpHprofData());
+                    })
+                    .build());
+        }
+
+        private String dumpHprofData() {
+            try {
+                final String fileName = getDataDir().getPath() + "/ActivityLeakHeapDump.hprof";
+                Debug.dumpHprofData(fileName);
+                return "memory dump filename: " + fileName;
+            } catch (Throwable e) {
+                Log.e(TAG, "dumpHprofData failed", e);
+                return "failed to save memory dump";
+            }
+        }
+    }
+}
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
index cf07221..9621f68 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
@@ -73,6 +73,7 @@
 import android.util.Log;
 import android.util.Slog;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.app.IVoiceActionCheckCallback;
 import com.android.internal.app.IVoiceInteractionManagerService;
 import com.android.internal.app.IVoiceInteractionSessionListener;
@@ -230,6 +231,10 @@
         private int mCurUser;
         private boolean mCurUserUnlocked;
         private boolean mCurUserSupported;
+
+        @GuardedBy("this")
+        private boolean mTemporarilyDisabled;
+
         private final boolean mEnableService;
 
         VoiceInteractionManagerServiceStub() {
@@ -316,8 +321,12 @@
                     Settings.Secure.VOICE_INTERACTION_SERVICE, userHandle);
             ComponentName curRecognizer = getCurRecognizer(userHandle);
             VoiceInteractionServiceInfo curInteractorInfo = null;
-            if (DEBUG) Slog.d(TAG, "curInteractorStr=" + curInteractorStr
-                    + " curRecognizer=" + curRecognizer);
+            if (DEBUG) {
+                Slog.d(TAG, "curInteractorStr=" + curInteractorStr
+                        + " curRecognizer=" + curRecognizer
+                        + " mEnableService=" + mEnableService
+                        + " mTemporarilyDisabled=" + mTemporarilyDisabled);
+            }
             if (curInteractorStr == null && curRecognizer != null && mEnableService) {
                 // If there is no interactor setting, that means we are upgrading
                 // from an older platform version.  If the current recognizer is not
@@ -472,10 +481,11 @@
         }
 
         void switchImplementationIfNeededLocked(boolean force) {
-            if (!mCurUserSupported) {
+            if (!mCurUserSupported || mTemporarilyDisabled) {
                 if (DEBUG_USER) {
-                    Slog.d(TAG, "switchImplementationIfNeeded(): skipping on unsuported user "
-                            + mCurUser);
+                    Slog.d(TAG, "switchImplementationIfNeeded(): skipping: force= " + force
+                            + "mCurUserSupported=" + mCurUserSupported
+                            + "mTemporarilyDisabled=" + mTemporarilyDisabled);
                 }
                 if (mImpl != null) {
                     mImpl.shutdownLocked();
@@ -928,6 +938,25 @@
             }
         }
 
+        @Override
+        public void setDisabled(boolean disabled) {
+            enforceCallingPermission(Manifest.permission.ACCESS_VOICE_INTERACTION_SERVICE);
+            synchronized (this) {
+                if (mTemporarilyDisabled == disabled) {
+                    if (DEBUG) Slog.d(TAG, "setDisabled(): already " + disabled);
+                    return;
+                }
+                Slog.i(TAG, "setDisabled(): changing to " + disabled);
+                final long caller = Binder.clearCallingIdentity();
+                try {
+                    mTemporarilyDisabled = disabled;
+                    switchImplementationIfNeeded(/* force= */ false);
+                } finally {
+                    Binder.restoreCallingIdentity(caller);
+                }
+            }
+        }
+
         //----------------- Model management APIs --------------------------------//
 
         @Override
@@ -1378,6 +1407,7 @@
             synchronized (this) {
                 pw.println("VOICE INTERACTION MANAGER (dumpsys voiceinteraction)");
                 pw.println("  mEnableService: " + mEnableService);
+                pw.println("  mTemporarilyDisabled: " + mTemporarilyDisabled);
                 pw.println("  mCurUser: " + mCurUser);
                 pw.println("  mCurUserUnlocked: " + mCurUserUnlocked);
                 pw.println("  mCurUserSupported: " + mCurUserSupported);
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceShellCommand.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceShellCommand.java
index 3f4ddb6..6c355a3 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceShellCommand.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceShellCommand.java
@@ -52,6 +52,8 @@
                 return requestShow(pw);
             case "hide":
                 return requestHide(pw);
+            case "disable":
+                return requestDisable(pw);
             default:
                 return handleDefaultCommands(cmd);
         }
@@ -69,6 +71,8 @@
             pw.println("");
             pw.println("  hide");
             pw.println("    Hides the current session");
+            pw.println("  disable [true|false]");
+            pw.println("    Temporarily disable (when true) service");
             pw.println("");
         }
     }
@@ -127,6 +131,17 @@
         return 0;
     }
 
+    private int requestDisable(PrintWriter pw) {
+        boolean disabled = Boolean.parseBoolean(getNextArgRequired());
+        Slog.i(TAG, "requestDisable(): " + disabled);
+        try {
+            mService.setDisabled(disabled);
+        } catch (Exception e) {
+            return handleError(pw, "requestDisable()", e);
+        }
+        return 0;
+    }
+
     private static int handleError(PrintWriter pw, String message, Exception e) {
         Slog.e(TAG,  "error calling " + message, e);
         pw.printf("Error calling %s: %s\n", message, e);
diff --git a/tests/RollbackTest/MultiUserRollbackTest.xml b/tests/RollbackTest/MultiUserRollbackTest.xml
index ba86c3f..2f62af1 100644
--- a/tests/RollbackTest/MultiUserRollbackTest.xml
+++ b/tests/RollbackTest/MultiUserRollbackTest.xml
@@ -15,6 +15,12 @@
 -->
 <configuration description="Runs rollback tests for multiple users">
     <option name="test-suite-tag" value="MultiUserRollbackTest" />
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <option name="run-command" value="pm uninstall com.android.cts.install.lib.testapp.A" />
+        <option name="run-command" value="pm uninstall com.android.cts.install.lib.testapp.B" />
+        <option name="teardown-command" value="pm uninstall com.android.cts.install.lib.testapp.A" />
+        <option name="teardown-command" value="pm uninstall com.android.cts.install.lib.testapp.B" />
+    </target_preparer>
     <test class="com.android.tradefed.testtype.HostTest" >
         <option name="class" value="com.android.tests.rollback.host.MultiUserRollbackTest" />
     </test>
diff --git a/tests/RollbackTest/MultiUserRollbackTest/src/com/android/tests/rollback/host/MultiUserRollbackTest.java b/tests/RollbackTest/MultiUserRollbackTest/src/com/android/tests/rollback/host/MultiUserRollbackTest.java
index a4c81d5..42b886f 100644
--- a/tests/RollbackTest/MultiUserRollbackTest/src/com/android/tests/rollback/host/MultiUserRollbackTest.java
+++ b/tests/RollbackTest/MultiUserRollbackTest/src/com/android/tests/rollback/host/MultiUserRollbackTest.java
@@ -40,23 +40,18 @@
     private static final long SWITCH_USER_COMPLETED_NUMBER_OF_POLLS = 60;
     private static final long SWITCH_USER_COMPLETED_POLL_INTERVAL_IN_MILLIS = 1000;
 
-    private void cleanUp() throws Exception {
-        getDevice().executeShellCommand("pm rollback-app com.android.cts.install.lib.testapp.A");
-        getDevice().executeShellCommand("pm uninstall com.android.cts.install.lib.testapp.A");
-    }
-
     @After
     public void tearDown() throws Exception {
-        cleanUp();
         removeSecondaryUserIfNecessary();
+        runPhaseForUsers("cleanUp", mOriginalUserId);
     }
 
     @Before
     public void setup() throws Exception {
-        cleanUp();
         mOriginalUserId = getDevice().getCurrentUser();
         createAndStartSecondaryUser();
         installPackage("RollbackTest.apk", "--user all");
+        runPhaseForUsers("cleanUp", mOriginalUserId);
     }
 
     @Test
diff --git a/tests/RollbackTest/NetworkStagedRollbackTest/src/com/android/tests/rollback/host/NetworkStagedRollbackTest.java b/tests/RollbackTest/NetworkStagedRollbackTest/src/com/android/tests/rollback/host/NetworkStagedRollbackTest.java
index 57c52f9..61d7c76 100644
--- a/tests/RollbackTest/NetworkStagedRollbackTest/src/com/android/tests/rollback/host/NetworkStagedRollbackTest.java
+++ b/tests/RollbackTest/NetworkStagedRollbackTest/src/com/android/tests/rollback/host/NetworkStagedRollbackTest.java
@@ -59,12 +59,14 @@
 
     @Before
     public void setUp() throws Exception {
+        runPhase("cleanUp");
         mLogger.start(getDevice());
     }
 
     @After
     public void tearDown() throws Exception {
         mLogger.stop();
+        runPhase("cleanUp");
     }
 
     /**
diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/MultiUserRollbackTest.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/MultiUserRollbackTest.java
index 400bb04..8641f4d 100644
--- a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/MultiUserRollbackTest.java
+++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/MultiUserRollbackTest.java
@@ -22,6 +22,7 @@
 
 import android.Manifest;
 import android.content.rollback.RollbackInfo;
+import android.content.rollback.RollbackManager;
 
 import com.android.cts.install.lib.Install;
 import com.android.cts.install.lib.InstallUtils;
@@ -54,6 +55,17 @@
     }
 
     @Test
+    public void cleanUp() {
+        RollbackManager rm = RollbackUtils.getRollbackManager();
+        rm.getAvailableRollbacks().stream().flatMap(info -> info.getPackages().stream())
+                .map(info -> info.getPackageName()).forEach(rm::expireRollbackForPackage);
+        rm.getRecentlyCommittedRollbacks().stream().flatMap(info -> info.getPackages().stream())
+                .map(info -> info.getPackageName()).forEach(rm::expireRollbackForPackage);
+        assertThat(rm.getAvailableRollbacks()).isEmpty();
+        assertThat(rm.getRecentlyCommittedRollbacks()).isEmpty();
+    }
+
+    @Test
     public void testBasic() throws Exception {
         new RollbackTest().testBasic();
     }
diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/NetworkStagedRollbackTest.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/NetworkStagedRollbackTest.java
index 8fb59c7..42b0c60 100644
--- a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/NetworkStagedRollbackTest.java
+++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/NetworkStagedRollbackTest.java
@@ -19,6 +19,8 @@
 import static com.android.cts.rollback.lib.RollbackInfoSubject.assertThat;
 import static com.android.cts.rollback.lib.RollbackUtils.getUniqueRollbackInfoForPackage;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import android.Manifest;
 import android.content.ComponentName;
 import android.content.Intent;
@@ -91,14 +93,23 @@
     }
 
     @Test
+    public void cleanUp() {
+        RollbackManager rm = RollbackUtils.getRollbackManager();
+        rm.getAvailableRollbacks().stream().flatMap(info -> info.getPackages().stream())
+                .map(info -> info.getPackageName()).forEach(rm::expireRollbackForPackage);
+        rm.getRecentlyCommittedRollbacks().stream().flatMap(info -> info.getPackages().stream())
+                .map(info -> info.getPackageName()).forEach(rm::expireRollbackForPackage);
+        assertThat(rm.getAvailableRollbacks()).isEmpty();
+        assertThat(rm.getRecentlyCommittedRollbacks()).isEmpty();
+        uninstallNetworkStackPackage();
+    }
+
+    @Test
     public void testNetworkFailedRollback_Phase1() throws Exception {
         // Remove available rollbacks and uninstall NetworkStack on /data/
         RollbackManager rm = RollbackUtils.getRollbackManager();
         String networkStack = getNetworkStackPackageName();
 
-        rm.expireRollbackForPackage(networkStack);
-        uninstallNetworkStackPackage();
-
         assertThat(getUniqueRollbackInfoForPackage(rm.getAvailableRollbacks(),
                 networkStack)).isNull();
 
@@ -153,9 +164,6 @@
         RollbackManager rm = RollbackUtils.getRollbackManager();
         String networkStack = getNetworkStackPackageName();
 
-        rm.expireRollbackForPackage(networkStack);
-        uninstallNetworkStackPackage();
-
         assertThat(getUniqueRollbackInfoForPackage(rm.getAvailableRollbacks(),
                 networkStack)).isNull();
 
diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java
index 48b5bed..dd08771 100644
--- a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java
+++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/RollbackTest.java
@@ -48,6 +48,8 @@
 import com.android.cts.rollback.lib.RollbackBroadcastReceiver;
 import com.android.cts.rollback.lib.RollbackUtils;
 
+import org.junit.After;
+import org.junit.Before;
 import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -81,6 +83,21 @@
                         pri -> packageName.equals(pri.getPackageName())));
     }
 
+    @Before
+    @After
+    public void cleanUp() {
+        try {
+            InstallUtils.adoptShellPermissionIdentity(Manifest.permission.TEST_MANAGE_ROLLBACKS);
+            RollbackManager rm = RollbackUtils.getRollbackManager();
+            rm.getAvailableRollbacks().stream().flatMap(info -> info.getPackages().stream())
+                    .map(info -> info.getPackageName()).forEach(rm::expireRollbackForPackage);
+            rm.getRecentlyCommittedRollbacks().stream().flatMap(info -> info.getPackages().stream())
+                    .map(info -> info.getPackageName()).forEach(rm::expireRollbackForPackage);
+        } finally {
+            InstallUtils.dropShellPermissionIdentity();
+        }
+    }
+
     /**
      * Test basic rollbacks.
      */
diff --git a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java
index 6c9ffe2..00bd4cf 100644
--- a/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java
+++ b/tests/RollbackTest/RollbackTest/src/com/android/tests/rollback/StagedRollbackTest.java
@@ -360,7 +360,10 @@
         RollbackManager rm = RollbackUtils.getRollbackManager();
         rm.getAvailableRollbacks().stream().flatMap(info -> info.getPackages().stream())
                 .map(info -> info.getPackageName()).forEach(rm::expireRollbackForPackage);
-        assertThat(RollbackUtils.getRollbackManager().getAvailableRollbacks()).isEmpty();
+        rm.getRecentlyCommittedRollbacks().stream().flatMap(info -> info.getPackages().stream())
+                .map(info -> info.getPackageName()).forEach(rm::expireRollbackForPackage);
+        assertThat(rm.getAvailableRollbacks()).isEmpty();
+        assertThat(rm.getRecentlyCommittedRollbacks()).isEmpty();
     }
 
     private static final String APK_IN_APEX_TESTAPEX_NAME = "com.android.apex.apkrollback.test";
diff --git a/tests/RollbackTest/StagedRollbackTest.xml b/tests/RollbackTest/StagedRollbackTest.xml
index 2750d37..83fef8e 100644
--- a/tests/RollbackTest/StagedRollbackTest.xml
+++ b/tests/RollbackTest/StagedRollbackTest.xml
@@ -19,6 +19,12 @@
         <option name="cleanup-apks" value="true" />
         <option name="test-file-name" value="RollbackTest.apk" />
     </target_preparer>
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <option name="run-command" value="pm uninstall com.android.cts.install.lib.testapp.A" />
+        <option name="run-command" value="pm uninstall com.android.cts.install.lib.testapp.B" />
+        <option name="teardown-command" value="pm uninstall com.android.cts.install.lib.testapp.A" />
+        <option name="teardown-command" value="pm uninstall com.android.cts.install.lib.testapp.B" />
+    </target_preparer>
     <test class="com.android.tradefed.testtype.HostTest" >
         <option name="class" value="com.android.tests.rollback.host.StagedRollbackTest" />
     </test>
diff --git a/tools/aapt2/Debug.cpp b/tools/aapt2/Debug.cpp
index 1eb7d95..439f231 100644
--- a/tools/aapt2/Debug.cpp
+++ b/tools/aapt2/Debug.cpp
@@ -252,29 +252,6 @@
   Printer* printer_;
 };
 
-std::string OverlayablePoliciesToString(PolicyFlags policies) {
-  std::string str;
-
-  uint32_t remaining = policies;
-  for (auto const& policy : kPolicyStringToFlag) {
-    if ((policies & policy.second) != policy.second) {
-      continue;
-    }
-    if (!str.empty()) {
-      str.append("|");
-    }
-    str.append(policy.first.data());
-    remaining &= ~policy.second;
-  }
-  if (remaining != 0) {
-    if (!str.empty()) {
-      str.append("|");
-    }
-    str.append(StringPrintf("0x%08x", remaining));
-  }
-  return !str.empty() ? str : "none";
-}
-
 }  // namespace
 
 void Debug::PrintTable(const ResourceTable& table, const DebugPrintTableOptions& options,
@@ -575,7 +552,7 @@
               overlayable_item.overlayable->name.c_str(),
               overlayable_item.overlayable->actor.c_str());
           const auto policy_subsection = StringPrintf(R"(policies="%s")",
-              OverlayablePoliciesToString(overlayable_item.policies).c_str());
+              android::idmap2::policy::PoliciesToDebugString(overlayable_item.policies).c_str());
           const auto value =
             StringPrintf("%s/%s", to_string(type->type).data(), entry->name.c_str());
           items.push_back(DumpOverlayableEntry{overlayable_section, policy_subsection, value});