Merge "Helper API for: QAS/suspension precedence validation." into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 16389b3..941d842 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -755,6 +755,13 @@
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
 
+java_aconfig_library {
+    name: "android.hardware.usb.flags-aconfig-java-host",
+    aconfig_declarations: "android.hardware.usb.flags-aconfig",
+    host_supported: true,
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
 // WindowingTools
 aconfig_declarations {
     name: "android.tracing.flags-aconfig",
diff --git a/apct-tests/perftests/windowmanager/src/android/wm/InternalWindowOperationPerfTest.java b/apct-tests/perftests/windowmanager/src/android/wm/InternalWindowOperationPerfTest.java
index 3a11417..d3b4f23 100644
--- a/apct-tests/perftests/windowmanager/src/android/wm/InternalWindowOperationPerfTest.java
+++ b/apct-tests/perftests/windowmanager/src/android/wm/InternalWindowOperationPerfTest.java
@@ -52,9 +52,8 @@
     private final TraceMarkParser mTraceMarkParser = new TraceMarkParser(
             "applyPostLayoutPolicy",
             "applySurfaceChanges",
-            "AppTransitionReady",
-            "closeSurfaceTransaction",
-            "openSurfaceTransaction",
+            "onTransactionReady",
+            "applyTransaction",
             "performLayout",
             "performSurfacePlacement",
             "prepareSurfaces",
diff --git a/cmds/uinput/jni/com_android_commands_uinput_Device.cpp b/cmds/uinput/jni/com_android_commands_uinput_Device.cpp
index ec2b1f4..a78a465 100644
--- a/cmds/uinput/jni/com_android_commands_uinput_Device.cpp
+++ b/cmds/uinput/jni/com_android_commands_uinput_Device.cpp
@@ -96,9 +96,9 @@
     return env;
 }
 
-std::unique_ptr<UinputDevice> UinputDevice::open(int32_t id, const char* name, int32_t vid,
-                                                 int32_t pid, uint16_t bus, uint32_t ffEffectsMax,
-                                                 const char* port,
+std::unique_ptr<UinputDevice> UinputDevice::open(int32_t id, const char* name, int32_t vendorId,
+                                                 int32_t productId, int32_t versionId, uint16_t bus,
+                                                 uint32_t ffEffectsMax, const char* port,
                                                  std::unique_ptr<DeviceCallback> callback) {
     android::base::unique_fd fd(::open(UINPUT_PATH, O_RDWR | O_NONBLOCK | O_CLOEXEC));
     if (!fd.ok()) {
@@ -118,8 +118,9 @@
     strlcpy(setupDescriptor.name, name, UINPUT_MAX_NAME_SIZE);
     setupDescriptor.id.version = 1;
     setupDescriptor.id.bustype = bus;
-    setupDescriptor.id.vendor = vid;
-    setupDescriptor.id.product = pid;
+    setupDescriptor.id.vendor = vendorId;
+    setupDescriptor.id.product = productId;
+    setupDescriptor.id.version = versionId;
     setupDescriptor.ff_effects_max = ffEffectsMax;
 
     // Request device configuration.
@@ -242,9 +243,9 @@
     return data;
 }
 
-static jlong openUinputDevice(JNIEnv* env, jclass /* clazz */, jstring rawName, jint id, jint vid,
-                              jint pid, jint bus, jint ffEffectsMax, jstring rawPort,
-                              jobject callback) {
+static jlong openUinputDevice(JNIEnv* env, jclass /* clazz */, jstring rawName, jint id,
+                              jint vendorId, jint productId, jint versionId, jint bus,
+                              jint ffEffectsMax, jstring rawPort, jobject callback) {
     ScopedUtfChars name(env, rawName);
     if (name.c_str() == nullptr) {
         return 0;
@@ -255,8 +256,8 @@
             std::make_unique<uinput::DeviceCallback>(env, callback);
 
     std::unique_ptr<uinput::UinputDevice> d =
-            uinput::UinputDevice::open(id, name.c_str(), vid, pid, bus, ffEffectsMax, port.c_str(),
-                                       std::move(cb));
+            uinput::UinputDevice::open(id, name.c_str(), vendorId, productId, versionId, bus,
+                                       ffEffectsMax, port.c_str(), std::move(cb));
     return reinterpret_cast<jlong>(d.release());
 }
 
@@ -326,7 +327,7 @@
 
 static JNINativeMethod sMethods[] = {
         {"nativeOpenUinputDevice",
-         "(Ljava/lang/String;IIIIILjava/lang/String;"
+         "(Ljava/lang/String;IIIIIILjava/lang/String;"
          "Lcom/android/commands/uinput/Device$DeviceCallback;)J",
          reinterpret_cast<void*>(openUinputDevice)},
         {"nativeInjectEvent", "(JIII)V", reinterpret_cast<void*>(injectEvent)},
diff --git a/cmds/uinput/jni/com_android_commands_uinput_Device.h b/cmds/uinput/jni/com_android_commands_uinput_Device.h
index 6da3d79..9769a75 100644
--- a/cmds/uinput/jni/com_android_commands_uinput_Device.h
+++ b/cmds/uinput/jni/com_android_commands_uinput_Device.h
@@ -46,9 +46,9 @@
 
 class UinputDevice {
 public:
-    static std::unique_ptr<UinputDevice> open(int32_t id, const char* name, int32_t vid,
-                                              int32_t pid, uint16_t bus, uint32_t ff_effects_max,
-                                              const char* port,
+    static std::unique_ptr<UinputDevice> open(int32_t id, const char* name, int32_t vendorId,
+                                              int32_t productId, int32_t versionId, uint16_t bus,
+                                              uint32_t ff_effects_max, const char* port,
                                               std::unique_ptr<DeviceCallback> callback);
 
     virtual ~UinputDevice();
diff --git a/cmds/uinput/src/com/android/commands/uinput/Device.java b/cmds/uinput/src/com/android/commands/uinput/Device.java
index b0fa34c..787055c 100644
--- a/cmds/uinput/src/com/android/commands/uinput/Device.java
+++ b/cmds/uinput/src/com/android/commands/uinput/Device.java
@@ -61,8 +61,9 @@
         System.loadLibrary("uinputcommand_jni");
     }
 
-    private static native long nativeOpenUinputDevice(String name, int id, int vid, int pid,
-            int bus, int ffEffectsMax, String port, DeviceCallback callback);
+    private static native long nativeOpenUinputDevice(String name, int id, int vendorId,
+            int productId, int versionId, int bus, int ffEffectsMax, String port,
+            DeviceCallback callback);
     private static native void nativeCloseUinputDevice(long ptr);
     private static native void nativeInjectEvent(long ptr, int type, int code, int value);
     private static native void nativeConfigure(int handle, int code, int[] configs);
@@ -71,7 +72,7 @@
     private static native int nativeGetEvdevEventCodeByLabel(int type, String label);
     private static native int nativeGetEvdevInputPropByLabel(String label);
 
-    public Device(int id, String name, int vid, int pid, int bus,
+    public Device(int id, String name, int vendorId, int productId, int versionId, int bus,
             SparseArray<int[]> configuration, int ffEffectsMax,
             SparseArray<InputAbsInfo> absInfo, String port) {
         mId = id;
@@ -83,19 +84,20 @@
         mOutputStream = System.out;
         SomeArgs args = SomeArgs.obtain();
         args.argi1 = id;
-        args.argi2 = vid;
-        args.argi3 = pid;
-        args.argi4 = bus;
-        args.argi5 = ffEffectsMax;
+        args.argi2 = vendorId;
+        args.argi3 = productId;
+        args.argi4 = versionId;
+        args.argi5 = bus;
+        args.argi6 = ffEffectsMax;
         if (name != null) {
             args.arg1 = name;
         } else {
-            args.arg1 = id + ":" + vid + ":" + pid;
+            args.arg1 = id + ":" + vendorId + ":" + productId;
         }
         if (port != null) {
             args.arg2 = port;
         } else {
-            args.arg2 = "uinput:" + id + ":" + vid + ":" + pid;
+            args.arg2 = "uinput:" + id + ":" + vendorId + ":" + productId;
         }
 
         mHandler.obtainMessage(MSG_OPEN_UINPUT_DEVICE, args).sendToTarget();
@@ -161,8 +163,10 @@
                 case MSG_OPEN_UINPUT_DEVICE:
                     SomeArgs args = (SomeArgs) msg.obj;
                     String name = (String) args.arg1;
-                    mPtr = nativeOpenUinputDevice(name, args.argi1, args.argi2,
-                            args.argi3, args.argi4, args.argi5, (String) args.arg2,
+                    mPtr = nativeOpenUinputDevice(name, args.argi1 /* id */,
+                            args.argi2 /* vendorId */, args.argi3 /* productId */,
+                            args.argi4 /* versionId */, args.argi5 /* bus */,
+                            args.argi6 /* ffEffectsMax */, (String) args.arg2 /* port */,
                             new DeviceCallback());
                     if (mPtr == 0) {
                         RuntimeException ex = new RuntimeException(
diff --git a/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java b/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java
index b89e2cd..7652f24 100644
--- a/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java
+++ b/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java
@@ -19,8 +19,8 @@
 import android.annotation.Nullable;
 import android.util.SparseArray;
 
-import java.io.BufferedReader;
 import java.io.IOException;
+import java.io.LineNumberReader;
 import java.io.Reader;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
@@ -47,10 +47,11 @@
     private static final int REGISTRATION_DELAY_MILLIS = 500;
 
     private static class CommentAwareReader {
-        private final BufferedReader mReader;
+        private final LineNumberReader mReader;
+        private String mPreviousLine;
         private String mNextLine;
 
-        CommentAwareReader(BufferedReader in) throws IOException {
+        CommentAwareReader(LineNumberReader in) throws IOException {
             mReader = in;
             mNextLine = findNextLine();
         }
@@ -90,12 +91,46 @@
 
         /** Moves to the next line of the file. */
         public void advance() throws IOException {
+            mPreviousLine = mNextLine;
             mNextLine = findNextLine();
         }
 
         public boolean isAtEndOfFile() {
             return mNextLine == null;
         }
+
+        /** Returns the previous line, for error messages. */
+        public String getPreviousLine() {
+            return mPreviousLine;
+        }
+
+        /** Returns the number of the <b>previous</b> line. */
+        public int getPreviousLineNumber() {
+            return mReader.getLineNumber() - 1;
+        }
+    }
+
+    public static class ParsingException extends RuntimeException {
+        private final int mLineNumber;
+        private final String mLine;
+
+        ParsingException(String message, CommentAwareReader reader) {
+            this(message, reader.getPreviousLine(), reader.getPreviousLineNumber());
+        }
+
+        ParsingException(String message, String line, int lineNumber) {
+            super(message);
+            mLineNumber = lineNumber;
+            mLine = line;
+        }
+
+        /** Returns a nicely formatted error message, including the line number and line. */
+        public String makeErrorMessage() {
+            return String.format("""
+                    Parsing error on line %d: %s
+                    --> %s
+                    """, mLineNumber, getMessage(), mLine);
+        }
     }
 
     private final CommentAwareReader mReader;
@@ -107,7 +142,7 @@
     private final Queue<Event> mQueuedEvents = new ArrayDeque<>(2);
 
     public EvemuParser(Reader in) throws IOException {
-        mReader = new CommentAwareReader(new BufferedReader(in));
+        mReader = new CommentAwareReader(new LineNumberReader(in));
         mQueuedEvents.add(parseRegistrationEvent());
 
         // The kernel takes a little time to set up an evdev device after the initial
@@ -133,20 +168,22 @@
             return null;
         }
 
-        final String[] parts = expectLineWithParts("E", 4);
+        final String line = expectLine("E");
+        final String[] parts = expectParts(line, 4);
         final String[] timeParts = parts[0].split("\\.");
         if (timeParts.length != 2) {
-            throw new RuntimeException("Invalid timestamp (does not contain a '.')");
+            throw new ParsingException(
+                    "Invalid timestamp '" + parts[0] + "' (should contain a single '.')", mReader);
         }
         // TODO(b/310958309): use timeMicros to set the timestamp on the event being sent.
         final long timeMicros =
-                Long.parseLong(timeParts[0]) * 1_000_000 + Integer.parseInt(timeParts[1]);
+                parseLong(timeParts[0], 10) * 1_000_000 + parseInt(timeParts[1], 10);
         final Event.Builder eb = new Event.Builder();
         eb.setId(DEVICE_ID);
         eb.setCommand(Event.Command.INJECT);
-        final int eventType = Integer.parseInt(parts[1], 16);
-        final int eventCode = Integer.parseInt(parts[2], 16);
-        final int value = Integer.parseInt(parts[3]);
+        final int eventType = parseInt(parts[1], 16);
+        final int eventCode = parseInt(parts[2], 16);
+        final int value = parseInt(parts[3], 10);
         eb.setInjections(new int[] {eventType, eventCode, value});
 
         if (mLastEventTimeMicros == -1) {
@@ -184,11 +221,12 @@
         eb.setCommand(Event.Command.REGISTER);
         eb.setName(expectLine("N"));
 
-        final String[] idStrings = expectLineWithParts("I", 4);
-        eb.setBusId(Integer.parseInt(idStrings[0], 16));
-        eb.setVid(Integer.parseInt(idStrings[1], 16));
-        eb.setPid(Integer.parseInt(idStrings[2], 16));
-        // TODO(b/302297266): support setting the version ID, and set it to idStrings[3].
+        final String idsLine = expectLine("I");
+        final String[] idStrings = expectParts(idsLine, 4);
+        eb.setBusId(parseInt(idStrings[0], 16));
+        eb.setVendorId(parseInt(idStrings[1], 16));
+        eb.setProductId(parseInt(idStrings[2], 16));
+        eb.setVersionId(parseInt(idStrings[3], 16));
 
         final SparseArray<int[]> config = new SparseArray<>();
         config.append(Event.UinputControlCode.UI_SET_PROPBIT.getValue(), parseProperties());
@@ -215,33 +253,39 @@
     }
 
     private int[] parseProperties() throws IOException {
-        final List<String> propBitmapParts = new ArrayList<>();
+        final ArrayList<Integer> propBitmapParts = new ArrayList<>();
         String line = acceptLine("P");
         while (line != null) {
-            propBitmapParts.addAll(List.of(line.strip().split(" ")));
+            String[] parts = line.strip().split(" ");
+            propBitmapParts.ensureCapacity(propBitmapParts.size() + parts.length);
+            for (String part : parts) {
+                propBitmapParts.add(parseBitmapPart(part, line));
+            }
             line = acceptLine("P");
         }
-        return hexStringBitmapToEventCodes(propBitmapParts);
+        return bitmapToEventCodes(propBitmapParts);
     }
 
     private void parseAxisBitmaps(SparseArray<int[]> config) throws IOException {
-        final Map<Integer, List<String>> axisBitmapParts = new HashMap<>();
+        final Map<Integer, ArrayList<Integer>> axisBitmapParts = new HashMap<>();
         String line = acceptLine("B");
         while (line != null) {
             final String[] parts = line.strip().split(" ");
             if (parts.length < 2) {
-                throw new RuntimeException(
+                throw new ParsingException(
                         "Expected event type and at least one bitmap byte on 'B:' line; only found "
-                                + parts.length + " elements");
+                                + parts.length + " elements", mReader);
             }
-            final int eventType = Integer.parseInt(parts[0], 16);
+            final int eventType = parseInt(parts[0], 16);
             // EV_SYN cannot be configured through uinput, so skip it.
             if (eventType != Event.EV_SYN) {
                 if (!axisBitmapParts.containsKey(eventType)) {
                     axisBitmapParts.put(eventType, new ArrayList<>());
                 }
+                ArrayList<Integer> bitmapParts = axisBitmapParts.get(eventType);
+                bitmapParts.ensureCapacity(bitmapParts.size() + parts.length);
                 for (int i = 1; i < parts.length; i++) {
-                    axisBitmapParts.get(eventType).add(parts[i]);
+                    axisBitmapParts.get(eventType).add(parseBitmapPart(parts[i], line));
                 }
             }
             line = acceptLine("B");
@@ -253,7 +297,7 @@
             }
             final Event.UinputControlCode controlCode =
                     Event.UinputControlCode.forEventType(entry.getKey());
-            final int[] eventCodes = hexStringBitmapToEventCodes(entry.getValue());
+            final int[] eventCodes = bitmapToEventCodes(entry.getValue());
             if (controlCode != null && eventCodes.length > 0) {
                 config.append(controlCode.getValue(), eventCodes);
                 eventTypesToSet.add(entry.getKey());
@@ -263,24 +307,33 @@
                 Event.UinputControlCode.UI_SET_EVBIT.getValue(), unboxIntList(eventTypesToSet));
     }
 
+    private int parseBitmapPart(String part, String line) {
+        int b = parseInt(part, 16);
+        if (b < 0x0 || b > 0xff) {
+            throw new ParsingException("Bitmap part '" + part
+                    + "' invalid; parts must be hexadecimal values between 00 and ff.", mReader);
+        }
+        return b;
+    }
+
     private SparseArray<InputAbsInfo> parseAbsInfos() throws IOException {
         final SparseArray<InputAbsInfo> absInfos = new SparseArray<>();
         String line = acceptLine("A");
         while (line != null) {
             final String[] parts = line.strip().split(" ");
             if (parts.length < 5 || parts.length > 6) {
-                throw new RuntimeException(
-                        "'A:' lines should have the format 'A: <index (hex)> <min> <max> <fuzz> "
+                throw new ParsingException(
+                        "AbsInfo lines should have the format 'A: <index (hex)> <min> <max> <fuzz> "
                                 + "<flat> [<resolution>]'; expected 5 or 6 numbers but found "
-                                + parts.length);
+                                + parts.length, mReader);
             }
-            final int axisCode = Integer.parseInt(parts[0], 16);
+            final int axisCode = parseInt(parts[0], 16);
             final InputAbsInfo info = new InputAbsInfo();
-            info.minimum = Integer.parseInt(parts[1]);
-            info.maximum = Integer.parseInt(parts[2]);
-            info.fuzz = Integer.parseInt(parts[3]);
-            info.flat = Integer.parseInt(parts[4]);
-            info.resolution = parts.length > 5 ? Integer.parseInt(parts[5]) : 0;
+            info.minimum = parseInt(parts[1], 10);
+            info.maximum = parseInt(parts[2], 10);
+            info.fuzz = parseInt(parts[3], 10);
+            info.flat = parseInt(parts[4], 10);
+            info.resolution = parts.length > 5 ? parseInt(parts[5], 10) : 0;
             absInfos.append(axisCode, info);
             line = acceptLine("A");
         }
@@ -305,7 +358,9 @@
     private String expectLine(String type) throws IOException {
         final String line = acceptLine(type);
         if (line == null) {
-            throw new RuntimeException("Expected line of type '" + type + "'");
+            throw new ParsingException("Expected line of type '" + type + "'. (Lines should be in "
+                    + "the order N, I, P, B, A, L, S, E.)",
+                    mReader.peekLine(), mReader.getPreviousLineNumber() + 1);
         } else {
             return line;
         }
@@ -325,9 +380,8 @@
         }
         final String[] lineParts = line.split(": ", 2);
         if (lineParts.length < 2) {
-            // TODO(b/302297266): make a proper exception class for syntax errors, including line
-            // numbers, etc.. (We can use LineNumberReader to track them.)
-            throw new RuntimeException("Line without ': '");
+            throw new ParsingException("Missing type separator ': '",
+                    line, mReader.getPreviousLineNumber() + 1);
         }
         if (lineParts[0].equals(type)) {
             mReader.advance();
@@ -337,31 +391,37 @@
         }
     }
 
-    /**
-     * Like {@link #expectLine(String)}, but also checks that the contents of the line is formed of
-     * {@code numParts} space-separated parts.
-     *
-     * @param type the type of the line to expect, represented by the letter before the ':'.
-     * @param numParts the number of parts to expect.
-     * @return the part of the line after the ": ", split into {@code numParts} sections.
-     */
-    private String[] expectLineWithParts(String type, int numParts) throws IOException {
-        final String[] parts = expectLine(type).strip().split(" ");
+    private String[] expectParts(String line, int numParts) {
+        final String[] parts = line.strip().split(" ");
         if (parts.length != numParts) {
-            throw new RuntimeException("Expected a '" + type + "' line with " + numParts
-                    + " parts, found one with " + parts.length);
+            throw new ParsingException(
+                    "Expected a line with " + numParts + " space-separated parts, but found one "
+                            + "with " + parts.length, mReader);
         }
         return parts;
     }
 
-    private static int[] hexStringBitmapToEventCodes(List<String> strs) {
+    private int parseInt(String s, int radix) {
+        try {
+            return Integer.parseInt(s, radix);
+        } catch (NumberFormatException ex) {
+            throw new ParsingException(
+                    "'" + s + "' is not a valid integer of base " + radix, mReader);
+        }
+    }
+
+    private long parseLong(String s, int radix) {
+        try {
+            return Long.parseLong(s, radix);
+        } catch (NumberFormatException ex) {
+            throw new ParsingException("'" + s + "' is not a valid long of base " + radix, mReader);
+        }
+    }
+
+    private static int[] bitmapToEventCodes(List<Integer> bytes) {
         final List<Integer> codes = new ArrayList<>();
-        for (int iByte = 0; iByte < strs.size(); iByte++) {
-            int b = Integer.parseInt(strs.get(iByte), 16);
-            if (b < 0x0 || b > 0xff) {
-                throw new RuntimeException("Bitmap part '" + strs.get(iByte)
-                        + "' invalid; parts must be between 00 and ff.");
-            }
+        for (int iByte = 0; iByte < bytes.size(); iByte++) {
+            int b = bytes.get(iByte);
             for (int iBit = 0; iBit < 8; iBit++) {
                 if ((b & 1) != 0) {
                     codes.add(iByte * 8 + iBit);
diff --git a/cmds/uinput/src/com/android/commands/uinput/Event.java b/cmds/uinput/src/com/android/commands/uinput/Event.java
index 5ec40e5..0f16a27 100644
--- a/cmds/uinput/src/com/android/commands/uinput/Event.java
+++ b/cmds/uinput/src/com/android/commands/uinput/Event.java
@@ -94,8 +94,9 @@
     private int mId;
     private Command mCommand;
     private String mName;
-    private int mVid;
-    private int mPid;
+    private int mVendorId;
+    private int mProductId;
+    private int mVersionId;
     private int mBusId;
     private int[] mInjections;
     private SparseArray<int[]> mConfiguration;
@@ -118,11 +119,15 @@
     }
 
     public int getVendorId() {
-        return mVid;
+        return mVendorId;
     }
 
     public int getProductId() {
-        return mPid;
+        return mProductId;
+    }
+
+    public int getVersionId() {
+        return mVersionId;
     }
 
     public int getBus() {
@@ -172,8 +177,8 @@
         return "Event{id=" + mId
             + ", command=" + mCommand
             + ", name=" + mName
-            + ", vid=" + mVid
-            + ", pid=" + mPid
+            + ", vid=" + mVendorId
+            + ", pid=" + mProductId
             + ", busId=" + mBusId
             + ", events=" + Arrays.toString(mInjections)
             + ", configuration=" + mConfiguration
@@ -216,12 +221,16 @@
             mEvent.mConfiguration = configuration;
         }
 
-        public void setVid(int vid) {
-            mEvent.mVid = vid;
+        public void setVendorId(int vendorId) {
+            mEvent.mVendorId = vendorId;
         }
 
-        public void setPid(int pid) {
-            mEvent.mPid = pid;
+        public void setProductId(int productId) {
+            mEvent.mProductId = productId;
+        }
+
+        public void setVersionId(int versionId) {
+            mEvent.mVersionId = versionId;
         }
 
         public void setBusId(int busId) {
diff --git a/cmds/uinput/src/com/android/commands/uinput/JsonStyleParser.java b/cmds/uinput/src/com/android/commands/uinput/JsonStyleParser.java
index 888ec5a..ed3ff33 100644
--- a/cmds/uinput/src/com/android/commands/uinput/JsonStyleParser.java
+++ b/cmds/uinput/src/com/android/commands/uinput/JsonStyleParser.java
@@ -60,8 +60,8 @@
                         case "command" -> eb.setCommand(
                                 Event.Command.valueOf(mReader.nextString().toUpperCase()));
                         case "name" -> eb.setName(mReader.nextString());
-                        case "vid" -> eb.setVid(readInt());
-                        case "pid" -> eb.setPid(readInt());
+                        case "vid" -> eb.setVendorId(readInt());
+                        case "pid" -> eb.setProductId(readInt());
                         case "bus" -> eb.setBusId(readBus());
                         case "events" -> {
                             int[] injections = readInjectedEvents().stream()
diff --git a/cmds/uinput/src/com/android/commands/uinput/Uinput.java b/cmds/uinput/src/com/android/commands/uinput/Uinput.java
index 684a12f..04df279 100644
--- a/cmds/uinput/src/com/android/commands/uinput/Uinput.java
+++ b/cmds/uinput/src/com/android/commands/uinput/Uinput.java
@@ -60,6 +60,10 @@
                 stream = new FileInputStream(f);
             }
             (new Uinput(stream)).run();
+        } catch (EvemuParser.ParsingException e) {
+            System.err.println(e.makeErrorMessage());
+            error(e.makeErrorMessage(), e);
+            System.exit(1);
         } catch (Exception e) {
             error("Uinput injection failed.", e);
             System.exit(1);
@@ -142,8 +146,9 @@
                     "Tried to send command \"" + e.getCommand() + "\" to an unregistered device!");
         }
         int id = e.getId();
-        Device d = new Device(id, e.getName(), e.getVendorId(), e.getProductId(), e.getBus(),
-                e.getConfiguration(), e.getFfEffectsMax(), e.getAbsInfo(), e.getPort());
+        Device d = new Device(id, e.getName(), e.getVendorId(), e.getProductId(),
+                e.getVersionId(), e.getBus(), e.getConfiguration(), e.getFfEffectsMax(),
+                e.getAbsInfo(), e.getPort());
         mDevices.append(id, d);
     }
 
diff --git a/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java b/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java
index 6ee987f..06b0aac2 100644
--- a/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java
+++ b/cmds/uinput/tests/src/com/android/commands/uinput/tests/EvemuParserTest.java
@@ -19,6 +19,8 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 
+import static org.junit.Assert.fail;
+
 import android.platform.test.annotations.Postsubmit;
 import android.util.SparseArray;
 
@@ -68,7 +70,7 @@
         assertThat(event.getBus()).isEqualTo(0x0001);
         assertThat(event.getVendorId()).isEqualTo(0x1234);
         assertThat(event.getProductId()).isEqualTo(0x5678);
-        // TODO(b/302297266): check version ID once it's supported
+        assertThat(event.getVersionId()).isEqualTo(0x9abc);
     }
 
     @Test
@@ -241,6 +243,25 @@
     }
 
     @Test
+    public void testErrorLineNumberReporting() throws IOException {
+        StringReader reader = new StringReader("""
+                # EVEMU 1.3
+                N: ACME Widget
+                # Comment to make sure they're taken into account when numbering lines
+                I: 0001 1234 5678 9abc
+                00 00 00 00 00 00 00 00  # Missing a type
+                E: 0.000001 0001 0015 0001   # KEY_Y press
+                E: 0.000001 0000 0000 0000   # SYN_REPORT
+                """);
+        try {
+            new EvemuParser(reader);
+            fail("Parser should have thrown an error about the line with the missing type.");
+        } catch (EvemuParser.ParsingException ex) {
+            assertThat(ex.makeErrorMessage()).startsWith("Parsing error on line 5:");
+        }
+    }
+
+    @Test
     public void testFreeDesktopEvemuRecording() throws IOException {
         // This is a real recording from FreeDesktop's evemu-record tool, as a basic compatibility
         // check with the FreeDesktop tools.
diff --git a/core/api/current.txt b/core/api/current.txt
index d7341e0..9d13d8a 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -43742,7 +43742,9 @@
     field public static final String KEY_SUPPORTED_IKE_SESSION_ENCRYPTION_ALGORITHMS_INT_ARRAY = "iwlan.supported_ike_session_encryption_algorithms_int_array";
     field public static final String KEY_SUPPORTED_INTEGRITY_ALGORITHMS_INT_ARRAY = "iwlan.supported_integrity_algorithms_int_array";
     field public static final String KEY_SUPPORTED_PRF_ALGORITHMS_INT_ARRAY = "iwlan.supported_prf_algorithms_int_array";
+    field @FlaggedApi("com.android.internal.telephony.flags.enable_multiple_sa_proposals") public static final String KEY_SUPPORTS_CHILD_SESSION_MULTIPLE_SA_PROPOSALS_BOOL = "iwlan.supports_child_session_multiple_sa_proposals_bool";
     field public static final String KEY_SUPPORTS_EAP_AKA_FAST_REAUTH_BOOL = "iwlan.supports_eap_aka_fast_reauth_bool";
+    field @FlaggedApi("com.android.internal.telephony.flags.enable_multiple_sa_proposals") public static final String KEY_SUPPORTS_IKE_SESSION_MULTIPLE_SA_PROPOSALS_BOOL = "iwlan.supports_ike_session_multiple_sa_proposals_bool";
   }
 
   public abstract class CellIdentity implements android.os.Parcelable {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 01ca6d9..ce5752f 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -4211,10 +4211,18 @@
 
   public final class UserProperties implements android.os.Parcelable {
     method public int describeContents();
+    method public int getShowInQuietMode();
+    method public int getShowInSharingSurfaces();
     method public boolean isCredentialShareableWithParent();
     method public boolean isMediaSharedWithParent();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
     field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.UserProperties> CREATOR;
+    field public static final int SHOW_IN_QUIET_MODE_DEFAULT = 2; // 0x2
+    field public static final int SHOW_IN_QUIET_MODE_HIDDEN = 1; // 0x1
+    field public static final int SHOW_IN_QUIET_MODE_PAUSED = 0; // 0x0
+    field public static final int SHOW_IN_SHARING_SURFACES_NO = 2; // 0x2
+    field public static final int SHOW_IN_SHARING_SURFACES_SEPARATE = 1; // 0x1
+    field public static final int SHOW_IN_SHARING_SURFACES_WITH_PARENT = 0; // 0x0
   }
 
 }
@@ -10560,6 +10568,7 @@
     method @NonNull public java.util.List<android.os.UserHandle> getEnabledProfiles();
     method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS, android.Manifest.permission.QUERY_USERS}) public android.os.UserHandle getMainUser();
     method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS, android.Manifest.permission.QUERY_USERS}) public android.os.UserHandle getPreviousForegroundUser();
+    method @NonNull public String getProfileLabel();
     method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.INTERACT_ACROSS_USERS}) public android.os.UserHandle getProfileParent(@NonNull android.os.UserHandle);
     method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS, android.Manifest.permission.QUERY_USERS}) public int getRemainingCreatableProfileCount(@NonNull String);
     method @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS, android.Manifest.permission.QUERY_USERS}) public int getRemainingCreatableUserCount(@NonNull String);
diff --git a/core/java/android/app/time/TEST_MAPPING b/core/java/android/app/time/TEST_MAPPING
index 47a152a..0f7a070 100644
--- a/core/java/android/app/time/TEST_MAPPING
+++ b/core/java/android/app/time/TEST_MAPPING
@@ -7,20 +7,6 @@
           "include-filter": "android.app."
         }
       ]
-    }
-  ],
-  // TODO(b/182461754): Change to "presubmit" when go/test-mapping-slo-guide allows.
-  "postsubmit": [
-    {
-      "name": "FrameworksServicesTests",
-      "options": [
-        {
-          "include-filter": "com.android.server.timezonedetector."
-        },
-        {
-          "include-filter": "com.android.server.timedetector."
-        }
-      ]
     },
     {
       "name": "CtsTimeTestCases",
@@ -30,5 +16,19 @@
         }
       ]
     }
+  ],
+  // TODO(b/182461754): Change to "presubmit" when go/test-mapping-slo-guide allows.
+  "postsubmit": [
+    {
+      "name": "FrameworksTimeServicesTests",
+      "options": [
+        {
+          "include-filter": "com.android.server.timezonedetector."
+        },
+        {
+          "include-filter": "com.android.server.timedetector."
+        }
+      ]
+    }
   ]
 }
diff --git a/core/java/android/app/timedetector/TEST_MAPPING b/core/java/android/app/timedetector/TEST_MAPPING
index 9517fb9..43dd82f 100644
--- a/core/java/android/app/timedetector/TEST_MAPPING
+++ b/core/java/android/app/timedetector/TEST_MAPPING
@@ -7,17 +7,6 @@
           "include-filter": "android.app."
         }
       ]
-    }
-  ],
-  // TODO(b/182461754): Change to "presubmit" when go/test-mapping-slo-guide allows.
-  "postsubmit": [
-    {
-      "name": "FrameworksServicesTests",
-      "options": [
-        {
-          "include-filter": "com.android.server.timedetector."
-        }
-      ]
     },
     {
       "name": "CtsTimeTestCases",
@@ -27,5 +16,16 @@
         }
       ]
     }
+  ],
+  // TODO(b/182461754): Change to "presubmit" when go/test-mapping-slo-guide allows.
+  "postsubmit": [
+    {
+      "name": "FrameworksTimeServicesTests",
+      "options": [
+        {
+          "include-filter": "com.android.server.timedetector."
+        }
+      ]
+    }
   ]
 }
diff --git a/core/java/android/app/timezonedetector/TEST_MAPPING b/core/java/android/app/timezonedetector/TEST_MAPPING
index fd41b86..2be5614 100644
--- a/core/java/android/app/timezonedetector/TEST_MAPPING
+++ b/core/java/android/app/timezonedetector/TEST_MAPPING
@@ -7,17 +7,6 @@
           "include-filter": "android.app."
         }
       ]
-    }
-  ],
-  // TODO(b/182461754): Change to "presubmit" when go/test-mapping-slo-guide allows.
-  "postsubmit": [
-    {
-      "name": "FrameworksServicesTests",
-      "options": [
-        {
-          "include-filter": "com.android.server.timezonedetector."
-        }
-      ]
     },
     {
       "name": "CtsTimeTestCases",
@@ -27,5 +16,16 @@
         }
       ]
     }
+  ],
+  // TODO(b/182461754): Change to "presubmit" when go/test-mapping-slo-guide allows.
+  "postsubmit": [
+    {
+      "name": "FrameworksTimeServicesTests",
+      "options": [
+        {
+          "include-filter": "com.android.server.timezonedetector."
+        }
+      ]
+    }
   ]
 }
diff --git a/core/java/android/content/pm/UserProperties.java b/core/java/android/content/pm/UserProperties.java
index f532c4c..445ca0c 100644
--- a/core/java/android/content/pm/UserProperties.java
+++ b/core/java/android/content/pm/UserProperties.java
@@ -19,6 +19,7 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
 import android.annotation.TestApi;
 import android.os.Parcel;
@@ -49,7 +50,8 @@
     private static final String ATTR_SHOW_IN_LAUNCHER = "showInLauncher";
     private static final String ATTR_START_WITH_PARENT = "startWithParent";
     private static final String ATTR_SHOW_IN_SETTINGS = "showInSettings";
-    private static final String ATTR_HIDE_IN_SETTINGS_IN_QUIET_MODE = "hideInSettingsInQuietMode";
+    private static final String ATTR_SHOW_IN_QUIET_MODE = "showInQuietMode";
+    private static final String ATTR_SHOW_IN_SHARING_SURFACES = "showInSharingSurfaces";
     private static final String ATTR_INHERIT_DEVICE_POLICY = "inheritDevicePolicy";
     private static final String ATTR_USE_PARENTS_CONTACTS = "useParentsContacts";
     private static final String ATTR_UPDATE_CROSS_PROFILE_INTENT_FILTERS_ON_OTA =
@@ -81,7 +83,8 @@
             INDEX_CREDENTIAL_SHAREABLE_WITH_PARENT,
             INDEX_DELETE_APP_WITH_PARENT,
             INDEX_ALWAYS_VISIBLE,
-            INDEX_HIDE_IN_SETTINGS_IN_QUIET_MODE,
+            INDEX_SHOW_IN_QUIET_MODE,
+            INDEX_SHOW_IN_SHARING_SURFACES,
             INDEX_AUTH_ALWAYS_REQUIRED_TO_DISABLE_QUIET_MODE,
     })
     @Retention(RetentionPolicy.SOURCE)
@@ -99,8 +102,9 @@
     private static final int INDEX_CREDENTIAL_SHAREABLE_WITH_PARENT = 9;
     private static final int INDEX_DELETE_APP_WITH_PARENT = 10;
     private static final int INDEX_ALWAYS_VISIBLE = 11;
-    private static final int INDEX_HIDE_IN_SETTINGS_IN_QUIET_MODE = 12;
+    private static final int INDEX_SHOW_IN_QUIET_MODE = 12;
     private static final int INDEX_AUTH_ALWAYS_REQUIRED_TO_DISABLE_QUIET_MODE = 13;
+    private static final int INDEX_SHOW_IN_SHARING_SURFACES = 14;
     /** A bit set, mapping each PropertyIndex to whether it is present (1) or absent (0). */
     private long mPropertiesPresent = 0;
 
@@ -286,6 +290,81 @@
      */
     public static final int CROSS_PROFILE_INTENT_RESOLUTION_STRATEGY_NO_FILTERING = 1;
 
+    /**
+     * Possible values for the profile visibility when in quiet mode. This affects the profile data
+     * and apps surfacing in Settings, sharing surfaces, and file picker surfaces. It signifies
+     * whether the profile data and apps will be shown or not.
+     *
+     * @hide
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = "SHOW_IN_QUIET_MODE_",
+            value = {
+                    SHOW_IN_QUIET_MODE_PAUSED,
+                    SHOW_IN_QUIET_MODE_HIDDEN,
+                    SHOW_IN_QUIET_MODE_DEFAULT,
+            }
+    )
+    public @interface ShowInQuietMode {
+    }
+
+    /**
+     * Indicates that the profile should still be visible in quiet mode but should be shown as
+     * paused (e.g. by greying out its icons).
+     */
+    @SuppressLint("UnflaggedApi") // b/306636213
+    public static final int SHOW_IN_QUIET_MODE_PAUSED = 0;
+    /**
+     * Indicates that the profile should not be visible when the profile is in quiet mode.
+     * For example, the profile should not be shown in tabbed views in Settings, files sharing
+     * surfaces etc when in quiet mode.
+     */
+    @SuppressLint("UnflaggedApi") // b/306636213
+    public static final int SHOW_IN_QUIET_MODE_HIDDEN = 1;
+    /**
+     * Indicates that quiet mode should not have any effect on the profile visibility. If the
+     * profile is meant to be visible, it will remain visible and vice versa.
+     */
+    @SuppressLint("UnflaggedApi") // b/306636213
+    public static final int SHOW_IN_QUIET_MODE_DEFAULT = 2;
+
+    /**
+     * Possible values for the profile apps visibility in sharing surfaces. This indicates the
+     * profile data and apps should be shown in separate tabs or mixed with its parent user's data
+     * and apps in sharing surfaces and file picker surfaces.
+     *
+     * @hide
+     */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = "SHOW_IN_SHARING_SURFACES_",
+            value = {
+                    SHOW_IN_SHARING_SURFACES_SEPARATE,
+                    SHOW_IN_SHARING_SURFACES_WITH_PARENT,
+                    SHOW_IN_SHARING_SURFACES_NO,
+            }
+    )
+    public @interface ShowInSharingSurfaces {
+    }
+
+    /**
+     * Indicates that the profile data and apps should be shown in sharing surfaces intermixed with
+     * parent user's data and apps.
+     */
+    @SuppressLint("UnflaggedApi") // b/306636213
+    public static final int SHOW_IN_SHARING_SURFACES_WITH_PARENT = SHOW_IN_LAUNCHER_WITH_PARENT;
+
+    /**
+     * Indicates that the profile data and apps should be shown in sharing surfaces separate from
+     * parent user's data and apps.
+     */
+    @SuppressLint("UnflaggedApi") // b/306636213
+    public static final int SHOW_IN_SHARING_SURFACES_SEPARATE = SHOW_IN_LAUNCHER_SEPARATE;
+
+    /**
+     * Indicates that the profile data and apps should not be shown in sharing surfaces at all.
+     */
+    @SuppressLint("UnflaggedApi") // b/306636213
+    public static final int SHOW_IN_SHARING_SURFACES_NO = SHOW_IN_LAUNCHER_NO;
 
     /**
      * Creates a UserProperties (intended for the SystemServer) that stores a reference to the given
@@ -331,7 +410,6 @@
         if (hasManagePermission) {
             // Add items that require MANAGE_USERS or stronger.
             setShowInSettings(orig.getShowInSettings());
-            setHideInSettingsInQuietMode(orig.getHideInSettingsInQuietMode());
             setUseParentsContacts(orig.getUseParentsContacts());
             setAuthAlwaysRequiredToDisableQuietMode(
                     orig.isAuthAlwaysRequiredToDisableQuietMode());
@@ -343,6 +421,8 @@
         setShowInLauncher(orig.getShowInLauncher());
         setMediaSharedWithParent(orig.isMediaSharedWithParent());
         setCredentialShareableWithParent(orig.isCredentialShareableWithParent());
+        setShowInQuietMode(orig.getShowInQuietMode());
+        setShowInSharingSurfaces(orig.getShowInSharingSurfaces());
     }
 
     /**
@@ -419,40 +499,59 @@
     private @ShowInSettings int mShowInSettings;
 
     /**
-     * Returns whether a user should be shown in the Settings app depending on the quiet mode.
-     * This is generally inapplicable for non-profile users.
+     * Returns whether a user should be shown in the Settings and sharing surfaces depending on the
+     * {@link android.os.UserManager#requestQuietModeEnabled(boolean, android.os.UserHandle)
+     * quiet mode}. This is only applicable to profile users since the quiet mode concept is only
+     * applicable to profile users.
      *
-     * <p> {@link #getShowInSettings()} returns whether / how a user should be shown in Settings.
-     * However, if this behaviour should be changed based on the quiet mode of the user, then this
-     * property can be used. If the property is not set then the user is shown in the Settings app
-     * irrespective of whether the user is in quiet mode or not. If the property is set, then the
-     * user is shown in the Settings app only if the user is not in the quiet mode. Please note that
-     * this property takes effect only if {@link #getShowInSettings()} does not return
-     * {@link #SHOW_IN_SETTINGS_NO}.
+     * <p> Please note that, in Settings, this property takes effect only if
+     * {@link #getShowInSettings()} does not return {@link #SHOW_IN_SETTINGS_NO}.
+     * Also note that in Sharing surfaces this property takes effect only if
+     * {@link #getShowInSharingSurfaces()} does not return {@link #SHOW_IN_SHARING_SURFACES_NO}.
      *
-     * <p> The caller must have {@link android.Manifest.permission#MANAGE_USERS} to query this
-     * property.
-     *
-     * @return true if a profile should be shown in the Settings only when the user is not in the
-     * quiet mode.
-     *
-     * See also {@link #getShowInSettings()}, {@link #setShowInSettings(int)},
-     * {@link ShowInSettings}
-     *
-     * @hide
+     * @return One of {@link #SHOW_IN_QUIET_MODE_HIDDEN},
+     *         {@link #SHOW_IN_QUIET_MODE_PAUSED}, or
+     *         {@link #SHOW_IN_QUIET_MODE_DEFAULT} depending on whether the profile should be
+     *         shown in quiet mode or not.
      */
-    public boolean getHideInSettingsInQuietMode() {
-        if (isPresent(INDEX_HIDE_IN_SETTINGS_IN_QUIET_MODE)) return mHideInSettingsInQuietMode;
-        if (mDefaultProperties != null) return mDefaultProperties.mHideInSettingsInQuietMode;
+    @SuppressLint("UnflaggedApi") // b/306636213
+    public @ShowInQuietMode int getShowInQuietMode() {
+        // NOTE: Launcher currently does not make use of this property.
+        if (isPresent(INDEX_SHOW_IN_QUIET_MODE)) return mShowInQuietMode;
+        if (mDefaultProperties != null) return mDefaultProperties.mShowInQuietMode;
         throw new SecurityException(
-                "You don't have permission to query HideInSettingsInQuietMode");
+                "You don't have permission to query ShowInQuietMode");
     }
     /** @hide */
-    public void setHideInSettingsInQuietMode(boolean hideInSettingsInQuietMode) {
-        this.mHideInSettingsInQuietMode = hideInSettingsInQuietMode;
-        setPresent(INDEX_HIDE_IN_SETTINGS_IN_QUIET_MODE);
+    public void setShowInQuietMode(@ShowInQuietMode int showInQuietMode) {
+        this.mShowInQuietMode = showInQuietMode;
+        setPresent(INDEX_SHOW_IN_QUIET_MODE);
     }
-    private boolean mHideInSettingsInQuietMode;
+    private int mShowInQuietMode;
+
+    /**
+     * Returns whether a user's data and apps should be shown in sharing surfaces in a separate tab
+     * or mixed with the parent user's data/apps. This is only applicable to profile users.
+     *
+     * @return One of {@link #SHOW_IN_SHARING_SURFACES_NO},
+     *         {@link #SHOW_IN_SHARING_SURFACES_SEPARATE}, or
+     *         {@link #SHOW_IN_SHARING_SURFACES_WITH_PARENT} depending on whether the profile
+     *         should be shown separate from its parent's data, mixed with the parent's data, or
+     *         not shown at all.
+     */
+    @SuppressLint("UnflaggedApi") // b/306636213
+    public @ShowInSharingSurfaces int getShowInSharingSurfaces() {
+        if (isPresent(INDEX_SHOW_IN_SHARING_SURFACES)) return mShowInSharingSurfaces;
+        if (mDefaultProperties != null) return mDefaultProperties.mShowInSharingSurfaces;
+        throw new SecurityException(
+                "You don't have permission to query ShowInSharingSurfaces");
+    }
+    /** @hide */
+    public void setShowInSharingSurfaces(@ShowInSharingSurfaces int showInSharingSurfaces) {
+        this.mShowInSharingSurfaces = showInSharingSurfaces;
+        setPresent(INDEX_SHOW_IN_SHARING_SURFACES);
+    }
+    private int mShowInSharingSurfaces;
 
     /**
      * Returns whether a profile should be started when its parent starts (unless in quiet mode).
@@ -799,8 +898,11 @@
                 case ATTR_SHOW_IN_SETTINGS:
                     setShowInSettings(parser.getAttributeInt(i));
                     break;
-                case ATTR_HIDE_IN_SETTINGS_IN_QUIET_MODE:
-                    setHideInSettingsInQuietMode(parser.getAttributeBoolean(i));
+                case ATTR_SHOW_IN_QUIET_MODE:
+                    setShowInQuietMode(parser.getAttributeInt(i));
+                    break;
+                case ATTR_SHOW_IN_SHARING_SURFACES:
+                    setShowInSharingSurfaces(parser.getAttributeInt(i));
                     break;
                 case ATTR_INHERIT_DEVICE_POLICY:
                     setInheritDevicePolicy(parser.getAttributeInt(i));
@@ -858,9 +960,12 @@
         if (isPresent(INDEX_SHOW_IN_SETTINGS)) {
             serializer.attributeInt(null, ATTR_SHOW_IN_SETTINGS, mShowInSettings);
         }
-        if (isPresent(INDEX_HIDE_IN_SETTINGS_IN_QUIET_MODE)) {
-            serializer.attributeBoolean(null, ATTR_HIDE_IN_SETTINGS_IN_QUIET_MODE,
-                    mHideInSettingsInQuietMode);
+        if (isPresent(INDEX_SHOW_IN_QUIET_MODE)) {
+            serializer.attributeInt(null, ATTR_SHOW_IN_QUIET_MODE,
+                    mShowInQuietMode);
+        }
+        if (isPresent(INDEX_SHOW_IN_SHARING_SURFACES)) {
+            serializer.attributeInt(null, ATTR_SHOW_IN_SHARING_SURFACES, mShowInSharingSurfaces);
         }
         if (isPresent(INDEX_INHERIT_DEVICE_POLICY)) {
             serializer.attributeInt(null, ATTR_INHERIT_DEVICE_POLICY,
@@ -912,7 +1017,8 @@
         dest.writeInt(mShowInLauncher);
         dest.writeBoolean(mStartWithParent);
         dest.writeInt(mShowInSettings);
-        dest.writeBoolean(mHideInSettingsInQuietMode);
+        dest.writeInt(mShowInQuietMode);
+        dest.writeInt(mShowInSharingSurfaces);
         dest.writeInt(mInheritDevicePolicy);
         dest.writeBoolean(mUseParentsContacts);
         dest.writeBoolean(mUpdateCrossProfileIntentFiltersOnOTA);
@@ -936,7 +1042,8 @@
         mShowInLauncher = source.readInt();
         mStartWithParent = source.readBoolean();
         mShowInSettings = source.readInt();
-        mHideInSettingsInQuietMode = source.readBoolean();
+        mShowInQuietMode = source.readInt();
+        mShowInSharingSurfaces = source.readInt();
         mInheritDevicePolicy = source.readInt();
         mUseParentsContacts = source.readBoolean();
         mUpdateCrossProfileIntentFiltersOnOTA = source.readBoolean();
@@ -974,7 +1081,10 @@
         private @ShowInLauncher int mShowInLauncher = SHOW_IN_LAUNCHER_WITH_PARENT;
         private boolean mStartWithParent = false;
         private @ShowInSettings int mShowInSettings = SHOW_IN_SETTINGS_WITH_PARENT;
-        private boolean mHideInSettingsInQuietMode = false;
+        private @ShowInQuietMode int mShowInQuietMode =
+                SHOW_IN_QUIET_MODE_PAUSED;
+        private @ShowInSharingSurfaces int mShowInSharingSurfaces =
+                SHOW_IN_SHARING_SURFACES_SEPARATE;
         private @InheritDevicePolicy int mInheritDevicePolicy = INHERIT_DEVICE_POLICY_NO;
         private boolean mUseParentsContacts = false;
         private boolean mUpdateCrossProfileIntentFiltersOnOTA = false;
@@ -1005,9 +1115,15 @@
             return this;
         }
 
-        /** Sets the value for {@link #mHideInSettingsInQuietMode} */
-        public Builder setHideInSettingsInQuietMode(boolean hideInSettingsInQuietMode) {
-            mHideInSettingsInQuietMode = hideInSettingsInQuietMode;
+        /** Sets the value for {@link #mShowInQuietMode} */
+        public Builder setShowInQuietMode(@ShowInQuietMode int showInQuietMode) {
+            mShowInQuietMode = showInQuietMode;
+            return this;
+        }
+
+        /** Sets the value for {@link #mShowInSharingSurfaces}. */
+        public Builder setShowInSharingSurfaces(@ShowInSharingSurfaces int showInSharingSurfaces) {
+            mShowInSharingSurfaces = showInSharingSurfaces;
             return this;
         }
 
@@ -1081,7 +1197,8 @@
                     mShowInLauncher,
                     mStartWithParent,
                     mShowInSettings,
-                    mHideInSettingsInQuietMode,
+                    mShowInQuietMode,
+                    mShowInSharingSurfaces,
                     mInheritDevicePolicy,
                     mUseParentsContacts,
                     mUpdateCrossProfileIntentFiltersOnOTA,
@@ -1100,7 +1217,8 @@
             @ShowInLauncher int showInLauncher,
             boolean startWithParent,
             @ShowInSettings int showInSettings,
-            boolean hideInSettingsInQuietMode,
+            @ShowInQuietMode int showInQuietMode,
+            @ShowInSharingSurfaces int showInSharingSurfaces,
             @InheritDevicePolicy int inheritDevicePolicy,
             boolean useParentsContacts, boolean updateCrossProfileIntentFiltersOnOTA,
             @CrossProfileIntentFilterAccessControlLevel int crossProfileIntentFilterAccessControl,
@@ -1114,7 +1232,8 @@
         setShowInLauncher(showInLauncher);
         setStartWithParent(startWithParent);
         setShowInSettings(showInSettings);
-        setHideInSettingsInQuietMode(hideInSettingsInQuietMode);
+        setShowInQuietMode(showInQuietMode);
+        setShowInSharingSurfaces(showInSharingSurfaces);
         setInheritDevicePolicy(inheritDevicePolicy);
         setUseParentsContacts(useParentsContacts);
         setUpdateCrossProfileIntentFiltersOnOTA(updateCrossProfileIntentFiltersOnOTA);
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index 9ec082a..6c6b33b 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -42,3 +42,10 @@
     description: "Allow using all cpu cores during a user switch."
     bug: "308105403"
 }
+
+flag {
+    name: "enable_biometrics_to_unlock_private_space"
+    namespace: "profile_experiences"
+    description: "Add support to unlock the private space using biometrics"
+    bug: "312184187"
+}
diff --git a/core/java/android/os/IUserManager.aidl b/core/java/android/os/IUserManager.aidl
index 330b992..c0d1fb9 100644
--- a/core/java/android/os/IUserManager.aidl
+++ b/core/java/android/os/IUserManager.aidl
@@ -131,6 +131,7 @@
     int getUserBadgeDarkColorResId(int userId);
     int getUserStatusBarIconResId(int userId);
     boolean hasBadge(int userId);
+    int getProfileLabelResId(int userId);
     boolean isUserUnlocked(int userId);
     boolean isUserRunning(int userId);
     boolean isUserForeground(int userId);
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 08d6e02..ec6d20f 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -5693,6 +5693,44 @@
     }
 
     /**
+     * Returns the string/label that should be used to represent the context user. For example,
+     * this string can represent a profile in tabbed views. This is only applicable to
+     * {@link #isProfile() profile users}. This string is translated to the device default language.
+     *
+     * @return String representing the label for the context user.
+     *
+     * @throws android.content.res.Resources.NotFoundException if the user does not have a label
+     * defined.
+     *
+     * @hide
+     */
+    @SystemApi
+    @SuppressLint("UnflaggedApi") // b/306636213
+    @UserHandleAware(
+            requiresAnyOfPermissionsIfNotCallerProfileGroup = {
+                    Manifest.permission.MANAGE_USERS,
+                    Manifest.permission.QUERY_USERS,
+                    Manifest.permission.INTERACT_ACROSS_USERS})
+    public @NonNull String getProfileLabel() {
+        if (isManagedProfile(mUserId)) {
+            DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class);
+            return dpm.getResources().getString(
+                    android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB,
+                    () -> getDefaultProfileLabel(mUserId));
+        }
+        return getDefaultProfileLabel(mUserId);
+    }
+
+    private String getDefaultProfileLabel(int userId) {
+        try {
+            final int resourceId = mService.getProfileLabelResId(userId);
+            return Resources.getSystem().getString(resourceId);
+        } catch (RemoteException re) {
+            throw re.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * If the user is a {@link UserManager#isProfile profile}, checks if the user
      * shares media with its parent user (the user that created this profile).
      * Returns false for any other type of user.
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 7b45600..7a6c292 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -130,7 +130,6 @@
 import android.graphics.HardwareRenderer;
 import android.graphics.HardwareRenderer.FrameDrawingCallback;
 import android.graphics.HardwareRendererObserver;
-import android.graphics.Insets;
 import android.graphics.Matrix;
 import android.graphics.Paint;
 import android.graphics.PixelFormat;
@@ -206,7 +205,6 @@
 import android.view.accessibility.IAccessibilityInteractionConnectionCallback;
 import android.view.animation.AccelerateDecelerateInterpolator;
 import android.view.animation.Interpolator;
-import android.view.autofill.AutofillId;
 import android.view.autofill.AutofillManager;
 import android.view.contentcapture.ContentCaptureManager;
 import android.view.contentcapture.ContentCaptureSession;
@@ -4029,56 +4027,20 @@
     }
 
     private void notifyContentCaptureEvents() {
-        try {
-            if (!isContentCaptureEnabled()) {
-                if (DEBUG_CONTENT_CAPTURE) {
-                    Log.d(mTag, "notifyContentCaptureEvents while disabled");
-                }
-                mAttachInfo.mContentCaptureEvents = null;
-                return;
-            }
-            if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
-                Trace.traceBegin(Trace.TRACE_TAG_VIEW, "notifyContentCaptureEvents");
-            }
-            MainContentCaptureSession mainSession = mAttachInfo.mContentCaptureManager
-                    .getMainContentCaptureSession();
-            for (int i = 0; i < mAttachInfo.mContentCaptureEvents.size(); i++) {
-                int sessionId = mAttachInfo.mContentCaptureEvents.keyAt(i);
-                mainSession.notifyViewTreeEvent(sessionId, /* started= */ true);
-                ArrayList<Object> events = mAttachInfo.mContentCaptureEvents
-                        .valueAt(i);
-                for_each_event: for (int j = 0; j < events.size(); j++) {
-                    Object event = events.get(j);
-                    if (event instanceof AutofillId) {
-                        mainSession.notifyViewDisappeared(sessionId, (AutofillId) event);
-                    } else if (event instanceof View) {
-                        View view = (View) event;
-                        ContentCaptureSession session = view.getContentCaptureSession();
-                        if (session == null) {
-                            Log.w(mTag, "no content capture session on view: " + view);
-                            continue for_each_event;
-                        }
-                        int actualId = session.getId();
-                        if (actualId != sessionId) {
-                            Log.w(mTag, "content capture session mismatch for view (" + view
-                                    + "): was " + sessionId + " before, it's " + actualId + " now");
-                            continue for_each_event;
-                        }
-                        ViewStructure structure = session.newViewStructure(view);
-                        view.onProvideContentCaptureStructure(structure, /* flags= */ 0);
-                        session.notifyViewAppeared(structure);
-                    } else if (event instanceof Insets) {
-                        mainSession.notifyViewInsetsChanged(sessionId, (Insets) event);
-                    } else {
-                        Log.w(mTag, "invalid content capture event: " + event);
-                    }
-                }
-                mainSession.notifyViewTreeEvent(sessionId, /* started= */ false);
+        if (!isContentCaptureEnabled()) {
+            if (DEBUG_CONTENT_CAPTURE) {
+                Log.d(mTag, "notifyContentCaptureEvents while disabled");
             }
             mAttachInfo.mContentCaptureEvents = null;
-        } finally {
-            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+            return;
         }
+
+        final ContentCaptureManager manager = mAttachInfo.mContentCaptureManager;
+        if (manager != null && mAttachInfo.mContentCaptureEvents != null) {
+            final MainContentCaptureSession session = manager.getMainContentCaptureSession();
+            session.notifyContentCaptureEvents(mAttachInfo.mContentCaptureEvents);
+        }
+        mAttachInfo.mContentCaptureEvents = null;
     }
 
     private void notifyHolderSurfaceDestroyed() {
diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java
index 5a058ff..a829747 100644
--- a/core/java/android/view/contentcapture/ContentCaptureManager.java
+++ b/core/java/android/view/contentcapture/ContentCaptureManager.java
@@ -18,6 +18,7 @@
 import static android.view.contentcapture.ContentCaptureHelper.sDebug;
 import static android.view.contentcapture.ContentCaptureHelper.sVerbose;
 import static android.view.contentcapture.ContentCaptureHelper.toSet;
+import static android.view.contentcapture.flags.Flags.runOnBackgroundThreadEnabled;
 
 import android.annotation.CallbackExecutor;
 import android.annotation.IntDef;
@@ -52,6 +53,7 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.RingBuffer;
 import com.android.internal.util.SyncResultReceiver;
 
@@ -495,10 +497,9 @@
     @GuardedBy("mLock")
     private int mFlags;
 
-    // TODO(b/119220549): use UI Thread directly (as calls are one-way) or a shared thread / handler
-    // held at the Application level
-    @NonNull
-    private final Handler mHandler;
+    @Nullable
+    @GuardedBy("mLock")
+    private Handler mHandler;
 
     @GuardedBy("mLock")
     private MainContentCaptureSession mMainSession;
@@ -562,11 +563,6 @@
 
         if (sVerbose) Log.v(TAG, "Constructor for " + context.getPackageName());
 
-        // TODO(b/119220549): we might not even need a handler, as the IPCs are oneway. But if we
-        // do, then we should optimize it to run the tests after the Choreographer finishes the most
-        // important steps of the frame.
-        mHandler = Handler.createAsync(Looper.getMainLooper());
-
         mDataShareAdapterResourceManager = new LocalDataShareAdapterResourceManager();
 
         if (mOptions.contentProtectionOptions.enableReceiver
@@ -594,13 +590,27 @@
     public MainContentCaptureSession getMainContentCaptureSession() {
         synchronized (mLock) {
             if (mMainSession == null) {
-                mMainSession = new MainContentCaptureSession(mContext, this, mHandler, mService);
+                mMainSession = new MainContentCaptureSession(
+                        mContext, this, prepareContentCaptureHandler(), mService);
                 if (sVerbose) Log.v(TAG, "getMainContentCaptureSession(): created " + mMainSession);
             }
             return mMainSession;
         }
     }
 
+    @NonNull
+    @GuardedBy("mLock")
+    private Handler prepareContentCaptureHandler() {
+        if (mHandler == null) {
+            if (runOnBackgroundThreadEnabled()) {
+                mHandler = BackgroundThread.getHandler();
+            } else {
+                mHandler = Handler.createAsync(Looper.getMainLooper());
+            }
+        }
+        return mHandler;
+    }
+
     /** @hide */
     @UiThread
     public void onActivityCreated(@NonNull IBinder applicationToken,
diff --git a/core/java/android/view/contentcapture/MainContentCaptureSession.java b/core/java/android/view/contentcapture/MainContentCaptureSession.java
index d9b0f80..542c783c 100644
--- a/core/java/android/view/contentcapture/MainContentCaptureSession.java
+++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java
@@ -34,7 +34,6 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.UiThread;
 import android.content.ComponentName;
 import android.content.pm.ParceledListSlice;
 import android.graphics.Insets;
@@ -50,7 +49,10 @@
 import android.text.TextUtils;
 import android.util.LocalLog;
 import android.util.Log;
+import android.util.SparseArray;
 import android.util.TimeUtils;
+import android.view.View;
+import android.view.ViewStructure;
 import android.view.autofill.AutofillId;
 import android.view.contentcapture.ViewNode.ViewStructureImpl;
 import android.view.contentprotection.ContentProtectionEventProcessor;
@@ -207,7 +209,8 @@
             } else {
                 binder = null;
             }
-            mainSession.mHandler.post(() -> mainSession.onSessionStarted(resultCode, binder));
+            mainSession.mHandler.post(() ->
+                    mainSession.onSessionStarted(resultCode, binder));
         }
     }
 
@@ -244,9 +247,14 @@
     /**
      * Starts this session.
      */
-    @UiThread
     void start(@NonNull IBinder token, @NonNull IBinder shareableActivityToken,
             @NonNull ComponentName component, int flags) {
+        runOnContentCaptureThread(() -> startImpl(token, shareableActivityToken, component, flags));
+    }
+
+    private void startImpl(@NonNull IBinder token, @NonNull IBinder shareableActivityToken,
+               @NonNull ComponentName component, int flags) {
+        checkOnContentCaptureThread();
         if (!isContentCaptureEnabled()) return;
 
         if (sVerbose) {
@@ -280,17 +288,15 @@
             Log.w(TAG, "Error starting session for " + component.flattenToShortString() + ": " + e);
         }
     }
-
     @Override
     void onDestroy() {
-        mHandler.removeMessages(MSG_FLUSH);
-        mHandler.post(() -> {
+        clearAndRunOnContentCaptureThread(() -> {
             try {
                 flush(FLUSH_REASON_SESSION_FINISHED);
             } finally {
                 destroySession();
             }
-        });
+        }, MSG_FLUSH);
     }
 
     /**
@@ -302,8 +308,8 @@
      * @hide
      */
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
-    @UiThread
     public void onSessionStarted(int resultCode, @Nullable IBinder binder) {
+        checkOnContentCaptureThread();
         if (binder != null) {
             mDirectServiceInterface = IContentCaptureDirectManager.Stub.asInterface(binder);
             mDirectServiceVulture = () -> {
@@ -347,13 +353,12 @@
 
     /** @hide */
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
-    @UiThread
     public void sendEvent(@NonNull ContentCaptureEvent event) {
         sendEvent(event, /* forceFlush= */ false);
     }
 
-    @UiThread
     private void sendEvent(@NonNull ContentCaptureEvent event, boolean forceFlush) {
+        checkOnContentCaptureThread();
         final int eventType = event.getType();
         if (sVerbose) Log.v(TAG, "handleSendEvent(" + getDebugState() + "): " + event);
         if (!hasStarted() && eventType != ContentCaptureEvent.TYPE_SESSION_STARTED
@@ -396,15 +401,15 @@
         }
     }
 
-    @UiThread
     private void sendContentProtectionEvent(@NonNull ContentCaptureEvent event) {
+        checkOnContentCaptureThread();
         if (mContentProtectionEventProcessor != null) {
             mContentProtectionEventProcessor.processEvent(event);
         }
     }
 
-    @UiThread
     private void sendContentCaptureEvent(@NonNull ContentCaptureEvent event, boolean forceFlush) {
+        checkOnContentCaptureThread();
         final int eventType = event.getType();
         final int maxBufferSize = mManager.mOptions.maxBufferSize;
         if (mEvents == null) {
@@ -538,13 +543,13 @@
         flush(flushReason);
     }
 
-    @UiThread
     private boolean hasStarted() {
+        checkOnContentCaptureThread();
         return mState != UNKNOWN_STATE;
     }
 
-    @UiThread
     private void scheduleFlush(@FlushReason int reason, boolean checkExisting) {
+        checkOnContentCaptureThread();
         if (sVerbose) {
             Log.v(TAG, "handleScheduleFlush(" + getDebugState(reason)
                     + ", checkExisting=" + checkExisting);
@@ -588,8 +593,8 @@
         mHandler.postDelayed(() -> flushIfNeeded(reason), MSG_FLUSH, flushFrequencyMs);
     }
 
-    @UiThread
     private void flushIfNeeded(@FlushReason int reason) {
+        checkOnContentCaptureThread();
         if (mEvents == null || mEvents.isEmpty()) {
             if (sVerbose) Log.v(TAG, "Nothing to flush");
             return;
@@ -600,8 +605,12 @@
     /** @hide */
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     @Override
-    @UiThread
     public void flush(@FlushReason int reason) {
+        runOnContentCaptureThread(() -> flushImpl(reason));
+    }
+
+    private void flushImpl(@FlushReason int reason) {
+        checkOnContentCaptureThread();
         if (mEvents == null || mEvents.size() == 0) {
             if (sVerbose) {
                 Log.v(TAG, "Don't flush for empty event buffer.");
@@ -669,8 +678,8 @@
      * Resets the buffer and return a {@link ParceledListSlice} with the previous events.
      */
     @NonNull
-    @UiThread
     private ParceledListSlice<ContentCaptureEvent> clearEvents() {
+        checkOnContentCaptureThread();
         // NOTE: we must save a reference to the current mEvents and then set it to to null,
         // otherwise clearing it would clear it in the receiving side if the service is also local.
         if (mEvents == null) {
@@ -684,8 +693,8 @@
 
     /** hide */
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
-    @UiThread
     public void destroySession() {
+        checkOnContentCaptureThread();
         if (sDebug) {
             Log.d(TAG, "Destroying session (ctx=" + mContext + ", id=" + mId + ") with "
                     + (mEvents == null ? 0 : mEvents.size()) + " event(s) for "
@@ -710,8 +719,8 @@
     // clearings out.
     /** @hide */
     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
-    @UiThread
     public void resetSession(int newState) {
+        checkOnContentCaptureThread();
         if (sVerbose) {
             Log.v(TAG, "handleResetSession(" + getActivityName() + "): from "
                     + getStateAsString(mState) + " to " + getStateAsString(newState));
@@ -794,24 +803,26 @@
     // change should also get get rid of the "internalNotifyXXXX" methods above
     void notifyChildSessionStarted(int parentSessionId, int childSessionId,
             @NonNull ContentCaptureContext clientContext) {
-        mHandler.post(() -> sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_STARTED)
+        runOnContentCaptureThread(
+                () -> sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_STARTED)
                 .setParentSessionId(parentSessionId).setClientContext(clientContext),
                 FORCE_FLUSH));
     }
 
     void notifyChildSessionFinished(int parentSessionId, int childSessionId) {
-        mHandler.post(() -> sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_FINISHED)
+        runOnContentCaptureThread(
+                () -> sendEvent(new ContentCaptureEvent(childSessionId, TYPE_SESSION_FINISHED)
                 .setParentSessionId(parentSessionId), FORCE_FLUSH));
     }
 
     void notifyViewAppeared(int sessionId, @NonNull ViewStructureImpl node) {
-        mHandler.post(() -> sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_APPEARED)
+        runOnContentCaptureThread(() ->
+                sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_APPEARED)
                 .setViewNode(node.mNode)));
     }
 
-    /** Public because is also used by ViewRootImpl */
-    public void notifyViewDisappeared(int sessionId, @NonNull AutofillId id) {
-        mHandler.post(() -> sendEvent(
+    void notifyViewDisappeared(int sessionId, @NonNull AutofillId id) {
+        runOnContentCaptureThread(() -> sendEvent(
                 new ContentCaptureEvent(sessionId, TYPE_VIEW_DISAPPEARED).setAutofillId(id)));
     }
 
@@ -836,52 +847,102 @@
 
         final int startIndex = Selection.getSelectionStart(text);
         final int endIndex = Selection.getSelectionEnd(text);
-        mHandler.post(() -> sendEvent(
+        runOnContentCaptureThread(() -> sendEvent(
                 new ContentCaptureEvent(sessionId, TYPE_VIEW_TEXT_CHANGED)
                         .setAutofillId(id).setText(eventText)
                         .setComposingIndex(composingStart, composingEnd)
                         .setSelectionIndex(startIndex, endIndex)));
     }
 
-    /** Public because is also used by ViewRootImpl */
-    public void notifyViewInsetsChanged(int sessionId, @NonNull Insets viewInsets) {
-        mHandler.post(() -> sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_INSETS_CHANGED)
+    void notifyViewInsetsChanged(int sessionId, @NonNull Insets viewInsets) {
+        runOnContentCaptureThread(() ->
+                sendEvent(new ContentCaptureEvent(sessionId, TYPE_VIEW_INSETS_CHANGED)
                 .setInsets(viewInsets)));
     }
 
-    /** Public because is also used by ViewRootImpl */
-    public void notifyViewTreeEvent(int sessionId, boolean started) {
+    void notifyViewTreeEvent(int sessionId, boolean started) {
         final int type = started ? TYPE_VIEW_TREE_APPEARING : TYPE_VIEW_TREE_APPEARED;
         final boolean disableFlush = mManager.getFlushViewTreeAppearingEventDisabled();
 
-        mHandler.post(() -> sendEvent(
+        runOnContentCaptureThread(() -> sendEvent(
                 new ContentCaptureEvent(sessionId, type),
                 disableFlush ? !started : FORCE_FLUSH));
     }
 
     void notifySessionResumed(int sessionId) {
-        mHandler.post(() -> sendEvent(
+        runOnContentCaptureThread(() -> sendEvent(
                 new ContentCaptureEvent(sessionId, TYPE_SESSION_RESUMED), FORCE_FLUSH));
     }
 
     void notifySessionPaused(int sessionId) {
-        mHandler.post(() -> sendEvent(
+        runOnContentCaptureThread(() -> sendEvent(
                 new ContentCaptureEvent(sessionId, TYPE_SESSION_PAUSED), FORCE_FLUSH));
     }
 
     void notifyContextUpdated(int sessionId, @Nullable ContentCaptureContext context) {
-        mHandler.post(() -> sendEvent(new ContentCaptureEvent(sessionId, TYPE_CONTEXT_UPDATED)
+        runOnContentCaptureThread(() ->
+                sendEvent(new ContentCaptureEvent(sessionId, TYPE_CONTEXT_UPDATED)
                 .setClientContext(context), FORCE_FLUSH));
     }
 
     /** public because is also used by ViewRootImpl */
     public void notifyWindowBoundsChanged(int sessionId, @NonNull Rect bounds) {
-        mHandler.post(() -> sendEvent(
+        runOnContentCaptureThread(() -> sendEvent(
                 new ContentCaptureEvent(sessionId, TYPE_WINDOW_BOUNDS_CHANGED)
                 .setBounds(bounds)
         ));
     }
 
+    /** public because is also used by ViewRootImpl */
+    public void notifyContentCaptureEvents(
+            @NonNull SparseArray<ArrayList<Object>> contentCaptureEvents) {
+        runOnContentCaptureThread(() -> notifyContentCaptureEventsImpl(contentCaptureEvents));
+    }
+
+    private void notifyContentCaptureEventsImpl(
+            @NonNull SparseArray<ArrayList<Object>> contentCaptureEvents) {
+        checkOnContentCaptureThread();
+        try {
+            if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
+                Trace.traceBegin(Trace.TRACE_TAG_VIEW, "notifyContentCaptureEvents");
+            }
+            for (int i = 0; i < contentCaptureEvents.size(); i++) {
+                int sessionId = contentCaptureEvents.keyAt(i);
+                notifyViewTreeEvent(sessionId, /* started= */ true);
+                ArrayList<Object> events = contentCaptureEvents.valueAt(i);
+                for_each_event: for (int j = 0; j < events.size(); j++) {
+                    Object event = events.get(j);
+                    if (event instanceof AutofillId) {
+                        notifyViewDisappeared(sessionId, (AutofillId) event);
+                    } else if (event instanceof View) {
+                        View view = (View) event;
+                        ContentCaptureSession session = view.getContentCaptureSession();
+                        if (session == null) {
+                            Log.w(TAG, "no content capture session on view: " + view);
+                            continue for_each_event;
+                        }
+                        int actualId = session.getId();
+                        if (actualId != sessionId) {
+                            Log.w(TAG, "content capture session mismatch for view (" + view
+                                    + "): was " + sessionId + " before, it's " + actualId + " now");
+                            continue for_each_event;
+                        }
+                        ViewStructure structure = session.newViewStructure(view);
+                        view.onProvideContentCaptureStructure(structure, /* flags= */ 0);
+                        session.notifyViewAppeared(structure);
+                    } else if (event instanceof Insets) {
+                        notifyViewInsetsChanged(sessionId, (Insets) event);
+                    } else {
+                        Log.w(TAG, "invalid content capture event: " + event);
+                    }
+                }
+                notifyViewTreeEvent(sessionId, /* started= */ false);
+            }
+        } finally {
+            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+        }
+    }
+
     @Override
     void dump(@NonNull String prefix, @NonNull PrintWriter pw) {
         super.dump(prefix, pw);
@@ -960,17 +1021,14 @@
         return getDebugState() + ", reason=" + getFlushReasonAsString(reason);
     }
 
-    @UiThread
     private boolean isContentProtectionReceiverEnabled() {
         return mManager.mOptions.contentProtectionOptions.enableReceiver;
     }
 
-    @UiThread
     private boolean isContentCaptureReceiverEnabled() {
         return mManager.mOptions.enableReceiver;
     }
 
-    @UiThread
     private boolean isContentProtectionEnabled() {
         // Should not be possible for mComponentName to be null here but check anyway
         // Should not be possible for groups to be empty if receiver is enabled but check anyway
@@ -980,4 +1038,42 @@
                 && (!mManager.mOptions.contentProtectionOptions.requiredGroups.isEmpty()
                         || !mManager.mOptions.contentProtectionOptions.optionalGroups.isEmpty());
     }
+
+    /**
+     * Checks that the current work is running on the assigned thread from {@code mHandler}.
+     *
+     * <p>It is not guaranteed that the callers always invoke function from a single thread.
+     * Therefore, accessing internal properties in {@link MainContentCaptureSession} should
+     * always delegate to the assigned thread from {@code mHandler} for synchronization.</p>
+     */
+    private void checkOnContentCaptureThread() {
+        // TODO(b/309411951): Add metrics to track the issue instead.
+        final boolean onContentCaptureThread = mHandler.getLooper().isCurrentThread();
+        if (!onContentCaptureThread) {
+            Log.e(TAG, "MainContentCaptureSession running on " + Thread.currentThread());
+        }
+    }
+
+    /**
+     * Ensures that {@code r} will be running on the assigned thread.
+     *
+     * <p>This is to prevent unnecessary delegation to Handler that results in fragmented runnable.
+     * </p>
+     */
+    private void runOnContentCaptureThread(@NonNull Runnable r) {
+        if (!mHandler.getLooper().isCurrentThread()) {
+            mHandler.post(r);
+        } else {
+            r.run();
+        }
+    }
+
+    private void clearAndRunOnContentCaptureThread(@NonNull Runnable r, int what) {
+        if (!mHandler.getLooper().isCurrentThread()) {
+            mHandler.removeMessages(what);
+            mHandler.post(r);
+        } else {
+            r.run();
+        }
+    }
 }
diff --git a/core/java/android/view/contentprotection/ContentProtectionEventProcessor.java b/core/java/android/view/contentprotection/ContentProtectionEventProcessor.java
index aaf90bd..858401a9 100644
--- a/core/java/android/view/contentprotection/ContentProtectionEventProcessor.java
+++ b/core/java/android/view/contentprotection/ContentProtectionEventProcessor.java
@@ -18,7 +18,6 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.UiThread;
 import android.content.ContentCaptureOptions;
 import android.content.pm.ParceledListSlice;
 import android.os.Handler;
@@ -102,7 +101,6 @@
     }
 
     /** Main entry point for {@link ContentCaptureEvent} processing. */
-    @UiThread
     public void processEvent(@NonNull ContentCaptureEvent event) {
         if (EVENT_TYPES_TO_STORE.contains(event.getType())) {
             storeEvent(event);
@@ -112,7 +110,6 @@
         }
     }
 
-    @UiThread
     private void storeEvent(@NonNull ContentCaptureEvent event) {
         // Ensure receiver gets the package name which might not be set
         ViewNode viewNode = (event.getViewNode() != null) ? event.getViewNode() : new ViewNode();
@@ -121,7 +118,6 @@
         mEventBuffer.append(event);
     }
 
-    @UiThread
     private void processViewAppearedEvent(@NonNull ContentCaptureEvent event) {
         ViewNode viewNode = event.getViewNode();
         String eventText = ContentProtectionUtils.getEventTextLower(event);
@@ -154,7 +150,6 @@
         }
     }
 
-    @UiThread
     private void loginDetected() {
         if (mLastFlushTime == null
                 || Instant.now().isAfter(mLastFlushTime.plus(MIN_DURATION_BETWEEN_FLUSHING))) {
@@ -163,13 +158,11 @@
         resetLoginFlags();
     }
 
-    @UiThread
     private void resetLoginFlags() {
         mGroupsAll.forEach(group -> group.mFound = false);
         mAnyGroupFound = false;
     }
 
-    @UiThread
     private void maybeResetLoginFlags() {
         if (mAnyGroupFound) {
             if (mResetLoginRemainingEventsToProcess <= 0) {
@@ -183,7 +176,6 @@
         }
     }
 
-    @UiThread
     private void flush() {
         mLastFlushTime = Instant.now();
 
@@ -192,7 +184,6 @@
         mHandler.post(() -> handlerOnLoginDetected(events));
     }
 
-    @UiThread
     @NonNull
     private ParceledListSlice<ContentCaptureEvent> clearEvents() {
         List<ContentCaptureEvent> events = Arrays.asList(mEventBuffer.toArray());
diff --git a/core/java/android/webkit/WebViewDelegate.java b/core/java/android/webkit/WebViewDelegate.java
index 1b9ff44..8e89541 100644
--- a/core/java/android/webkit/WebViewDelegate.java
+++ b/core/java/android/webkit/WebViewDelegate.java
@@ -16,6 +16,8 @@
 
 package android.webkit;
 
+import static android.webkit.Flags.updateServiceV2;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SystemApi;
@@ -205,6 +207,9 @@
      * Returns whether WebView should run in multiprocess mode.
      */
     public boolean isMultiProcessEnabled() {
+        if (updateServiceV2()) {
+            return true;
+        }
         try {
             return WebViewFactory.getUpdateService().isMultiProcessEnabled();
         } catch (RemoteException e) {
diff --git a/core/java/android/webkit/WebViewZygote.java b/core/java/android/webkit/WebViewZygote.java
index bc7a5fd..e14ae72 100644
--- a/core/java/android/webkit/WebViewZygote.java
+++ b/core/java/android/webkit/WebViewZygote.java
@@ -16,6 +16,8 @@
 
 package android.webkit;
 
+import static android.webkit.Flags.updateServiceV2;
+
 import android.content.pm.PackageInfo;
 import android.os.Build;
 import android.os.ChildZygoteProcess;
@@ -50,8 +52,8 @@
     private static PackageInfo sPackage;
 
     /**
-     * Flag for whether multi-process WebView is enabled. If this is {@code false}, the zygote
-     * will not be started.
+     * Flag for whether multi-process WebView is enabled. If this is {@code false}, the zygote will
+     * not be started. Should be removed entirely after we remove the updateServiceV2 flag.
      */
     @GuardedBy("sLock")
     private static boolean sMultiprocessEnabled = false;
@@ -73,11 +75,19 @@
 
     public static boolean isMultiprocessEnabled() {
         synchronized (sLock) {
-            return sMultiprocessEnabled && sPackage != null;
+            if (updateServiceV2()) {
+                return sPackage != null;
+            } else {
+                return sMultiprocessEnabled && sPackage != null;
+            }
         }
     }
 
     public static void setMultiprocessEnabled(boolean enabled) {
+        if (updateServiceV2()) {
+            throw new IllegalStateException(
+                    "setMultiprocessEnabled shouldn't be called if update_service_v2 flag is set.");
+        }
         synchronized (sLock) {
             sMultiprocessEnabled = enabled;
 
diff --git a/core/java/android/window/flags/window_surfaces.aconfig b/core/java/android/window/flags/window_surfaces.aconfig
index 0da03fb..7f93213 100644
--- a/core/java/android/window/flags/window_surfaces.aconfig
+++ b/core/java/android/window/flags/window_surfaces.aconfig
@@ -35,8 +35,8 @@
 
 flag {
     namespace: "window_surfaces"
-    name: "remove_capture_display"
-    description: "Remove uses of ScreenCapture#captureDisplay"
+    name: "delete_capture_display"
+    description: "Delete uses of ScreenCapture#captureDisplay"
     is_fixed_read_only: true
     bug: "293445881"
 }
diff --git a/core/java/com/android/internal/os/MonotonicClock.java b/core/java/com/android/internal/os/MonotonicClock.java
index 6f114e3..d0d2354 100644
--- a/core/java/com/android/internal/os/MonotonicClock.java
+++ b/core/java/com/android/internal/os/MonotonicClock.java
@@ -30,6 +30,7 @@
 
 import java.io.ByteArrayInputStream;
 import java.io.File;
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -98,14 +99,11 @@
             return;
         }
 
-        mFile.write(out -> {
-            try {
-                writeXml(out, Xml.newBinarySerializer());
-                out.close();
-            } catch (IOException e) {
-                Log.e(TAG, "Cannot write monotonic clock to " + mFile.getBaseFile(), e);
-            }
-        });
+        try (FileOutputStream out = mFile.startWrite()) {
+            writeXml(out, Xml.newBinarySerializer());
+        } catch (IOException e) {
+            Log.e(TAG, "Cannot write monotonic clock to " + mFile.getBaseFile(), e);
+        }
     }
 
     /**
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index eed186a..73a7e42 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -6350,4 +6350,20 @@
     <string name="keyboard_layout_notification_multiple_selected_title">Physical keyboards configured</string>
     <!-- Notification message when multiple keyboards with selected layouts have been connected the first time simultaneously [CHAR LIMIT=NOTIF_BODY] -->
     <string name="keyboard_layout_notification_multiple_selected_message">Tap to view keyboards</string>
+
+    <!-- Private profile label on a screen. This can be used as a tab label for this profile in tabbed views and can be used to represent the profile in sharing surfaces, etc. [CHAR LIMIT=20] -->
+    <string name="profile_label_private">Private</string>
+    <!-- Clone profile label on a screen. This can be used as a tab label for this profile in tabbed views and can be used to represent the profile in sharing surfaces, etc. [CHAR LIMIT=20] -->
+    <string name="profile_label_clone">Clone</string>
+    <!-- Work profile label on a screen. This can be used as a tab label for this profile in tabbed views and can be used to represent the profile in sharing surfaces, etc. [CHAR LIMIT=20] -->
+    <string name="profile_label_work">Work</string>
+    <!-- 2nd Work profile label on a screen in case a device has more than one work profiles. This can be used as a tab label for this profile in tabbed views and can be used to represent the profile in sharing surfaces, etc. [CHAR LIMIT=20] -->
+    <string name="profile_label_work_2">Work 2</string>
+    <!-- 3rd Work profile label on a screen in case a device has more than two work profiles. This can be used as a tab label for this profile in tabbed views and can be used to represent the profile in sharing surfaces, etc. [CHAR LIMIT=20] -->
+    <string name="profile_label_work_3">Work 3</string>
+    <!-- Test profile label on a screen. This can be used as a tab label for this profile in tabbed views and can be used to represent the profile in sharing surfaces, etc. [CHAR LIMIT=20]  -->
+    <string name="profile_label_test">Test</string>
+    <!-- Communal profile label on a screen. This can be used as a tab label for this profile in tabbed views and can be used to represent the profile in sharing surfaces, etc. [CHAR LIMIT=20] -->
+    <string name="profile_label_communal">Communal</string>
+
 </resources>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 24b39bc..38f1f67 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1086,6 +1086,13 @@
   <java-symbol type="string" name="managed_profile_label_badge_3" />
   <java-symbol type="string" name="clone_profile_label_badge" />
   <java-symbol type="string" name="private_profile_label_badge" />
+  <java-symbol type="string" name="profile_label_private" />
+  <java-symbol type="string" name="profile_label_clone" />
+  <java-symbol type="string" name="profile_label_work" />
+  <java-symbol type="string" name="profile_label_work_2" />
+  <java-symbol type="string" name="profile_label_work_3" />
+  <java-symbol type="string" name="profile_label_test" />
+  <java-symbol type="string" name="profile_label_communal" />
   <java-symbol type="string" name="mediasize_unknown_portrait" />
   <java-symbol type="string" name="mediasize_unknown_landscape" />
   <java-symbol type="string" name="mediasize_iso_a0" />
diff --git a/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java b/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java
index d47d789..1cdcb37 100644
--- a/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java
+++ b/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionTest.java
@@ -17,11 +17,15 @@
 package android.view.contentcapture;
 
 import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_STARTED;
+import static android.view.contentcapture.ContentCaptureSession.FLUSH_REASON_VIEW_TREE_APPEARED;
+import static android.view.contentcapture.ContentCaptureSession.FLUSH_REASON_VIEW_TREE_APPEARING;
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyZeroInteractions;
 
@@ -29,14 +33,20 @@
 import android.content.ContentCaptureOptions;
 import android.content.Context;
 import android.content.pm.ParceledListSlice;
+import android.graphics.Insets;
 import android.os.Handler;
-import android.os.Looper;
+import android.os.RemoteException;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.util.SparseArray;
+import android.view.View;
+import android.view.autofill.AutofillId;
 import android.view.contentprotection.ContentProtectionEventProcessor;
 
 import androidx.test.core.app.ApplicationProvider;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.SmallTest;
 
+import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -56,8 +66,9 @@
  * <p>Run with: {@code atest
  * FrameworksCoreTests:android.view.contentcapture.MainContentCaptureSessionTest}
  */
-@RunWith(AndroidJUnit4.class)
+@RunWith(AndroidTestingRunner.class)
 @SmallTest
+@TestableLooper.RunWithLooper
 public class MainContentCaptureSessionTest {
 
     private static final int BUFFER_SIZE = 100;
@@ -75,6 +86,8 @@
     private static final ContentCaptureManager.StrippedContext sStrippedContext =
             new ContentCaptureManager.StrippedContext(sContext);
 
+    private TestableLooper mTestableLooper;
+
     @Rule public final MockitoRule mMockitoRule = MockitoJUnit.rule();
 
     @Mock private IContentCaptureManager mMockSystemServerInterface;
@@ -83,12 +96,18 @@
 
     @Mock private IContentCaptureDirectManager mMockContentCaptureDirectManager;
 
+    @Before
+    public void setup() {
+        mTestableLooper = TestableLooper.get(this);
+    }
+
     @Test
     public void onSessionStarted_contentProtectionEnabled_processorCreated() {
         MainContentCaptureSession session = createSession();
         assertThat(session.mContentProtectionEventProcessor).isNull();
 
         session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null);
+        mTestableLooper.processAllMessages();
 
         assertThat(session.mContentProtectionEventProcessor).isNotNull();
     }
@@ -102,6 +121,7 @@
         session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
 
         session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null);
+        mTestableLooper.processAllMessages();
 
         assertThat(session.mContentProtectionEventProcessor).isNull();
         verifyZeroInteractions(mMockContentProtectionEventProcessor);
@@ -122,6 +142,7 @@
         session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
 
         session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null);
+        mTestableLooper.processAllMessages();
 
         assertThat(session.mContentProtectionEventProcessor).isNull();
         verifyZeroInteractions(mMockContentProtectionEventProcessor);
@@ -142,6 +163,7 @@
         session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
 
         session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null);
+        mTestableLooper.processAllMessages();
 
         assertThat(session.mContentProtectionEventProcessor).isNull();
         verifyZeroInteractions(mMockContentProtectionEventProcessor);
@@ -153,6 +175,7 @@
         session.mComponentName = null;
 
         session.onSessionStarted(/* resultCode= */ 0, /* binder= */ null);
+        mTestableLooper.processAllMessages();
 
         assertThat(session.mContentProtectionEventProcessor).isNull();
     }
@@ -166,6 +189,7 @@
         session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
 
         session.sendEvent(EVENT);
+        mTestableLooper.processAllMessages();
 
         verifyZeroInteractions(mMockContentProtectionEventProcessor);
         assertThat(session.mEvents).isNull();
@@ -180,6 +204,7 @@
         session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
 
         session.sendEvent(EVENT);
+        mTestableLooper.processAllMessages();
 
         verify(mMockContentProtectionEventProcessor).processEvent(EVENT);
         assertThat(session.mEvents).isNull();
@@ -194,6 +219,7 @@
         session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
 
         session.sendEvent(EVENT);
+        mTestableLooper.processAllMessages();
 
         verifyZeroInteractions(mMockContentProtectionEventProcessor);
         assertThat(session.mEvents).isNotNull();
@@ -206,6 +232,7 @@
         session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
 
         session.sendEvent(EVENT);
+        mTestableLooper.processAllMessages();
 
         verify(mMockContentProtectionEventProcessor).processEvent(EVENT);
         assertThat(session.mEvents).isNotNull();
@@ -220,6 +247,7 @@
                         /* enableContentProtectionReceiver= */ true);
 
         session.sendEvent(EVENT);
+        mTestableLooper.processAllMessages();
 
         verifyZeroInteractions(mMockContentProtectionEventProcessor);
         assertThat(session.mEvents).isNull();
@@ -236,6 +264,7 @@
         session.mDirectServiceInterface = mMockContentCaptureDirectManager;
 
         session.flush(REASON);
+        mTestableLooper.processAllMessages();
 
         verifyZeroInteractions(mMockContentProtectionEventProcessor);
         verifyZeroInteractions(mMockContentCaptureDirectManager);
@@ -252,6 +281,7 @@
         session.mDirectServiceInterface = mMockContentCaptureDirectManager;
 
         session.flush(REASON);
+        mTestableLooper.processAllMessages();
 
         verifyZeroInteractions(mMockContentProtectionEventProcessor);
         verifyZeroInteractions(mMockContentCaptureDirectManager);
@@ -269,6 +299,7 @@
         session.mDirectServiceInterface = mMockContentCaptureDirectManager;
 
         session.flush(REASON);
+        mTestableLooper.processAllMessages();
 
         verifyZeroInteractions(mMockContentProtectionEventProcessor);
         assertThat(session.mEvents).isEmpty();
@@ -286,6 +317,7 @@
         session.mDirectServiceInterface = mMockContentCaptureDirectManager;
 
         session.flush(REASON);
+        mTestableLooper.processAllMessages();
 
         verifyZeroInteractions(mMockContentProtectionEventProcessor);
         assertThat(session.mEvents).isEmpty();
@@ -298,6 +330,7 @@
         session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
 
         session.destroySession();
+        mTestableLooper.processAllMessages();
 
         verify(mMockSystemServerInterface).finishSession(anyInt());
         verifyZeroInteractions(mMockContentProtectionEventProcessor);
@@ -311,6 +344,7 @@
         session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
 
         session.resetSession(/* newState= */ 0);
+        mTestableLooper.processAllMessages();
 
         verifyZeroInteractions(mMockSystemServerInterface);
         verifyZeroInteractions(mMockContentProtectionEventProcessor);
@@ -318,6 +352,111 @@
         assertThat(session.mContentProtectionEventProcessor).isNull();
     }
 
+    @Test
+    @SuppressWarnings("GuardedBy")
+    public void notifyContentCaptureEvents_notStarted_ContentCaptureDisabled_ProtectionDisabled() {
+        ContentCaptureOptions options =
+                createOptions(
+                        /* enableContentCaptureReceiver= */ false,
+                        /* enableContentProtectionReceiver= */ false);
+        MainContentCaptureSession session = createSession(options);
+
+        notifyContentCaptureEvents(session);
+        mTestableLooper.processAllMessages();
+
+        verifyZeroInteractions(mMockContentCaptureDirectManager);
+        verifyZeroInteractions(mMockContentProtectionEventProcessor);
+        assertThat(session.mEvents).isNull();
+    }
+
+    @Test
+    @SuppressWarnings("GuardedBy")
+    public void notifyContentCaptureEvents_started_ContentCaptureDisabled_ProtectionDisabled() {
+        ContentCaptureOptions options =
+                createOptions(
+                        /* enableContentCaptureReceiver= */ false,
+                        /* enableContentProtectionReceiver= */ false);
+        MainContentCaptureSession session = createSession(options);
+
+        session.onSessionStarted(0x2, null);
+        notifyContentCaptureEvents(session);
+        mTestableLooper.processAllMessages();
+
+        verifyZeroInteractions(mMockContentCaptureDirectManager);
+        verifyZeroInteractions(mMockContentProtectionEventProcessor);
+        assertThat(session.mEvents).isNull();
+    }
+
+    @Test
+    @SuppressWarnings("GuardedBy")
+    public void notifyContentCaptureEvents_notStarted_ContentCaptureEnabled_ProtectionEnabled() {
+        ContentCaptureOptions options =
+                createOptions(
+                        /* enableContentCaptureReceiver= */ true,
+                        /* enableContentProtectionReceiver= */ true);
+        MainContentCaptureSession session = createSession(options);
+        session.mDirectServiceInterface = mMockContentCaptureDirectManager;
+        session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
+
+        notifyContentCaptureEvents(session);
+        mTestableLooper.processAllMessages();
+
+        verifyZeroInteractions(mMockContentCaptureDirectManager);
+        verifyZeroInteractions(mMockContentProtectionEventProcessor);
+        assertThat(session.mEvents).isNull();
+    }
+
+    @Test
+    @SuppressWarnings("GuardedBy")
+    public void notifyContentCaptureEvents_started_ContentCaptureEnabled_ProtectionEnabled()
+            throws RemoteException {
+        ContentCaptureOptions options =
+                createOptions(
+                        /* enableContentCaptureReceiver= */ true,
+                        /* enableContentProtectionReceiver= */ true);
+        MainContentCaptureSession session = createSession(options);
+        session.mDirectServiceInterface = mMockContentCaptureDirectManager;
+
+        session.onSessionStarted(0x2, null);
+        // Override the processor for interaction verification.
+        session.mContentProtectionEventProcessor = mMockContentProtectionEventProcessor;
+        notifyContentCaptureEvents(session);
+        mTestableLooper.processAllMessages();
+
+        // Force flush will happen twice.
+        verify(mMockContentCaptureDirectManager, times(1))
+                .sendEvents(any(), eq(FLUSH_REASON_VIEW_TREE_APPEARING), any());
+        verify(mMockContentCaptureDirectManager, times(1))
+                .sendEvents(any(), eq(FLUSH_REASON_VIEW_TREE_APPEARED), any());
+        // Other than the five view events, there will be two additional tree appearing events.
+        verify(mMockContentProtectionEventProcessor, times(7)).processEvent(any());
+        assertThat(session.mEvents).isEmpty();
+    }
+
+    /** Simulates the regular content capture events sequence. */
+    private void notifyContentCaptureEvents(final MainContentCaptureSession session) {
+        final ArrayList<Object> events = new ArrayList<>(
+                List.of(
+                        prepareView(session),
+                        prepareView(session),
+                        new AutofillId(0),
+                        prepareView(session),
+                        Insets.of(0, 0, 0, 0)
+                )
+        );
+
+        final SparseArray<ArrayList<Object>> contentCaptureEvents = new SparseArray<>();
+        contentCaptureEvents.set(session.getId(), events);
+
+        session.notifyContentCaptureEvents(contentCaptureEvents);
+    }
+
+    private View prepareView(final MainContentCaptureSession session) {
+        final View view = new View(sContext);
+        view.setContentCaptureSession(session);
+        return view;
+    }
+
     private static ContentCaptureOptions createOptions(
             boolean enableContentCaptureReceiver,
             ContentCaptureOptions.ContentProtectionOptions contentProtectionOptions) {
@@ -354,7 +493,7 @@
                 new MainContentCaptureSession(
                         sStrippedContext,
                         manager,
-                        new Handler(Looper.getMainLooper()),
+                        Handler.createAsync(mTestableLooper.getLooper()),
                         mMockSystemServerInterface);
         session.mComponentName = COMPONENT_NAME;
         return session;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 4a9ea6f..144555d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -21,7 +21,6 @@
 import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
 import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
-import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
 import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED
 import android.app.WindowConfiguration.WindowingMode
 import android.content.Context
@@ -321,24 +320,10 @@
     }
 
     /** Move a task with given `taskId` to fullscreen */
-    fun moveToFullscreen(taskId: Int) {
-        shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> moveToFullscreen(task) }
-    }
-
-    /** Move a task to fullscreen */
-    fun moveToFullscreen(task: RunningTaskInfo) {
-        KtProtoLog.v(
-            WM_SHELL_DESKTOP_MODE,
-            "DesktopTasksController: moveToFullscreen taskId=%d",
-            task.taskId
-        )
-
-        val wct = WindowContainerTransaction()
-        addMoveToFullscreenChanges(wct, task)
-        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
-            transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */)
-        } else {
-            shellTaskOrganizer.applyTransaction(wct)
+    fun moveToFullscreen(taskId: Int, windowDecor: DesktopModeWindowDecoration) {
+        shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task ->
+            windowDecor.incrementRelayoutBlock()
+            moveToFullscreenWithAnimation(task, task.positionInParent)
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
index dd6ca8d..03006f9 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java
@@ -428,7 +428,8 @@
                 if (isTaskInSplitScreen(mTaskId)) {
                     mSplitScreenController.moveTaskToFullscreen(mTaskId);
                 } else {
-                    mDesktopTasksController.ifPresent(c -> c.moveToFullscreen(mTaskId));
+                    mDesktopTasksController.ifPresent(c ->
+                            c.moveToFullscreen(mTaskId, mWindowDecorByTaskId.get(mTaskId)));
                 }
             } else if (id == R.id.split_screen_button) {
                 decoration.closeHandleMenu();
diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/splitscreen/AndroidTestTemplate.xml
index fdda597..05f937a 100644
--- a/libs/WindowManager/Shell/tests/flicker/splitscreen/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/AndroidTestTemplate.xml
@@ -95,6 +95,8 @@
         <option name="pull-pattern-keys" value="perfetto_file_path"/>
         <option name="directory-keys"
                 value="/data/user/0/com.android.wm.shell.flicker.splitscreen/files"/>
+        <option name="directory-keys"
+                value="/data/user/0/com.android.wm.shell.flicker.service/files"/>
         <option name="collect-on-run-ended-only" value="true"/>
         <option name="clean-up" value="true"/>
     </metrics_collector>
diff --git a/libs/WindowManager/Shell/tests/flicker/splitscreen/trace_config/trace_config.textproto b/libs/WindowManager/Shell/tests/flicker/splitscreen/trace_config/trace_config.textproto
index b55f4ec..67316d2 100644
--- a/libs/WindowManager/Shell/tests/flicker/splitscreen/trace_config/trace_config.textproto
+++ b/libs/WindowManager/Shell/tests/flicker/splitscreen/trace_config/trace_config.textproto
@@ -63,6 +63,7 @@
       atrace_categories: "sched_process_exit"
       atrace_apps: "com.android.server.wm.flicker.testapp"
       atrace_apps: "com.android.systemui"
+      atrace_apps: "com.android.wm.shell.flicker.service"
       atrace_apps: "com.android.wm.shell.flicker.splitscreen"
       atrace_apps: "com.google.android.apps.nexuslauncher"
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index fde6acb..94c862b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -63,6 +63,7 @@
 import com.android.wm.shell.transition.OneShotRemoteHandler
 import com.android.wm.shell.transition.Transitions
 import com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS
+import com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_DESKTOP_MODE
 import com.android.wm.shell.transition.Transitions.TransitionHandler
 import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration
 import com.google.common.truth.Truth.assertThat
@@ -392,8 +393,8 @@
     fun moveToFullscreen_displayFullscreen_windowingModeSetToUndefined() {
         val task = setUpFreeformTask()
         task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FULLSCREEN
-        controller.moveToFullscreen(task)
-        val wct = getLatestWct(type = TRANSIT_CHANGE)
+        controller.moveToFullscreen(task.taskId, desktopModeWindowDecoration)
+        val wct = getLatestExitDesktopWct()
         assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
             .isEqualTo(WINDOWING_MODE_UNDEFINED)
     }
@@ -402,15 +403,15 @@
     fun moveToFullscreen_displayFreeform_windowingModeSetToFullscreen() {
         val task = setUpFreeformTask()
         task.configuration.windowConfiguration.displayWindowingMode = WINDOWING_MODE_FREEFORM
-        controller.moveToFullscreen(task)
-        val wct = getLatestWct(type = TRANSIT_CHANGE)
+        controller.moveToFullscreen(task.taskId, desktopModeWindowDecoration)
+        val wct = getLatestExitDesktopWct()
         assertThat(wct.changes[task.token.asBinder()]?.windowingMode)
                 .isEqualTo(WINDOWING_MODE_FULLSCREEN)
     }
 
     @Test
     fun moveToFullscreen_nonExistentTask_doesNothing() {
-        controller.moveToFullscreen(999)
+        controller.moveToFullscreen(999, desktopModeWindowDecoration)
         verifyWCTNotExecuted()
     }
 
@@ -419,9 +420,9 @@
         val taskDefaultDisplay = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
         val taskSecondDisplay = setUpFreeformTask(displayId = SECOND_DISPLAY)
 
-        controller.moveToFullscreen(taskDefaultDisplay)
+        controller.moveToFullscreen(taskDefaultDisplay.taskId, desktopModeWindowDecoration)
 
-        with(getLatestWct(type = TRANSIT_CHANGE)) {
+        with(getLatestExitDesktopWct()) {
             assertThat(changes.keys).contains(taskDefaultDisplay.token.asBinder())
             assertThat(changes.keys).doesNotContain(taskSecondDisplay.token.asBinder())
         }
@@ -808,6 +809,17 @@
         return arg.value
     }
 
+    private fun getLatestExitDesktopWct(): WindowContainerTransaction {
+        val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java)
+        if (ENABLE_SHELL_TRANSITIONS) {
+            verify(exitDesktopTransitionHandler)
+                    .startTransition(eq(TRANSIT_EXIT_DESKTOP_MODE), arg.capture(), any(), any())
+        } else {
+            verify(shellTaskOrganizer).applyTransaction(arg.capture())
+        }
+        return arg.value
+    }
+
     private fun verifyWCTNotExecuted() {
         if (ENABLE_SHELL_TRANSITIONS) {
             verify(transitions, never()).startTransition(anyInt(), any(), isNull())
diff --git a/media/TEST_MAPPING b/media/TEST_MAPPING
index a9da832..8f5f1f6 100644
--- a/media/TEST_MAPPING
+++ b/media/TEST_MAPPING
@@ -49,6 +49,18 @@
         {"exclude-annotation": "org.junit.Ignore"}
       ]
     }
+  ],
+  "postsubmit": [
+    {
+      "file_patterns": [
+        "[^/]*(LoudnessCodec)[^/]*\\.java"
+      ],
+      "name": "LoudnessCodecApiTest",
+      "options": [
+        {
+          "include-annotation": "android.platform.test.annotations.Presubmit"
+        }
+      ]
+    }
   ]
 }
-
diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java
index 9f63dfd..9ae6f8d 100644
--- a/media/java/android/media/AudioManager.java
+++ b/media/java/android/media/AudioManager.java
@@ -19,8 +19,6 @@
 import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
 import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO;
 import static android.content.Context.DEVICE_ID_DEFAULT;
-import static android.media.audio.Flags.autoPublicVolumeApiHardening;
-import static android.media.audio.Flags.FLAG_LOUDNESS_CONFIGURATOR_API;
 import static android.media.audio.Flags.FLAG_FOCUS_FREEZE_TEST_API;
 
 import android.Manifest;
@@ -2924,33 +2922,6 @@
     }
 
     //====================================================================
-    // Loudness management
-    private final Object mLoudnessCodecLock = new Object();
-
-    @GuardedBy("mLoudnessCodecLock")
-    private LoudnessCodecDispatcher mLoudnessCodecDispatcher = null;
-
-    /**
-     * Creates a new instance of {@link LoudnessCodecConfigurator}.
-     * @return the {@link LoudnessCodecConfigurator} instance
-     *
-     * TODO: remove hide once API is final
-     * @hide
-     */
-    @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
-    public @NonNull LoudnessCodecConfigurator createLoudnessCodecConfigurator() {
-        LoudnessCodecConfigurator configurator;
-        synchronized (mLoudnessCodecLock) {
-            // initialize lazily
-            if (mLoudnessCodecDispatcher == null) {
-                mLoudnessCodecDispatcher = new LoudnessCodecDispatcher(this);
-            }
-            configurator = mLoudnessCodecDispatcher.createLoudnessCodecConfigurator();
-        }
-        return configurator;
-    }
-
-    //====================================================================
     // Bluetooth SCO control
     /**
      * Sticky broadcast intent action indicating that the Bluetooth SCO audio
diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java
index 61b5fd5..367b38a 100644
--- a/media/java/android/media/AudioSystem.java
+++ b/media/java/android/media/AudioSystem.java
@@ -2462,6 +2462,8 @@
     public static final int PLATFORM_VOICE = 1;
     /** @hide The platform is a television or a set-top box */
     public static final int PLATFORM_TELEVISION = 2;
+    /** @hide The platform is automotive */
+    public static final int PLATFORM_AUTOMOTIVE = 3;
 
     /**
      * @hide
@@ -2478,6 +2480,9 @@
             return PLATFORM_VOICE;
         } else if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
             return PLATFORM_TELEVISION;
+        } else if (context.getPackageManager().hasSystemFeature(
+                PackageManager.FEATURE_AUTOMOTIVE)) {
+            return PLATFORM_AUTOMOTIVE;
         } else {
             return PLATFORM_DEFAULT;
         }
diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl
index b4ca485..42400d1 100644
--- a/media/java/android/media/IAudioService.aidl
+++ b/media/java/android/media/IAudioService.aidl
@@ -52,7 +52,7 @@
 import android.media.ISpatializerOutputCallback;
 import android.media.IStreamAliasingDispatcher;
 import android.media.IVolumeController;
-import android.media.LoudnessCodecFormat;
+import android.media.LoudnessCodecInfo;
 import android.media.PlayerBase;
 import android.media.VolumeInfo;
 import android.media.VolumePolicy;
@@ -731,15 +731,13 @@
 
     void unregisterLoudnessCodecUpdatesDispatcher(in ILoudnessCodecUpdatesDispatcher dispatcher);
 
-    oneway void startLoudnessCodecUpdates(in int piid);
+    oneway void startLoudnessCodecUpdates(int piid, in List<LoudnessCodecInfo> codecInfoSet);
 
-    oneway void stopLoudnessCodecUpdates(in int piid);
+    oneway void stopLoudnessCodecUpdates(int piid);
 
-    oneway void addLoudnesssCodecFormat(in int piid, in LoudnessCodecFormat format);
+    oneway void addLoudnessCodecInfo(int piid, in LoudnessCodecInfo codecInfo);
 
-    oneway void addLoudnesssCodecFormatList(in int piid, in List<LoudnessCodecFormat> format);
+    oneway void removeLoudnessCodecInfo(int piid, in LoudnessCodecInfo codecInfo);
 
-    oneway void removeLoudnessCodecFormat(in int piid, in LoudnessCodecFormat format);
-
-    PersistableBundle getLoudnessParams(in int piid, in LoudnessCodecFormat format);
+    PersistableBundle getLoudnessParams(int piid, in LoudnessCodecInfo codecInfo);
 }
diff --git a/media/java/android/media/LoudnessCodecConfigurator.java b/media/java/android/media/LoudnessCodecConfigurator.java
index 409abc2..92f3372 100644
--- a/media/java/android/media/LoudnessCodecConfigurator.java
+++ b/media/java/android/media/LoudnessCodecConfigurator.java
@@ -16,6 +16,9 @@
 
 package android.media;
 
+import static android.media.AudioPlaybackConfiguration.PLAYER_PIID_INVALID;
+import static android.media.LoudnessCodecInfo.CodecMetadataType.CODEC_METADATA_TYPE_MPEG_4;
+import static android.media.LoudnessCodecInfo.CodecMetadataType.CODEC_METADATA_TYPE_MPEG_D;
 import static android.media.audio.Flags.FLAG_LOUDNESS_CONFIGURATOR_API;
 
 import android.annotation.CallbackExecutor;
@@ -23,21 +26,27 @@
 import android.os.Bundle;
 import android.util.Log;
 
+import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 
-import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * Class for getting recommended loudness parameter updates for audio decoders, according to the
  * encoded format and current audio routing. Those updates can be automatically applied to the
  * {@link MediaCodec} instance(s), or be provided to the user. The codec loudness management
- * updates are defined by the CTA-2075 standard.
+ * parameter updates are defined by the CTA-2075 standard.
  * <p>A new object should be instantiated for each {@link AudioTrack} with the help
- * of {@link AudioManager#createLoudnessCodecConfigurator()}.
+ * of {@link #create()} or {@link #create(Executor, OnLoudnessCodecUpdateListener)}.
  *
  * TODO: remove hide once API is final
  * @hide
@@ -81,120 +90,255 @@
 
     @NonNull private final LoudnessCodecDispatcher mLcDispatcher;
 
+    private final Object mConfiguratorLock = new Object();
+
+    @GuardedBy("mConfiguratorLock")
     private AudioTrack mAudioTrack;
 
-    private final List<MediaCodec> mMediaCodecs = new ArrayList<>();
+    @GuardedBy("mConfiguratorLock")
+    private final Executor mExecutor;
 
-    /** @hide */
-    protected LoudnessCodecConfigurator(@NonNull LoudnessCodecDispatcher lcDispatcher) {
-        mLcDispatcher = Objects.requireNonNull(lcDispatcher);
-    }
+    @GuardedBy("mConfiguratorLock")
+    private final OnLoudnessCodecUpdateListener mListener;
 
+    @GuardedBy("mConfiguratorLock")
+    private final HashMap<LoudnessCodecInfo, Set<MediaCodec>> mMediaCodecs = new HashMap<>();
 
     /**
-     * Starts receiving asynchronous loudness updates and registers the listener for
-     * receiving {@link MediaCodec} loudness parameter updates.
-     * <p>This method should be called before {@link #startLoudnessCodecUpdates()} or
-     * after {@link #stopLoudnessCodecUpdates()}.
+     * Creates a new instance of {@link LoudnessCodecConfigurator}
+     *
+     * <p>This method should be used when the client does not need to alter the
+     * codec loudness parameters before they are applied to the audio decoders.
+     * Otherwise, use {@link #create(Executor, OnLoudnessCodecUpdateListener)}.
+     *
+     * @return the {@link LoudnessCodecConfigurator} instance
+     *
+     * TODO: remove hide once API is final
+     * @hide
+     */
+    @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
+    public static @NonNull LoudnessCodecConfigurator create() {
+        return new LoudnessCodecConfigurator(new LoudnessCodecDispatcher(AudioManager.getService()),
+                Executors.newSingleThreadExecutor(), new OnLoudnessCodecUpdateListener() {});
+    }
+
+    /**
+     * Creates a new instance of {@link LoudnessCodecConfigurator}
+     *
+     * <p>This method should be used when the client wants to alter the codec
+     * loudness parameters before they are applied to the audio decoders.
+     * Otherwise, use {@link #create()}.
      *
      * @param executor {@link Executor} to handle the callbacks
-     * @param listener used to receive updates
+     * @param listener used for receiving updates
      *
-     * @return {@code true} if there is at least one {@link MediaCodec} and
-     * {@link AudioTrack} set and the user can expect receiving updates.
+     * @return the {@link LoudnessCodecConfigurator} instance
      *
      * TODO: remove hide once API is final
      * @hide
      */
     @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
-    public boolean startLoudnessCodecUpdates(@NonNull @CallbackExecutor Executor executor,
-                                             @NonNull OnLoudnessCodecUpdateListener listener) {
-        Objects.requireNonNull(executor,
-                "Executor must not be null");
-        Objects.requireNonNull(listener,
-                "OnLoudnessCodecUpdateListener must not be null");
-        mLcDispatcher.addLoudnessCodecListener(this, executor, listener);
+    public static @NonNull LoudnessCodecConfigurator create(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OnLoudnessCodecUpdateListener listener) {
+        Objects.requireNonNull(executor, "Executor cannot be null");
+        Objects.requireNonNull(listener, "OnLoudnessCodecUpdateListener cannot be null");
 
-        return checkStartLoudnessConfigurator();
+        return new LoudnessCodecConfigurator(new LoudnessCodecDispatcher(AudioManager.getService()),
+                executor, listener);
     }
 
     /**
-     * Starts receiving asynchronous loudness updates.
-     * <p>The registered MediaCodecs will be updated automatically without any client
-     * callbacks.
+     * Creates a new instance of {@link LoudnessCodecConfigurator}
      *
-     * @return {@code true} if there is at least one MediaCodec and AudioTrack set
-     * (see {@link #setAudioTrack(AudioTrack)}, {@link #addMediaCodec(MediaCodec)})
-     * and the user can expect receiving updates.
+     * <p>This method should be used only in testing
      *
-     * TODO: remove hide once API is final
+     * @param service interface for communicating with AudioService
+     * @param executor {@link Executor} to handle the callbacks
+     * @param listener used for receiving updates
+     *
+     * @return the {@link LoudnessCodecConfigurator} instance
+     *
      * @hide
      */
-    @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
-    public boolean startLoudnessCodecUpdates() {
-        mLcDispatcher.addLoudnessCodecListener(this,
-                Executors.newSingleThreadExecutor(), new OnLoudnessCodecUpdateListener() {});
-        return checkStartLoudnessConfigurator();
+    public static @NonNull LoudnessCodecConfigurator createForTesting(
+            @NonNull IAudioService service,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OnLoudnessCodecUpdateListener listener) {
+        Objects.requireNonNull(service, "IAudioService cannot be null");
+        Objects.requireNonNull(executor, "Executor cannot be null");
+        Objects.requireNonNull(listener, "OnLoudnessCodecUpdateListener cannot be null");
+
+        return new LoudnessCodecConfigurator(new LoudnessCodecDispatcher(service),
+                executor, listener);
+    }
+
+    /** @hide */
+    private LoudnessCodecConfigurator(@NonNull LoudnessCodecDispatcher lcDispatcher,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull OnLoudnessCodecUpdateListener listener) {
+        mLcDispatcher = Objects.requireNonNull(lcDispatcher, "Dispatcher cannot be null");
+        mExecutor = Objects.requireNonNull(executor, "Executor cannot be null");
+        mListener = Objects.requireNonNull(listener,
+                "OnLoudnessCodecUpdateListener cannot be null");
     }
 
     /**
-     * Stops receiving asynchronous loudness updates.
+     * Sets the {@link AudioTrack} and starts receiving asynchronous updates for
+     * the registered {@link MediaCodec}s (see {@link #addMediaCodec(MediaCodec)})
+     *
+     * <p>The AudioTrack should be the one that receives audio data from the
+     * added audio decoders and is used to determine the device routing on which
+     * the audio streaming will take place. This will directly influence the
+     * loudness parameters.
+     * <p>After calling this method the framework will compute the initial set of
+     * parameters which will be applied to the registered codecs/returned to the
+     * listener for modification.
+     *
+     * @param audioTrack the track that will receive audio data from the provided
+     *                   audio decoders. In case this is {@code null} this
+     *                   method will have the effect of clearing the existing set
+     *                   {@link AudioTrack} and will stop receiving asynchronous
+     *                   loudness updates
      *
      * TODO: remove hide once API is final
      * @hide
      */
     @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
-    public void stopLoudnessCodecUpdates() {
-        mLcDispatcher.removeLoudnessCodecListener(this);
+    public void setAudioTrack(AudioTrack audioTrack) {
+        List<LoudnessCodecInfo> codecInfos;
+        int piid = PLAYER_PIID_INVALID;
+        int oldPiid = PLAYER_PIID_INVALID;
+        synchronized (mConfiguratorLock) {
+            if (mAudioTrack != null && mAudioTrack == audioTrack) {
+                Log.v(TAG, "Loudness configurator already started for piid: "
+                        + mAudioTrack.getPlayerIId());
+                return;
+            }
+
+            codecInfos = getLoudnessCodecInfoList_l();
+            if (mAudioTrack != null) {
+                oldPiid = mAudioTrack.getPlayerIId();
+                mLcDispatcher.removeLoudnessCodecListener(this);
+            }
+            if (audioTrack != null) {
+                piid = audioTrack.getPlayerIId();
+                mLcDispatcher.addLoudnessCodecListener(this, mExecutor, mListener);
+            }
+
+            mAudioTrack = audioTrack;
+        }
+
+        if (oldPiid != PLAYER_PIID_INVALID) {
+            Log.v(TAG, "Loudness configurator stopping updates for piid: " + oldPiid);
+            mLcDispatcher.stopLoudnessCodecUpdates(oldPiid);
+        }
+        if (piid != PLAYER_PIID_INVALID) {
+            Log.v(TAG, "Loudness configurator starting updates for piid: " + piid);
+            mLcDispatcher.startLoudnessCodecUpdates(piid, codecInfos);
+        }
     }
 
     /**
      * Adds a new {@link MediaCodec} that will stream data to an {@link AudioTrack}
-     * which is registered through {@link #setAudioTrack(AudioTrack)}.
+     * which the client sets
+     * (see {@link LoudnessCodecConfigurator#setAudioTrack(AudioTrack)}).
+     *
+     * <p>This method can be called while asynchronous updates are live.
+     *
+     * <p>No new element will be added if the passed {@code mediaCodec} was
+     * previously added.
+     *
+     * @param mediaCodec the codec to start receiving asynchronous loudness
+     *                   updates
      *
      * TODO: remove hide once API is final
      * @hide
      */
     @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
     public void addMediaCodec(@NonNull MediaCodec mediaCodec) {
-        mMediaCodecs.add(Objects.requireNonNull(mediaCodec,
-                "MediaCodec for addMediaCodec must not be null"));
+        final MediaCodec mc = Objects.requireNonNull(mediaCodec,
+                "MediaCodec for addMediaCodec cannot be null");
+        int piid = PLAYER_PIID_INVALID;
+        final LoudnessCodecInfo mcInfo = getCodecInfo(mc);
+
+        if (mcInfo != null) {
+            synchronized (mConfiguratorLock) {
+                final AtomicBoolean containsCodec = new AtomicBoolean(false);
+                Set<MediaCodec> newSet = mMediaCodecs.computeIfPresent(mcInfo, (info, codecSet) -> {
+                    containsCodec.set(!codecSet.add(mc));
+                    return codecSet;
+                });
+                if (newSet == null) {
+                    newSet = new HashSet<>();
+                    newSet.add(mc);
+                    mMediaCodecs.put(mcInfo, newSet);
+                }
+                if (containsCodec.get()) {
+                    Log.v(TAG, "Loudness configurator already added media codec " + mediaCodec);
+                    return;
+                }
+                if (mAudioTrack != null) {
+                    piid = mAudioTrack.getPlayerIId();
+                }
+            }
+
+            if (piid != PLAYER_PIID_INVALID) {
+                mLcDispatcher.addLoudnessCodecInfo(piid, mcInfo);
+            }
+        }
     }
 
     /**
      * Removes the {@link MediaCodec} from receiving loudness updates.
      *
+     * <p>This method can be called while asynchronous updates are live.
+     *
+     * <p>No elements will be removed if the passed mediaCodec was not added before.
+     *
+     * @param mediaCodec the element to remove for receiving asynchronous updates
+     *
      * TODO: remove hide once API is final
      * @hide
      */
     @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
     public void removeMediaCodec(@NonNull MediaCodec mediaCodec) {
-        mMediaCodecs.remove(Objects.requireNonNull(mediaCodec,
-                "MediaCodec for removeMediaCodec must not be null"));
+        int piid = PLAYER_PIID_INVALID;
+        LoudnessCodecInfo mcInfo;
+        AtomicBoolean removed = new AtomicBoolean(false);
+
+        mcInfo = getCodecInfo(Objects.requireNonNull(mediaCodec,
+                "MediaCodec for removeMediaCodec cannot be null"));
+
+        if (mcInfo != null) {
+            synchronized (mConfiguratorLock) {
+                if (mAudioTrack != null) {
+                    piid = mAudioTrack.getPlayerIId();
+                }
+                mMediaCodecs.computeIfPresent(mcInfo, (format, mcs) -> {
+                    removed.set(mcs.remove(mediaCodec));
+                    if (mcs.isEmpty()) {
+                        // remove the entry
+                        return null;
+                    }
+                    return mcs;
+                });
+            }
+
+            if (piid != PLAYER_PIID_INVALID && removed.get()) {
+                mLcDispatcher.removeLoudnessCodecInfo(piid, mcInfo);
+            }
+        }
     }
 
     /**
-     * Sets the {@link AudioTrack} that can receive audio data from the added
-     * {@link MediaCodec}'s. The {@link AudioTrack} is used to determine the devices
-     * on which the streaming will take place and hence will directly influence the
-     * loudness params.
-     * <p>Should be called before starting the loudness updates
-     * (see {@link #startLoudnessCodecUpdates()},
-     * {@link #startLoudnessCodecUpdates(Executor, OnLoudnessCodecUpdateListener)})
+     * Gets synchronous loudness updates when no listener is required. The provided
+     * {@link MediaCodec} streams audio data to the passed {@link AudioTrack}.
      *
-     * TODO: remove hide once API is final
-     * @hide
-     */
-    @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
-    public void setAudioTrack(@NonNull AudioTrack audioTrack) {
-        mAudioTrack = Objects.requireNonNull(audioTrack,
-                "AudioTrack for setAudioTrack must not be null");
-    }
-
-    /**
-     * Gets synchronous loudness updates when no listener is required and at least one
-     * {@link MediaCodec} which streams to a registered {@link AudioTrack} is set.
-     * Otherwise, an empty {@link Bundle} will be returned.
+     * @param audioTrack track that receives audio data from the passed
+     *                   {@link MediaCodec}
+     * @param mediaCodec codec that decodes loudness annotated data for the passed
+     *                   {@link AudioTrack}
      *
      * @return the {@link Bundle} containing the current loudness parameters. Caller is
      * responsible to update the {@link MediaCodec}
@@ -204,22 +348,89 @@
      */
     @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API)
     @NonNull
-    public Bundle getLoudnessCodecParams(@NonNull MediaCodec mediaCodec) {
-        // TODO: implement synchronous loudness params updates
-        return new Bundle();
+    public Bundle getLoudnessCodecParams(@NonNull AudioTrack audioTrack,
+            @NonNull MediaCodec mediaCodec) {
+        Objects.requireNonNull(audioTrack, "Passed audio track cannot be null");
+
+        LoudnessCodecInfo codecInfo = getCodecInfo(mediaCodec);
+        if (codecInfo == null) {
+            return new Bundle();
+        }
+
+        return mLcDispatcher.getLoudnessCodecParams(audioTrack.getPlayerIId(), codecInfo);
     }
 
-    private boolean checkStartLoudnessConfigurator() {
-        if (mAudioTrack == null) {
-            Log.w(TAG, "Cannot start loudness configurator without an AudioTrack");
-            return false;
+    /** @hide */
+    /*package*/ int getAssignedTrackPiid() {
+        int piid = PLAYER_PIID_INVALID;
+
+        synchronized (mConfiguratorLock) {
+            if (mAudioTrack == null) {
+                return piid;
+            }
+            piid = mAudioTrack.getPlayerIId();
         }
 
-        if (mMediaCodecs.isEmpty()) {
-            Log.w(TAG, "Cannot start loudness configurator without at least one MediaCodec");
-            return false;
+        return piid;
+    }
+
+    /** @hide */
+    /*package*/ List<MediaCodec> getRegisteredMediaCodecList() {
+        synchronized (mConfiguratorLock) {
+            return mMediaCodecs.values().stream().flatMap(Collection::stream).toList();
+        }
+    }
+
+    @GuardedBy("mConfiguratorLock")
+    private List<LoudnessCodecInfo> getLoudnessCodecInfoList_l() {
+        return mMediaCodecs.values().stream().flatMap(listMc -> listMc.stream().map(
+                LoudnessCodecConfigurator::getCodecInfo)).toList();
+    }
+
+    @Nullable
+    private static LoudnessCodecInfo getCodecInfo(@NonNull MediaCodec mediaCodec) {
+        LoudnessCodecInfo lci = new LoudnessCodecInfo();
+        final MediaCodecInfo codecInfo = mediaCodec.getCodecInfo();
+        if (codecInfo.isEncoder()) {
+            // loudness info only for decoders
+            Log.w(TAG, "MediaCodec used for encoding does not support loudness annotation");
+            return null;
         }
 
-        return true;
+        final MediaFormat inputFormat = mediaCodec.getInputFormat();
+        final String mimeType = inputFormat.getString(MediaFormat.KEY_MIME);
+        if (MediaFormat.MIMETYPE_AUDIO_AAC.equalsIgnoreCase(mimeType)) {
+            // check both KEY_AAC_PROFILE and KEY_PROFILE as some codecs may only recognize one of
+            // these two keys
+            int aacProfile = -1;
+            int profile = -1;
+            try {
+                aacProfile = inputFormat.getInteger(MediaFormat.KEY_AAC_PROFILE);
+            } catch (NullPointerException e) {
+                // does not contain KEY_AAC_PROFILE. do nothing
+            }
+            try {
+                profile = inputFormat.getInteger(MediaFormat.KEY_PROFILE);
+            } catch (NullPointerException e) {
+                // does not contain KEY_PROFILE. do nothing
+            }
+            if (aacProfile == MediaCodecInfo.CodecProfileLevel.AACObjectXHE
+                    || profile == MediaCodecInfo.CodecProfileLevel.AACObjectXHE) {
+                lci.metadataType = CODEC_METADATA_TYPE_MPEG_D;
+            } else {
+                lci.metadataType = CODEC_METADATA_TYPE_MPEG_4;
+            }
+        } else {
+            Log.w(TAG, "MediaCodec mime type not supported for loudness annotation");
+            return null;
+        }
+
+        final MediaFormat outputFormat = mediaCodec.getOutputFormat();
+        lci.isDownmixing = outputFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
+                < inputFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
+
+        lci.mediaCodecHashCode = mediaCodec.hashCode();
+
+        return lci;
     }
 }
diff --git a/media/java/android/media/LoudnessCodecDispatcher.java b/media/java/android/media/LoudnessCodecDispatcher.java
index fc5c354..be881b1 100644
--- a/media/java/android/media/LoudnessCodecDispatcher.java
+++ b/media/java/android/media/LoudnessCodecDispatcher.java
@@ -16,94 +16,217 @@
 
 package android.media;
 
+import static android.media.MediaFormat.KEY_AAC_DRC_EFFECT_TYPE;
+import static android.media.MediaFormat.KEY_AAC_DRC_HEAVY_COMPRESSION;
+import static android.media.MediaFormat.KEY_AAC_DRC_TARGET_REFERENCE_LEVEL;
+
 import android.annotation.CallbackExecutor;
 import android.media.LoudnessCodecConfigurator.OnLoudnessCodecUpdateListener;
+import android.os.Bundle;
 import android.os.PersistableBundle;
 import android.os.RemoteException;
+import android.util.Log;
 
 import androidx.annotation.NonNull;
 
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map.Entry;
 import java.util.Objects;
 import java.util.concurrent.Executor;
 
 /**
  * Class used to handle the loudness related communication with the audio service.
+ *
  * @hide
  */
-public class LoudnessCodecDispatcher {
-    private final class LoudnessCodecUpdatesDispatcherStub
-            extends ILoudnessCodecUpdatesDispatcher.Stub
-            implements CallbackUtil.DispatcherStub {
+public class LoudnessCodecDispatcher implements CallbackUtil.DispatcherStub {
+    private static final String TAG = "LoudnessCodecDispatcher";
+
+    private static final boolean DEBUG = false;
+
+    private static final class LoudnessCodecUpdatesDispatcherStub
+            extends ILoudnessCodecUpdatesDispatcher.Stub {
+        private static LoudnessCodecUpdatesDispatcherStub sLoudnessCodecStub;
+
+        private final CallbackUtil.LazyListenerManager<OnLoudnessCodecUpdateListener>
+                mLoudnessListenerMgr = new CallbackUtil.LazyListenerManager<>();
+
+        private final HashMap<OnLoudnessCodecUpdateListener, LoudnessCodecConfigurator>
+                mConfiguratorListener = new HashMap<>();
+
+        public static synchronized LoudnessCodecUpdatesDispatcherStub getInstance() {
+            if (sLoudnessCodecStub == null) {
+                sLoudnessCodecStub = new LoudnessCodecUpdatesDispatcherStub();
+            }
+            return sLoudnessCodecStub;
+        }
+
+        private LoudnessCodecUpdatesDispatcherStub() {}
+
         @Override
         public void dispatchLoudnessCodecParameterChange(int piid, PersistableBundle params) {
             mLoudnessListenerMgr.callListeners(listener ->
-                    mConfiguratorListener.computeIfPresent(listener, (l, c) -> {
-                        // TODO: send the bundle for the user to update
-                        return c;
+                    mConfiguratorListener.computeIfPresent(listener, (l, lcConfig) -> {
+                        // send the appropriate bundle for the user to update
+                        if (lcConfig.getAssignedTrackPiid() == piid) {
+                            final List<MediaCodec> mediaCodecs =
+                                    lcConfig.getRegisteredMediaCodecList();
+                            for (MediaCodec mediaCodec : mediaCodecs) {
+                                final String infoKey = Integer.toString(mediaCodec.hashCode());
+                                if (params.containsKey(infoKey)) {
+                                    Bundle bundle = new Bundle(
+                                            params.getPersistableBundle(infoKey));
+                                    if (DEBUG) {
+                                        Log.d(TAG,
+                                                "Received for piid " + piid + " bundle: " + bundle);
+                                    }
+                                    bundle =
+                                            LoudnessCodecUpdatesDispatcherStub.filterLoudnessParams(
+                                                    l.onLoudnessCodecUpdate(mediaCodec, bundle));
+                                    if (DEBUG) {
+                                        Log.d(TAG, "User changed for piid " + piid
+                                                + " to filtered bundle: " + bundle);
+                                    }
+
+                                    if (!bundle.isDefinitelyEmpty()) {
+                                        mediaCodec.setParameters(bundle);
+                                    }
+                                }
+                            }
+                        }
+
+                        return lcConfig;
                     }));
         }
 
-        @Override
-        public void register(boolean register) {
-            try {
-                if (register) {
-                    mAm.getService().registerLoudnessCodecUpdatesDispatcher(this);
-                } else {
-                    mAm.getService().unregisterLoudnessCodecUpdatesDispatcher(this);
+        private static Bundle filterLoudnessParams(Bundle bundle) {
+            Bundle filteredBundle = new Bundle();
+
+            if (bundle.containsKey(KEY_AAC_DRC_TARGET_REFERENCE_LEVEL)) {
+                filteredBundle.putInt(KEY_AAC_DRC_TARGET_REFERENCE_LEVEL,
+                        bundle.getInt(KEY_AAC_DRC_TARGET_REFERENCE_LEVEL));
+            }
+            if (bundle.containsKey(KEY_AAC_DRC_HEAVY_COMPRESSION)) {
+                filteredBundle.putInt(KEY_AAC_DRC_HEAVY_COMPRESSION,
+                        bundle.getInt(KEY_AAC_DRC_HEAVY_COMPRESSION));
+            }
+            if (bundle.containsKey(KEY_AAC_DRC_EFFECT_TYPE)) {
+                filteredBundle.putInt(KEY_AAC_DRC_EFFECT_TYPE,
+                        bundle.getInt(KEY_AAC_DRC_EFFECT_TYPE));
+            }
+
+            return filteredBundle;
+        }
+
+        void addLoudnessCodecListener(@NonNull CallbackUtil.DispatcherStub dispatcher,
+                @NonNull LoudnessCodecConfigurator configurator,
+                @NonNull @CallbackExecutor Executor executor,
+                @NonNull OnLoudnessCodecUpdateListener listener) {
+            Objects.requireNonNull(configurator);
+            Objects.requireNonNull(executor);
+            Objects.requireNonNull(listener);
+
+            mLoudnessListenerMgr.addListener(
+                    executor, listener, "addLoudnessCodecListener",
+                    () -> dispatcher);
+            mConfiguratorListener.put(listener, configurator);
+        }
+
+        void removeLoudnessCodecListener(@NonNull LoudnessCodecConfigurator configurator) {
+            Objects.requireNonNull(configurator);
+
+            for (Entry<OnLoudnessCodecUpdateListener, LoudnessCodecConfigurator> e :
+                    mConfiguratorListener.entrySet()) {
+                if (e.getValue() == configurator) {
+                    final OnLoudnessCodecUpdateListener listener = e.getKey();
+                    mConfiguratorListener.remove(listener);
+                    mLoudnessListenerMgr.removeListener(listener, "removeLoudnessCodecListener");
+                    break;
                 }
-            } catch (RemoteException e) {
-                e.rethrowFromSystemServer();
             }
         }
     }
 
-    private final CallbackUtil.LazyListenerManager<OnLoudnessCodecUpdateListener>
-            mLoudnessListenerMgr = new CallbackUtil.LazyListenerManager<>();
-
-    @NonNull private final LoudnessCodecUpdatesDispatcherStub mLoudnessCodecStub;
-
-    private final HashMap<OnLoudnessCodecUpdateListener, LoudnessCodecConfigurator>
-            mConfiguratorListener = new HashMap<>();
-
-    @NonNull private final AudioManager mAm;
-
-    protected LoudnessCodecDispatcher(@NonNull AudioManager am) {
-        mAm = Objects.requireNonNull(am);
-        mLoudnessCodecStub = new LoudnessCodecUpdatesDispatcherStub();
-    }
+    @NonNull private final IAudioService mAudioService;
 
     /** @hide */
-    public LoudnessCodecConfigurator createLoudnessCodecConfigurator() {
-        return new LoudnessCodecConfigurator(this);
+    public LoudnessCodecDispatcher(@NonNull IAudioService audioService) {
+        mAudioService = Objects.requireNonNull(audioService);
+    }
+
+    @Override
+    public void register(boolean register) {
+        try {
+            if (register) {
+                mAudioService.registerLoudnessCodecUpdatesDispatcher(
+                        LoudnessCodecUpdatesDispatcherStub.getInstance());
+            } else {
+                mAudioService.unregisterLoudnessCodecUpdatesDispatcher(
+                        LoudnessCodecUpdatesDispatcherStub.getInstance());
+            }
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
     }
 
     /** @hide */
     public void addLoudnessCodecListener(@NonNull LoudnessCodecConfigurator configurator,
                                          @NonNull @CallbackExecutor Executor executor,
                                          @NonNull OnLoudnessCodecUpdateListener listener) {
-        Objects.requireNonNull(configurator);
-        Objects.requireNonNull(executor);
-        Objects.requireNonNull(listener);
-
-        mConfiguratorListener.put(listener, configurator);
-        mLoudnessListenerMgr.addListener(
-                executor, listener, "addLoudnessCodecListener", () -> mLoudnessCodecStub);
+        LoudnessCodecUpdatesDispatcherStub.getInstance().addLoudnessCodecListener(this,
+                configurator, executor, listener);
     }
 
     /** @hide */
     public void removeLoudnessCodecListener(@NonNull LoudnessCodecConfigurator configurator) {
-        Objects.requireNonNull(configurator);
+        LoudnessCodecUpdatesDispatcherStub.getInstance().removeLoudnessCodecListener(configurator);
+    }
 
-        for (Entry<OnLoudnessCodecUpdateListener, LoudnessCodecConfigurator> e :
-                mConfiguratorListener.entrySet()) {
-            if (e.getValue() == configurator) {
-                final OnLoudnessCodecUpdateListener listener = e.getKey();
-                mConfiguratorListener.remove(listener);
-                mLoudnessListenerMgr.removeListener(listener, "removeLoudnessCodecListener");
-                break;
-            }
+    /** @hide */
+    public void startLoudnessCodecUpdates(int piid, List<LoudnessCodecInfo> codecInfoList) {
+        try {
+            mAudioService.startLoudnessCodecUpdates(piid, codecInfoList);
+        }  catch (RemoteException e) {
+            e.rethrowFromSystemServer();
         }
     }
+
+    /** @hide */
+    public void stopLoudnessCodecUpdates(int piid) {
+        try {
+            mAudioService.stopLoudnessCodecUpdates(piid);
+        }  catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    /** @hide */
+    public void addLoudnessCodecInfo(int piid, @NonNull LoudnessCodecInfo mcInfo) {
+        try {
+            mAudioService.addLoudnessCodecInfo(piid, mcInfo);
+        }  catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    /** @hide */
+    public void removeLoudnessCodecInfo(int piid, @NonNull LoudnessCodecInfo mcInfo) {
+        try {
+            mAudioService.removeLoudnessCodecInfo(piid, mcInfo);
+        }  catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    /** @hide */
+    public Bundle getLoudnessCodecParams(int piid, @NonNull LoudnessCodecInfo mcInfo) {
+        Bundle loudnessParams = null;
+        try {
+            loudnessParams = new Bundle(mAudioService.getLoudnessParams(piid, mcInfo));
+        }  catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+        return loudnessParams;
+    }
 }
diff --git a/media/java/android/media/LoudnessCodecFormat.aidl b/media/java/android/media/LoudnessCodecFormat.aidl
deleted file mode 100644
index 75c9060..0000000
--- a/media/java/android/media/LoudnessCodecFormat.aidl
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2023 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.media;
-
-
-/**
- * Loudness format which specifies the input attributes used for measuring
- * the parameters required to perform loudness alignment as specified by the
- * CTA2075 standard.
- *
- * {@hide}
- */
-parcelable LoudnessCodecFormat {
-    String metadataType;
-    boolean isDownmixing;
-}
\ No newline at end of file
diff --git a/media/java/android/media/LoudnessCodecInfo.aidl b/media/java/android/media/LoudnessCodecInfo.aidl
new file mode 100644
index 0000000..fd69517
--- /dev/null
+++ b/media/java/android/media/LoudnessCodecInfo.aidl
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 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.media;
+
+/**
+ * Loudness information for a {@link MediaCodec} object which specifies the
+ * input attributes used for measuring the parameters required to perform
+ * loudness alignment as specified by the CTA2075 standard.
+ *
+ * {@hide}
+ */
+@JavaDerive(equals = true)
+parcelable LoudnessCodecInfo {
+    /** Supported codec metadata types for loudness updates. */
+    @Backing(type="int")
+    enum CodecMetadataType {
+        CODEC_METADATA_TYPE_INVALID = 0,
+        CODEC_METADATA_TYPE_MPEG_4 = 1,
+        CODEC_METADATA_TYPE_MPEG_D = 2,
+        CODEC_METADATA_TYPE_AC_3 = 3,
+        CODEC_METADATA_TYPE_AC_4 = 4,
+        CODEC_METADATA_TYPE_DTS_HD = 5,
+        CODEC_METADATA_TYPE_DTS_UHD = 6
+    }
+
+    int mediaCodecHashCode;
+    CodecMetadataType metadataType;
+    boolean isDownmixing;
+}
\ No newline at end of file
diff --git a/media/tests/LoudnessCodecApiTest/Android.bp b/media/tests/LoudnessCodecApiTest/Android.bp
new file mode 100644
index 0000000..5ca0fc9
--- /dev/null
+++ b/media/tests/LoudnessCodecApiTest/Android.bp
@@ -0,0 +1,27 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test {
+    name: "LoudnessCodecApiTest",
+    srcs: ["**/*.java"],
+    static_libs: [
+        "androidx.test.ext.junit",
+        "androidx.test.rules",
+        "junit",
+        "junit-params",
+        "mockito-target-minus-junit4",
+        "flag-junit",
+        "hamcrest-library",
+        "platform-test-annotations",
+    ],
+    platform_apis: true,
+    certificate: "platform",
+    resource_dirs: ["res"],
+    test_suites: ["device-tests"],
+}
diff --git a/media/tests/LoudnessCodecApiTest/AndroidManifest.xml b/media/tests/LoudnessCodecApiTest/AndroidManifest.xml
new file mode 100644
index 0000000..91a671f
--- /dev/null
+++ b/media/tests/LoudnessCodecApiTest/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.loudnesscodecapitest">
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+            android:targetPackage="com.android.loudnesscodecapitest"
+            android:label="AudioManager loudness codec integration tests InstrumentationRunner">
+    </instrumentation>
+</manifest>
diff --git a/media/tests/LoudnessCodecApiTest/AndroidTest.xml b/media/tests/LoudnessCodecApiTest/AndroidTest.xml
new file mode 100644
index 0000000..0099d98
--- /dev/null
+++ b/media/tests/LoudnessCodecApiTest/AndroidTest.xml
@@ -0,0 +1,27 @@
+<?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.
+-->
+<configuration description="Runs Media Framework Tests">
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+        <option name="test-file-name" value="LoudnessCodecApiTest.apk" />
+    </target_preparer>
+
+    <option name="test-tag" value="LoudnessCodecApiTest" />
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.loudnesscodecapitest" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <option name="hidden-api-checks" value="false"/>
+    </test>
+</configuration>
diff --git a/media/tests/LoudnessCodecApiTest/res/layout/loudnesscodecapitest.xml b/media/tests/LoudnessCodecApiTest/res/layout/loudnesscodecapitest.xml
new file mode 100644
index 0000000..17fdba6
--- /dev/null
+++ b/media/tests/LoudnessCodecApiTest/res/layout/loudnesscodecapitest.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:orientation="vertical">
+</LinearLayout>
diff --git a/media/tests/LoudnessCodecApiTest/res/raw/noise_2ch_48khz_tlou_19lufs_anchor_17lufs_mp4.m4a b/media/tests/LoudnessCodecApiTest/res/raw/noise_2ch_48khz_tlou_19lufs_anchor_17lufs_mp4.m4a
new file mode 100644
index 0000000..acba4b3
--- /dev/null
+++ b/media/tests/LoudnessCodecApiTest/res/raw/noise_2ch_48khz_tlou_19lufs_anchor_17lufs_mp4.m4a
Binary files differ
diff --git a/media/tests/LoudnessCodecApiTest/res/values/strings.xml b/media/tests/LoudnessCodecApiTest/res/values/strings.xml
new file mode 100644
index 0000000..0c4227c
--- /dev/null
+++ b/media/tests/LoudnessCodecApiTest/res/values/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <!-- name of the app [CHAR LIMIT=25]-->
+    <string name="app_name">Loudness Codec API Tests</string>
+</resources>
diff --git a/media/tests/LoudnessCodecApiTest/src/com/android/loudnesscodecapitest/LoudnessCodecConfiguratorTest.java b/media/tests/LoudnessCodecApiTest/src/com/android/loudnesscodecapitest/LoudnessCodecConfiguratorTest.java
new file mode 100644
index 0000000..65a9799
--- /dev/null
+++ b/media/tests/LoudnessCodecApiTest/src/com/android/loudnesscodecapitest/LoudnessCodecConfiguratorTest.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2023 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.loudnesscodecapitest;
+
+import static android.media.audio.Flags.FLAG_LOUDNESS_CONFIGURATOR_API;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.res.AssetFileDescriptor;
+import android.media.AudioAttributes;
+import android.media.AudioFormat;
+import android.media.AudioTrack;
+import android.media.IAudioService;
+import android.media.LoudnessCodecConfigurator;
+import android.media.LoudnessCodecConfigurator.OnLoudnessCodecUpdateListener;
+import android.media.MediaCodec;
+import android.media.MediaExtractor;
+import android.media.MediaFormat;
+import android.os.PersistableBundle;
+import android.platform.test.annotations.Presubmit;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.util.Log;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.List;
+import java.util.concurrent.Executors;
+
+/**
+ * Unit tests for {@link LoudnessCodecConfigurator} checking the internal interactions with a mocked
+ * {@link IAudioService} without any real IPC interactions.
+ */
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class LoudnessCodecConfiguratorTest {
+    private static final String TAG = "LoudnessCodecConfiguratorTest";
+
+    private static final String TEST_MEDIA_AUDIO_CODEC_PREFIX = "audio/";
+    private static final int TEST_AUDIO_TRACK_BUFFER_SIZE = 2048;
+    private static final int TEST_AUDIO_TRACK_SAMPLERATE = 48000;
+    private static final int TEST_AUDIO_TRACK_CHANNELS = 2;
+
+    @Rule
+    public final MockitoRule mockito = MockitoJUnit.rule();
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    @Mock
+    private IAudioService mAudioService;
+
+    private LoudnessCodecConfigurator mLcc;
+
+    @Before
+    public void setUp() {
+        mLcc = LoudnessCodecConfigurator.createForTesting(mAudioService,
+                Executors.newSingleThreadExecutor(), new OnLoudnessCodecUpdateListener() {});
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+    public void setAudioTrack_callsAudioServiceStart() throws Exception {
+        final AudioTrack track = createAudioTrack();
+
+        mLcc.addMediaCodec(createAndConfigureMediaCodec());
+        mLcc.setAudioTrack(track);
+
+        verify(mAudioService).startLoudnessCodecUpdates(eq(track.getPlayerIId()),
+                anyList());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+    public void getLoudnessCodecParams_callsAudioServiceGetLoudness() throws Exception {
+        when(mAudioService.getLoudnessParams(anyInt(), any())).thenReturn(new PersistableBundle());
+        final AudioTrack track = createAudioTrack();
+
+        mLcc.getLoudnessCodecParams(track, createAndConfigureMediaCodec());
+
+        verify(mAudioService).getLoudnessParams(eq(track.getPlayerIId()), any());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+    public void setAudioTrack_addsAudioServicePiidCodecs() throws Exception {
+        final AudioTrack track = createAudioTrack();
+        final MediaCodec mediaCodec = createAndConfigureMediaCodec();
+
+        mLcc.addMediaCodec(mediaCodec);
+        mLcc.setAudioTrack(track);
+
+        verify(mAudioService).startLoudnessCodecUpdates(eq(track.getPlayerIId()), anyList());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+    public void setAudioTrackTwice_ignoresSecondCall() throws Exception {
+        final AudioTrack track = createAudioTrack();
+        final MediaCodec mediaCodec = createAndConfigureMediaCodec();
+
+        mLcc.addMediaCodec(mediaCodec);
+        mLcc.setAudioTrack(track);
+        mLcc.setAudioTrack(track);
+
+        verify(mAudioService, times(1)).startLoudnessCodecUpdates(eq(track.getPlayerIId()),
+                anyList());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+    public void setTrackNull_stopCodecUpdates() throws Exception {
+        final AudioTrack track = createAudioTrack();
+
+        mLcc.addMediaCodec(createAndConfigureMediaCodec());
+        mLcc.setAudioTrack(track);
+
+        mLcc.setAudioTrack(null);  // stops updates
+        verify(mAudioService).stopLoudnessCodecUpdates(eq(track.getPlayerIId()));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+    public void addMediaCodecTwice_ignoresSecondCall() throws Exception {
+        final ArgumentCaptor<List> argument = ArgumentCaptor.forClass(List.class);
+        final AudioTrack track = createAudioTrack();
+        final MediaCodec mediaCodec = createAndConfigureMediaCodec();
+
+        mLcc.addMediaCodec(mediaCodec);
+        mLcc.addMediaCodec(mediaCodec);
+        mLcc.setAudioTrack(track);
+
+        verify(mAudioService, times(1)).startLoudnessCodecUpdates(
+                eq(track.getPlayerIId()), argument.capture());
+        assertEquals(argument.getValue().size(), 1);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+    public void setClearTrack_removeAllAudioServicePiidCodecs() throws Exception {
+        final ArgumentCaptor<List> argument = ArgumentCaptor.forClass(List.class);
+
+        final AudioTrack track = createAudioTrack();
+
+        mLcc.addMediaCodec(createAndConfigureMediaCodec());
+        mLcc.setAudioTrack(track);
+        verify(mAudioService).startLoudnessCodecUpdates(eq(track.getPlayerIId()),
+                argument.capture());
+        assertEquals(argument.getValue().size(), 1);
+
+        mLcc.addMediaCodec(createAndConfigureMediaCodec());
+        mLcc.setAudioTrack(null);
+        verify(mAudioService).stopLoudnessCodecUpdates(eq(track.getPlayerIId()));
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+    public void removeAddedMediaCodecAfterSetTrack_callsAudioServiceRemoveCodec() throws Exception {
+        final AudioTrack track = createAudioTrack();
+        final MediaCodec mediaCodec = createAndConfigureMediaCodec();
+
+        mLcc.addMediaCodec(mediaCodec);
+        mLcc.setAudioTrack(track);
+        mLcc.removeMediaCodec(mediaCodec);
+
+        verify(mAudioService).removeLoudnessCodecInfo(eq(track.getPlayerIId()), any());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+    public void addMediaCodecAfterSetTrack_callsAudioServiceAdd() throws Exception {
+        final AudioTrack track = createAudioTrack();
+
+        mLcc.addMediaCodec(createAndConfigureMediaCodec());
+        mLcc.setAudioTrack(track);
+        verify(mAudioService).startLoudnessCodecUpdates(eq(track.getPlayerIId()), anyList());
+
+        mLcc.addMediaCodec(createAndConfigureMediaCodec());
+        verify(mAudioService).addLoudnessCodecInfo(eq(track.getPlayerIId()), any());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+    public void removeMediaCodecAfterSetTrack_callsAudioServiceRemove() throws Exception {
+        final AudioTrack track = createAudioTrack();
+        final MediaCodec mediaCodec = createAndConfigureMediaCodec();
+
+        mLcc.addMediaCodec(mediaCodec);
+        mLcc.setAudioTrack(track);
+        verify(mAudioService).startLoudnessCodecUpdates(eq(track.getPlayerIId()), anyList());
+
+        mLcc.removeMediaCodec(mediaCodec);
+        verify(mAudioService).removeLoudnessCodecInfo(eq(track.getPlayerIId()), any());
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_LOUDNESS_CONFIGURATOR_API)
+    public void removeWrongMediaCodecAfterSetTrack_noAudioServiceRemoveCall() throws Exception {
+        final AudioTrack track = createAudioTrack();
+
+        mLcc.addMediaCodec(createAndConfigureMediaCodec());
+        mLcc.setAudioTrack(track);
+        verify(mAudioService).startLoudnessCodecUpdates(eq(track.getPlayerIId()), anyList());
+
+        mLcc.removeMediaCodec(createAndConfigureMediaCodec());
+        verify(mAudioService, times(0)).removeLoudnessCodecInfo(eq(track.getPlayerIId()), any());
+    }
+
+    private static AudioTrack createAudioTrack() {
+        return new AudioTrack.Builder()
+                .setAudioAttributes(new AudioAttributes.Builder().build())
+                .setBufferSizeInBytes(TEST_AUDIO_TRACK_BUFFER_SIZE)
+                .setAudioFormat(new AudioFormat.Builder()
+                        .setChannelMask(TEST_AUDIO_TRACK_CHANNELS)
+                        .setSampleRate(TEST_AUDIO_TRACK_SAMPLERATE).build())
+                .build();
+    }
+
+    private MediaCodec createAndConfigureMediaCodec() throws Exception {
+        AssetFileDescriptor testFd = InstrumentationRegistry.getInstrumentation().getContext()
+                .getResources()
+                .openRawResourceFd(R.raw.noise_2ch_48khz_tlou_19lufs_anchor_17lufs_mp4);
+
+        MediaExtractor extractor;
+        extractor = new MediaExtractor();
+        extractor.setDataSource(testFd.getFileDescriptor(), testFd.getStartOffset(),
+                testFd.getLength());
+        testFd.close();
+
+        assertEquals("wrong number of tracks", 1, extractor.getTrackCount());
+        MediaFormat format = extractor.getTrackFormat(0);
+        String mime = format.getString(MediaFormat.KEY_MIME);
+        assertTrue("not an audio file", mime.startsWith(TEST_MEDIA_AUDIO_CODEC_PREFIX));
+        final MediaCodec mediaCodec = MediaCodec.createDecoderByType(mime);
+
+        Log.v(TAG, "configuring with " + format);
+        mediaCodec.configure(format, null /* surface */, null /* crypto */, 0 /* flags */);
+
+        return mediaCodec;
+    }
+}
diff --git a/packages/PackageInstaller/AndroidManifest.xml b/packages/PackageInstaller/AndroidManifest.xml
index 3a2b1ce..ef218fd 100644
--- a/packages/PackageInstaller/AndroidManifest.xml
+++ b/packages/PackageInstaller/AndroidManifest.xml
@@ -156,6 +156,15 @@
             </intent-filter>
         </receiver>
 
+        <receiver android:name=".v2.model.UninstallEventReceiver"
+            android:permission="android.permission.INSTALL_PACKAGES"
+            android:exported="false"
+            android:enabled="false">
+            <intent-filter android:priority="1">
+                <action android:name="com.android.packageinstaller.ACTION_UNINSTALL_COMMIT" />
+            </intent-filter>
+        </receiver>
+
         <receiver android:name=".PackageInstalledReceiver"
                 android:exported="false">
             <intent-filter android:priority="1">
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.java
index e188838..fe05237 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/PackageUtil.java
@@ -30,6 +30,8 @@
 import android.net.Uri;
 import android.os.Build;
 import android.os.Process;
+import android.os.UserHandle;
+import android.os.UserManager;
 import android.util.Log;
 import androidx.annotation.NonNull;
 import java.io.File;
@@ -404,6 +406,24 @@
     }
 
     /**
+     * Is a profile part of a user?
+     *
+     * @param userManager The user manager
+     * @param userHandle The handle of the user
+     * @param profileHandle The handle of the profile
+     *
+     * @return If the profile is part of the user or the profile parent of the user
+     */
+    public static boolean isProfileOfOrSame(UserManager userManager, UserHandle userHandle,
+        UserHandle profileHandle) {
+        if (userHandle.equals(profileHandle)) {
+            return true;
+        }
+        return userManager.getProfileParent(profileHandle) != null
+            && userManager.getProfileParent(profileHandle).equals(userHandle);
+    }
+
+    /**
      * The class to hold an incoming package's icon and label.
      * See {@link #getAppSnippet(Context, SessionInfo)},
      * {@link #getAppSnippet(Context, PackageInfo)},
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallEventReceiver.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallEventReceiver.java
new file mode 100644
index 0000000..79e00df
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallEventReceiver.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2023 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
+ *
+ *      https://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.packageinstaller.v2.model;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import androidx.annotation.NonNull;
+
+/**
+ * Receives uninstall events and persists them using a {@link EventResultPersister}.
+ */
+public class UninstallEventReceiver extends BroadcastReceiver {
+    private static final Object sLock = new Object();
+    private static EventResultPersister sReceiver;
+
+    /**
+     * Get the event receiver persisting the results
+     *
+     * @return The event receiver.
+     */
+    @NonNull private static EventResultPersister getReceiver(@NonNull Context context) {
+        synchronized (sLock) {
+            if (sReceiver == null) {
+                sReceiver = new EventResultPersister(
+                        TemporaryFileManager.getUninstallStateFile(context));
+            }
+        }
+
+        return sReceiver;
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        getReceiver(context).onEventReceived(context, intent);
+    }
+
+    /**
+     * Add an observer. If there is already an event for this id, call back inside of this call.
+     *
+     * @param context  A context of the current app
+     * @param id       The id the observer is for or {@code GENERATE_NEW_ID} to generate a new one.
+     * @param observer The observer to call back.
+     *
+     * @return The id for this event
+     */
+    public static int addObserver(@NonNull Context context, int id,
+            @NonNull EventResultPersister.EventResultObserver observer)
+            throws EventResultPersister.OutOfIdsException {
+        return getReceiver(context).addObserver(id, observer);
+    }
+
+    /**
+     * Remove a observer.
+     *
+     * @param context  A context of the current app
+     * @param id The id the observer was added for
+     */
+    static void removeObserver(@NonNull Context context, int id) {
+        getReceiver(context).removeObserver(id);
+    }
+
+    /**
+     * @param context A context of the current app
+     *
+     * @return A new uninstall id
+     */
+    static int getNewId(@NonNull Context context) throws EventResultPersister.OutOfIdsException {
+        return getReceiver(context).getNewId();
+    }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.java
index 6533c50..2e43b75 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/UninstallRepository.java
@@ -16,14 +16,699 @@
 
 package com.android.packageinstaller.v2.model;
 
+import static android.app.AppOpsManager.MODE_ALLOWED;
+import static android.os.UserManager.USER_TYPE_PROFILE_CLONE;
+import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED;
+import static com.android.packageinstaller.v2.model.PackageUtil.getMaxTargetSdkVersionForUid;
+import static com.android.packageinstaller.v2.model.PackageUtil.getPackageNameForUid;
+import static com.android.packageinstaller.v2.model.PackageUtil.isPermissionGranted;
+import static com.android.packageinstaller.v2.model.PackageUtil.isProfileOfOrSame;
+import static com.android.packageinstaller.v2.model.uninstallstagedata.UninstallAborted.ABORT_REASON_APP_UNAVAILABLE;
+import static com.android.packageinstaller.v2.model.uninstallstagedata.UninstallAborted.ABORT_REASON_GENERIC_ERROR;
+import static com.android.packageinstaller.v2.model.uninstallstagedata.UninstallAborted.ABORT_REASON_USER_NOT_ALLOWED;
+
+import android.Manifest;
+import android.app.Activity;
+import android.app.AppOpsManager;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.admin.DevicePolicyManager;
+import android.app.usage.StorageStats;
+import android.app.usage.StorageStatsManager;
+import android.content.ComponentName;
 import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageInstaller;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.UninstallCompleteCallback;
+import android.content.pm.VersionedPackage;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Process;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.MutableLiveData;
+import com.android.packageinstaller.R;
+import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallAborted;
+import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallFailed;
+import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallReady;
+import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallStage;
+import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallSuccess;
+import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallUninstalling;
+import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallUserActionRequired;
+import java.io.IOException;
+import java.util.List;
 
 public class UninstallRepository {
 
     private static final String TAG = UninstallRepository.class.getSimpleName();
+    private static final String UNINSTALL_FAILURE_CHANNEL = "uninstall_failure";
+    private static final String BROADCAST_ACTION =
+        "com.android.packageinstaller.ACTION_UNINSTALL_COMMIT";
+
+    private static final String EXTRA_UNINSTALL_ID =
+        "com.android.packageinstaller.extra.UNINSTALL_ID";
+    private static final String EXTRA_APP_LABEL =
+        "com.android.packageinstaller.extra.APP_LABEL";
+    private static final String EXTRA_IS_CLONE_APP =
+        "com.android.packageinstaller.extra.IS_CLONE_APP";
+    private static final String EXTRA_PACKAGE_NAME =
+        "com.android.packageinstaller.extra.EXTRA_PACKAGE_NAME";
+
     private final Context mContext;
+    private final AppOpsManager mAppOpsManager;
+    private final PackageManager mPackageManager;
+    private final UserManager mUserManager;
+    private final NotificationManager mNotificationManager;
+    private final MutableLiveData<UninstallStage> mUninstallResult = new MutableLiveData<>();
+    public UserHandle mUninstalledUser;
+    public UninstallCompleteCallback mCallback;
+    private ApplicationInfo mTargetAppInfo;
+    private ActivityInfo mTargetActivityInfo;
+    private Intent mIntent;
+    private CharSequence mTargetAppLabel;
+    private String mTargetPackageName;
+    private String mCallingActivity;
+    private boolean mUninstallFromAllUsers;
+    private boolean mIsClonedApp;
+    private int mUninstallId;
 
     public UninstallRepository(Context context) {
         mContext = context;
+        mAppOpsManager = context.getSystemService(AppOpsManager.class);
+        mPackageManager = context.getPackageManager();
+        mUserManager = context.getSystemService(UserManager.class);
+        mNotificationManager = context.getSystemService(NotificationManager.class);
+    }
+
+    public UninstallStage performPreUninstallChecks(Intent intent, CallerInfo callerInfo) {
+        mIntent = intent;
+
+        int callingUid = callerInfo.getUid();
+        mCallingActivity = callerInfo.getActivityName();
+
+        if (callingUid == Process.INVALID_UID) {
+            Log.e(TAG, "Could not determine the launching uid.");
+            return new UninstallAborted(ABORT_REASON_GENERIC_ERROR);
+            // TODO: should we give any indication to the user?
+        }
+
+        String callingPackage = getPackageNameForUid(mContext, callingUid, null);
+        if (callingPackage == null) {
+            Log.e(TAG, "Package not found for originating uid " + callingUid);
+            return new UninstallAborted(ABORT_REASON_GENERIC_ERROR);
+        } else {
+            if (mAppOpsManager.noteOpNoThrow(
+                AppOpsManager.OPSTR_REQUEST_DELETE_PACKAGES, callingUid, callingPackage)
+                != MODE_ALLOWED) {
+                Log.e(TAG, "Install from uid " + callingUid + " disallowed by AppOps");
+                return new UninstallAborted(ABORT_REASON_GENERIC_ERROR);
+            }
+        }
+
+        if (getMaxTargetSdkVersionForUid(mContext, callingUid) >= Build.VERSION_CODES.P
+            && !isPermissionGranted(mContext, Manifest.permission.REQUEST_DELETE_PACKAGES,
+            callingUid)
+            && !isPermissionGranted(mContext, Manifest.permission.DELETE_PACKAGES, callingUid)) {
+            Log.e(TAG, "Uid " + callingUid + " does not have "
+                + Manifest.permission.REQUEST_DELETE_PACKAGES + " or "
+                + Manifest.permission.DELETE_PACKAGES);
+
+            return new UninstallAborted(ABORT_REASON_GENERIC_ERROR);
+        }
+
+        // Get intent information.
+        // We expect an intent with URI of the form package:<packageName>#<className>
+        // className is optional; if specified, it is the activity the user chose to uninstall
+        final Uri packageUri = intent.getData();
+        if (packageUri == null) {
+            Log.e(TAG, "No package URI in intent");
+            return new UninstallAborted(ABORT_REASON_APP_UNAVAILABLE);
+        }
+        mTargetPackageName = packageUri.getEncodedSchemeSpecificPart();
+        if (mTargetPackageName == null) {
+            Log.e(TAG, "Invalid package name in URI: " + packageUri);
+            return new UninstallAborted(ABORT_REASON_APP_UNAVAILABLE);
+        }
+
+        mUninstallFromAllUsers = intent.getBooleanExtra(Intent.EXTRA_UNINSTALL_ALL_USERS,
+            false);
+        if (mUninstallFromAllUsers && !mUserManager.isAdminUser()) {
+            Log.e(TAG, "Only admin user can request uninstall for all users");
+            return new UninstallAborted(ABORT_REASON_USER_NOT_ALLOWED);
+        }
+
+        mUninstalledUser = intent.getParcelableExtra(Intent.EXTRA_USER, UserHandle.class);
+        if (mUninstalledUser == null) {
+            mUninstalledUser = Process.myUserHandle();
+        } else {
+            List<UserHandle> profiles = mUserManager.getUserProfiles();
+            if (!profiles.contains(mUninstalledUser)) {
+                Log.e(TAG, "User " + Process.myUserHandle() + " can't request uninstall "
+                    + "for user " + mUninstalledUser);
+                return new UninstallAborted(ABORT_REASON_USER_NOT_ALLOWED);
+            }
+        }
+
+        mCallback = intent.getParcelableExtra(PackageInstaller.EXTRA_CALLBACK,
+            PackageManager.UninstallCompleteCallback.class);
+
+        try {
+            mTargetAppInfo = mPackageManager.getApplicationInfo(mTargetPackageName,
+                PackageManager.ApplicationInfoFlags.of(PackageManager.MATCH_ANY_USER));
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.e(TAG, "Unable to get packageName");
+        }
+
+        if (mTargetAppInfo == null) {
+            Log.e(TAG, "Invalid packageName: " + mTargetPackageName);
+            return new UninstallAborted(ABORT_REASON_APP_UNAVAILABLE);
+        }
+
+        // The class name may have been specified (e.g. when deleting an app from all apps)
+        final String className = packageUri.getFragment();
+        if (className != null) {
+            try {
+                mTargetActivityInfo = mPackageManager.getActivityInfo(
+                    new ComponentName(mTargetPackageName, className),
+                    PackageManager.ComponentInfoFlags.of(0));
+            } catch (PackageManager.NameNotFoundException e) {
+                Log.e(TAG, "Unable to get className");
+                // Continue as the ActivityInfo isn't critical.
+            }
+        }
+
+        return new UninstallReady();
+    }
+
+    public UninstallStage generateUninstallDetails() {
+        UninstallUserActionRequired.Builder uarBuilder = new UninstallUserActionRequired.Builder();
+        StringBuilder messageBuilder = new StringBuilder();
+
+        mTargetAppLabel = mTargetAppInfo.loadSafeLabel(mPackageManager);
+
+        // If the Activity label differs from the App label, then make sure the user
+        // knows the Activity belongs to the App being uninstalled.
+        if (mTargetActivityInfo != null) {
+            final CharSequence activityLabel = mTargetActivityInfo.loadSafeLabel(mPackageManager);
+            if (CharSequence.compare(activityLabel, mTargetAppLabel) != 0) {
+                messageBuilder.append(
+                    mContext.getString(R.string.uninstall_activity_text, activityLabel));
+                messageBuilder.append(" ").append(mTargetAppLabel).append(".\n\n");
+            }
+        }
+
+        final boolean isUpdate =
+            (mTargetAppInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0;
+        final UserHandle myUserHandle = Process.myUserHandle();
+        boolean isSingleUser = isSingleUser();
+
+        if (isUpdate) {
+            messageBuilder.append(mContext.getString(
+                isSingleUser ? R.string.uninstall_update_text :
+                    R.string.uninstall_update_text_multiuser));
+        } else if (mUninstallFromAllUsers && !isSingleUser) {
+            messageBuilder.append(mContext.getString(
+                R.string.uninstall_application_text_all_users));
+        } else if (!mUninstalledUser.equals(myUserHandle)) {
+            // Uninstalling user is issuing uninstall for another user
+            UserManager customUserManager = mContext.createContextAsUser(mUninstalledUser, 0)
+                .getSystemService(UserManager.class);
+            String userName = customUserManager.getUserName();
+
+            String uninstalledUserType = getUninstalledUserType(myUserHandle, mUninstalledUser);
+            String messageString;
+            if (USER_TYPE_PROFILE_MANAGED.equals(uninstalledUserType)) {
+                messageString = mContext.getString(
+                    R.string.uninstall_application_text_current_user_work_profile, userName);
+            } else if (USER_TYPE_PROFILE_CLONE.equals(uninstalledUserType)) {
+                mIsClonedApp = true;
+                messageString = mContext.getString(
+                    R.string.uninstall_application_text_current_user_clone_profile);
+            } else {
+                messageString = mContext.getString(
+                    R.string.uninstall_application_text_user, userName);
+            }
+            messageBuilder.append(messageString);
+        } else if (isCloneProfile(mUninstalledUser)) {
+            mIsClonedApp = true;
+            messageBuilder.append(mContext.getString(
+                R.string.uninstall_application_text_current_user_clone_profile));
+        } else if (myUserHandle.equals(UserHandle.SYSTEM)
+            && hasClonedInstance(mTargetAppInfo.packageName)) {
+            messageBuilder.append(mContext.getString(
+                R.string.uninstall_application_text_with_clone_instance, mTargetAppLabel));
+        } else {
+            messageBuilder.append(mContext.getString(R.string.uninstall_application_text));
+        }
+
+        uarBuilder.setMessage(messageBuilder.toString());
+
+        if (mIsClonedApp) {
+            uarBuilder.setTitle(mContext.getString(R.string.cloned_app_label, mTargetAppLabel));
+        } else {
+            uarBuilder.setTitle(mTargetAppLabel.toString());
+        }
+
+        boolean suggestToKeepAppData = false;
+        try {
+            PackageInfo pkgInfo = mPackageManager.getPackageInfo(mTargetPackageName, 0);
+            suggestToKeepAppData =
+                pkgInfo.applicationInfo != null && pkgInfo.applicationInfo.hasFragileUserData();
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.e(TAG, "Cannot check hasFragileUserData for " + mTargetPackageName, e);
+        }
+
+        long appDataSize = 0;
+        if (suggestToKeepAppData) {
+            appDataSize = getAppDataSize(mTargetPackageName,
+                mUninstallFromAllUsers ? null : mUninstalledUser);
+        }
+        uarBuilder.setAppDataSize(appDataSize);
+
+        return uarBuilder.build();
+    }
+
+    /**
+     * Returns whether there is only one "full" user on this device.
+     *
+     * <p><b>Note:</b> on devices that use {@link android.os.UserManager#isHeadlessSystemUserMode()
+     * headless system user mode}, the system user is not "full", so it's not be considered in the
+     * calculation.</p>
+     */
+    private boolean isSingleUser() {
+        final int userCount = mUserManager.getUserCount();
+        return userCount == 1 || (UserManager.isHeadlessSystemUserMode() && userCount == 2);
+    }
+
+    /**
+     * Returns the type of the user from where an app is being uninstalled. We are concerned with
+     * only USER_TYPE_PROFILE_MANAGED and USER_TYPE_PROFILE_CLONE and whether the user and profile
+     * belong to the same profile group.
+     */
+    @Nullable
+    private String getUninstalledUserType(UserHandle myUserHandle,
+        UserHandle uninstalledUserHandle) {
+        if (!mUserManager.isSameProfileGroup(myUserHandle, uninstalledUserHandle)) {
+            return null;
+        }
+
+        UserManager customUserManager = mContext.createContextAsUser(uninstalledUserHandle, 0)
+            .getSystemService(UserManager.class);
+        String[] userTypes = {USER_TYPE_PROFILE_MANAGED, USER_TYPE_PROFILE_CLONE};
+        for (String userType : userTypes) {
+            if (customUserManager.isUserOfType(userType)) {
+                return userType;
+            }
+        }
+        return null;
+    }
+
+    private boolean hasClonedInstance(String packageName) {
+        // Check if clone user is present on the device.
+        UserHandle cloneUser = null;
+        List<UserHandle> profiles = mUserManager.getUserProfiles();
+        for (UserHandle userHandle : profiles) {
+            if (!userHandle.equals(UserHandle.SYSTEM) && isCloneProfile(userHandle)) {
+                cloneUser = userHandle;
+                break;
+            }
+        }
+        // Check if another instance of given package exists in clone user profile.
+        try {
+            return cloneUser != null
+                && mPackageManager.getPackageUidAsUser(packageName,
+                PackageManager.PackageInfoFlags.of(0), cloneUser.getIdentifier()) > 0;
+        } catch (PackageManager.NameNotFoundException e) {
+            return false;
+        }
+    }
+
+    private boolean isCloneProfile(UserHandle userHandle) {
+        UserManager customUserManager = mContext.createContextAsUser(userHandle, 0)
+            .getSystemService(UserManager.class);
+        return customUserManager.isUserOfType(UserManager.USER_TYPE_PROFILE_CLONE);
+    }
+
+    /**
+     * Get number of bytes of the app data of the package.
+     *
+     * @param pkg The package that might have app data.
+     * @param user The user the package belongs to or {@code null} if files of all users should
+     *     be counted.
+     * @return The number of bytes.
+     */
+    private long getAppDataSize(@NonNull String pkg, @Nullable UserHandle user) {
+        if (user != null) {
+            return getAppDataSizeForUser(pkg, user);
+        }
+        // We are uninstalling from all users. Get cumulative app data size for all users.
+        List<UserHandle> userHandles = mUserManager.getUserHandles(true);
+        long totalAppDataSize = 0;
+        int numUsers = userHandles.size();
+        for (int i = 0; i < numUsers; i++) {
+            totalAppDataSize += getAppDataSizeForUser(pkg, userHandles.get(i));
+        }
+        return totalAppDataSize;
+    }
+
+    /**
+     * Get number of bytes of the app data of the package.
+     *
+     * @param pkg The package that might have app data.
+     * @param user The user the package belongs to
+     * @return The number of bytes.
+     */
+    private long getAppDataSizeForUser(@NonNull String pkg, @NonNull UserHandle user) {
+        StorageStatsManager storageStatsManager =
+            mContext.getSystemService(StorageStatsManager.class);
+        try {
+            StorageStats stats = storageStatsManager.queryStatsForPackage(
+                mPackageManager.getApplicationInfo(pkg, 0).storageUuid, pkg, user);
+            return stats.getDataBytes();
+        } catch (PackageManager.NameNotFoundException | IOException | SecurityException e) {
+            Log.e(TAG, "Cannot determine amount of app data for " + pkg, e);
+        }
+        return 0;
+    }
+
+    public void initiateUninstall(boolean keepData) {
+        // Get an uninstallId to track results and show a notification on non-TV devices.
+        try {
+            mUninstallId = UninstallEventReceiver.addObserver(mContext,
+                EventResultPersister.GENERATE_NEW_ID, this::handleUninstallResult);
+        } catch (EventResultPersister.OutOfIdsException e) {
+            Log.e(TAG, "Failed to start uninstall", e);
+            handleUninstallResult(PackageInstaller.STATUS_FAILURE,
+                PackageManager.DELETE_FAILED_INTERNAL_ERROR, null, 0);
+            return;
+        }
+
+        // TODO: Check with UX whether to show UninstallUninstalling dialog / notification?
+        mUninstallResult.setValue(new UninstallUninstalling(mTargetAppLabel, mIsClonedApp));
+
+        Bundle uninstallData = new Bundle();
+        uninstallData.putInt(EXTRA_UNINSTALL_ID, mUninstallId);
+        uninstallData.putString(EXTRA_PACKAGE_NAME, mTargetPackageName);
+        uninstallData.putBoolean(Intent.EXTRA_UNINSTALL_ALL_USERS, mUninstallFromAllUsers);
+        uninstallData.putCharSequence(EXTRA_APP_LABEL, mTargetAppLabel);
+        uninstallData.putBoolean(EXTRA_IS_CLONE_APP, mIsClonedApp);
+        Log.i(TAG, "Uninstalling extras = " + uninstallData);
+
+        // Get a PendingIntent for result broadcast and issue an uninstall request
+        Intent broadcastIntent = new Intent(BROADCAST_ACTION);
+        broadcastIntent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+        broadcastIntent.putExtra(EventResultPersister.EXTRA_ID, mUninstallId);
+        broadcastIntent.setPackage(mContext.getPackageName());
+
+        PendingIntent pendingIntent =
+            PendingIntent.getBroadcast(mContext, mUninstallId, broadcastIntent,
+                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
+
+        if (!startUninstall(mTargetPackageName, mUninstalledUser, pendingIntent,
+            mUninstallFromAllUsers, keepData)) {
+            handleUninstallResult(PackageInstaller.STATUS_FAILURE,
+                PackageManager.DELETE_FAILED_INTERNAL_ERROR, null, 0);
+        }
+    }
+
+    private void handleUninstallResult(int status, int legacyStatus, @Nullable String message,
+        int serviceId) {
+        if (mCallback != null) {
+            // The caller will be informed about the result via a callback
+            mCallback.onUninstallComplete(mTargetPackageName, legacyStatus, message);
+
+            // Since the caller already received the results, just finish the app at this point
+            mUninstallResult.setValue(null);
+            return;
+        }
+
+        boolean returnResult = mIntent.getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false);
+        if (returnResult || mCallingActivity != null) {
+            Intent intent = new Intent();
+            intent.putExtra(Intent.EXTRA_INSTALL_RESULT, legacyStatus);
+
+            if (status == PackageInstaller.STATUS_SUCCESS) {
+                UninstallSuccess.Builder successBuilder = new UninstallSuccess.Builder()
+                    .setResultIntent(intent)
+                    .setActivityResultCode(Activity.RESULT_OK);
+                mUninstallResult.setValue(successBuilder.build());
+            } else {
+                UninstallFailed.Builder failedBuilder = new UninstallFailed.Builder(true)
+                    .setResultIntent(intent)
+                    .setActivityResultCode(Activity.RESULT_FIRST_USER);
+                mUninstallResult.setValue(failedBuilder.build());
+            }
+            return;
+        }
+
+        // Caller did not want the result back. So, we either show a Toast, or a Notification.
+        if (status == PackageInstaller.STATUS_SUCCESS) {
+            UninstallSuccess.Builder successBuilder = new UninstallSuccess.Builder()
+                .setActivityResultCode(legacyStatus)
+                .setMessage(mIsClonedApp
+                    ? mContext.getString(R.string.uninstall_done_clone_app, mTargetAppLabel)
+                    : mContext.getString(R.string.uninstall_done_app, mTargetAppLabel));
+            mUninstallResult.setValue(successBuilder.build());
+        } else {
+            UninstallFailed.Builder failedBuilder = new UninstallFailed.Builder(false);
+            Notification.Builder uninstallFailedNotification = null;
+
+            NotificationChannel uninstallFailureChannel = new NotificationChannel(
+                UNINSTALL_FAILURE_CHANNEL,
+                mContext.getString(R.string.uninstall_failure_notification_channel),
+                NotificationManager.IMPORTANCE_DEFAULT);
+            mNotificationManager.createNotificationChannel(uninstallFailureChannel);
+
+            uninstallFailedNotification = new Notification.Builder(mContext,
+                UNINSTALL_FAILURE_CHANNEL);
+
+            UserHandle myUserHandle = Process.myUserHandle();
+            switch (legacyStatus) {
+                case PackageManager.DELETE_FAILED_DEVICE_POLICY_MANAGER -> {
+                    // Find out if the package is an active admin for some non-current user.
+                    UserHandle otherBlockingUserHandle =
+                        findUserOfDeviceAdmin(myUserHandle, mTargetPackageName);
+
+                    if (otherBlockingUserHandle == null) {
+                        Log.d(TAG, "Uninstall failed because " + mTargetPackageName
+                            + " is a device admin");
+
+                        addDeviceManagerButton(mContext, uninstallFailedNotification);
+                        setBigText(uninstallFailedNotification, mContext.getString(
+                            R.string.uninstall_failed_device_policy_manager));
+                    } else {
+                        Log.d(TAG, "Uninstall failed because " + mTargetPackageName
+                            + " is a device admin of user " + otherBlockingUserHandle);
+
+                        String userName =
+                            mContext.createContextAsUser(otherBlockingUserHandle, 0)
+                                .getSystemService(UserManager.class).getUserName();
+                        setBigText(uninstallFailedNotification, String.format(
+                            mContext.getString(
+                                R.string.uninstall_failed_device_policy_manager_of_user),
+                            userName));
+                    }
+                }
+                case PackageManager.DELETE_FAILED_OWNER_BLOCKED -> {
+                    UserHandle otherBlockingUserHandle = findBlockingUser(mTargetPackageName);
+                    boolean isProfileOfOrSame = isProfileOfOrSame(mUserManager, myUserHandle,
+                        otherBlockingUserHandle);
+
+                    if (isProfileOfOrSame) {
+                        addDeviceManagerButton(mContext, uninstallFailedNotification);
+                    } else {
+                        addManageUsersButton(mContext, uninstallFailedNotification);
+                    }
+
+                    String bigText = null;
+                    if (otherBlockingUserHandle == null) {
+                        Log.d(TAG, "Uninstall failed for " + mTargetPackageName +
+                            " with code " + status + " no blocking user");
+                    } else if (otherBlockingUserHandle == UserHandle.SYSTEM) {
+                        bigText = mContext.getString(
+                            R.string.uninstall_blocked_device_owner);
+                    } else {
+                        bigText = mContext.getString(mUninstallFromAllUsers ?
+                            R.string.uninstall_all_blocked_profile_owner
+                            : R.string.uninstall_blocked_profile_owner);
+                    }
+                    if (bigText != null) {
+                        setBigText(uninstallFailedNotification, bigText);
+                    }
+                }
+                default -> {
+                    Log.d(TAG, "Uninstall blocked for " + mTargetPackageName
+                        + " with legacy code " + legacyStatus);
+                }
+            }
+
+            uninstallFailedNotification.setContentTitle(
+                mContext.getString(R.string.uninstall_failed_app, mTargetAppLabel));
+            uninstallFailedNotification.setOngoing(false);
+            uninstallFailedNotification.setSmallIcon(R.drawable.ic_error);
+            failedBuilder.setUninstallNotification(mUninstallId,
+                uninstallFailedNotification.build());
+
+            mUninstallResult.setValue(failedBuilder.build());
+        }
+    }
+
+    /**
+     * @param myUserHandle {@link UserHandle} of the current user.
+     * @param packageName Name of the package being uninstalled.
+     * @return the {@link UserHandle} of the user in which a package is a device admin.
+     */
+    @Nullable
+    private UserHandle findUserOfDeviceAdmin(UserHandle myUserHandle, String packageName) {
+        for (UserHandle otherUserHandle : mUserManager.getUserHandles(true)) {
+            // We only catch the case when the user in question is neither the
+            // current user nor its profile.
+            if (isProfileOfOrSame(mUserManager, myUserHandle, otherUserHandle)) {
+                continue;
+            }
+            DevicePolicyManager dpm = mContext.createContextAsUser(otherUserHandle, 0)
+                    .getSystemService(DevicePolicyManager.class);
+            if (dpm.packageHasActiveAdmins(packageName)) {
+                return otherUserHandle;
+            }
+        }
+        return null;
+    }
+
+    /**
+     *
+     * @param packageName Name of the package being uninstalled.
+     * @return {@link UserHandle} of the user in which a package is blocked from being uninstalled.
+     */
+    @Nullable
+    private UserHandle findBlockingUser(String packageName) {
+        for (UserHandle otherUserHandle : mUserManager.getUserHandles(true)) {
+            // TODO (b/307399586): Add a negation when the logic of the method
+            //  is fixed
+            if (mPackageManager.canUserUninstall(packageName, otherUserHandle)) {
+                return otherUserHandle;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Set big text for the notification.
+     *
+     * @param builder The builder of the notification
+     * @param text The text to set.
+     */
+    private void setBigText(@NonNull Notification.Builder builder,
+        @NonNull CharSequence text) {
+        builder.setStyle(new Notification.BigTextStyle().bigText(text));
+    }
+
+    /**
+     * Add a button to the notification that links to the user management.
+     *
+     * @param context The context the notification is created in
+     * @param builder The builder of the notification
+     */
+    private void addManageUsersButton(@NonNull Context context,
+        @NonNull Notification.Builder builder) {
+        builder.addAction((new Notification.Action.Builder(
+            Icon.createWithResource(context, R.drawable.ic_settings_multiuser),
+            context.getString(R.string.manage_users),
+            PendingIntent.getActivity(context, 0, getUserSettingsIntent(),
+                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))).build());
+    }
+
+    private Intent getUserSettingsIntent() {
+        Intent intent = new Intent(Settings.ACTION_USER_SETTINGS);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_NEW_TASK);
+        return intent;
+    }
+
+    /**
+     * Add a button to the notification that links to the device policy management.
+     *
+     * @param context The context the notification is created in
+     * @param builder The builder of the notification
+     */
+    private void addDeviceManagerButton(@NonNull Context context,
+        @NonNull Notification.Builder builder) {
+        builder.addAction((new Notification.Action.Builder(
+            Icon.createWithResource(context, R.drawable.ic_lock),
+            context.getString(R.string.manage_device_administrators),
+            PendingIntent.getActivity(context, 0, getDeviceManagerIntent(),
+                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE))).build());
+    }
+
+    private Intent getDeviceManagerIntent() {
+        Intent intent = new Intent();
+        intent.setClassName("com.android.settings",
+            "com.android.settings.Settings$DeviceAdminSettingsActivity");
+        intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_NEW_TASK);
+        return intent;
+    }
+
+    /**
+     * Starts an uninstall for the given package.
+     *
+     * @return {@code true} if there was no exception while uninstalling. This does not represent
+     *     the result of the uninstall. Result will be made available in
+     *     {@link #handleUninstallResult(int, int, String, int)}
+     */
+    private boolean startUninstall(String packageName, UserHandle targetUser,
+        PendingIntent pendingIntent, boolean uninstallFromAllUsers, boolean keepData) {
+        int flags = uninstallFromAllUsers ? PackageManager.DELETE_ALL_USERS : 0;
+        flags |= keepData ? PackageManager.DELETE_KEEP_DATA : 0;
+        try {
+            mContext.createContextAsUser(targetUser, 0)
+                .getPackageManager().getPackageInstaller().uninstall(
+                    new VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST),
+                    flags, pendingIntent.getIntentSender());
+            return true;
+        } catch (IllegalArgumentException e) {
+            Log.e(TAG, "Failed to uninstall", e);
+            return false;
+        }
+    }
+
+    public void cancelInstall() {
+        if (mCallback != null) {
+            mCallback.onUninstallComplete(mTargetPackageName,
+                PackageManager.DELETE_FAILED_ABORTED, "Cancelled by user");
+        }
+    }
+
+    public MutableLiveData<UninstallStage> getUninstallResult() {
+        return mUninstallResult;
+    }
+
+    public static class CallerInfo {
+
+        private final String mActivityName;
+        private final int mUid;
+
+        public CallerInfo(String activityName, int uid) {
+            mActivityName = activityName;
+            mUid = uid;
+        }
+
+        public String getActivityName() {
+            return mActivityName;
+        }
+
+        public int getUid() {
+            return mUid;
+        }
     }
 }
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallAborted.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallAborted.java
new file mode 100644
index 0000000..9aea6b1
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallAborted.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2023 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
+ *
+ *      https://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.packageinstaller.v2.model.uninstallstagedata;
+
+import android.app.Activity;
+import com.android.packageinstaller.R;
+
+public class UninstallAborted extends UninstallStage {
+
+    public static final int ABORT_REASON_GENERIC_ERROR = 0;
+    public static final int ABORT_REASON_APP_UNAVAILABLE = 1;
+    public static final int ABORT_REASON_USER_NOT_ALLOWED = 2;
+    private final int mStage = UninstallStage.STAGE_ABORTED;
+    private final int mAbortReason;
+    private final int mDialogTitleResource;
+    private final int mDialogTextResource;
+    private final int mActivityResultCode = Activity.RESULT_FIRST_USER;
+
+    public UninstallAborted(int abortReason) {
+        mAbortReason = abortReason;
+        switch (abortReason) {
+            case ABORT_REASON_APP_UNAVAILABLE -> {
+                mDialogTitleResource = R.string.app_not_found_dlg_title;
+                mDialogTextResource = R.string.app_not_found_dlg_text;
+            }
+            case ABORT_REASON_USER_NOT_ALLOWED -> {
+                mDialogTitleResource = 0;
+                mDialogTextResource = R.string.user_is_not_allowed_dlg_text;
+            }
+            default -> {
+                mDialogTitleResource = 0;
+                mDialogTextResource = R.string.generic_error_dlg_text;
+            }
+        }
+    }
+
+    public int getAbortReason() {
+        return mAbortReason;
+    }
+
+    public int getActivityResultCode() {
+        return mActivityResultCode;
+    }
+
+    public int getDialogTitleResource() {
+        return mDialogTitleResource;
+    }
+
+    public int getDialogTextResource() {
+        return mDialogTextResource;
+    }
+
+    @Override
+    public int getStageCode() {
+        return mStage;
+    }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallFailed.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallFailed.java
new file mode 100644
index 0000000..6ed8883
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallFailed.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2023 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
+ *
+ *      https://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.packageinstaller.v2.model.uninstallstagedata;
+
+import android.app.Activity;
+import android.app.Notification;
+import android.content.Intent;
+
+public class UninstallFailed extends UninstallStage {
+
+    private final int mStage = UninstallStage.STAGE_FAILED;
+    private final boolean mReturnResult;
+    /**
+     * If the caller wants the result back, the intent will hold the uninstall failure status code
+     * and legacy code.
+     */
+    private final Intent mResultIntent;
+    /**
+     * When the user does not request a result back, this notification will be shown indicating the
+     * reason for uninstall failure.
+     */
+    private final Notification mUninstallNotification;
+    /**
+     * ID used to show {@link #mUninstallNotification}
+     */
+    private final int mUninstallId;
+    private final int mActivityResultCode;
+
+    public UninstallFailed(boolean returnResult, Intent resultIntent, int activityResultCode,
+        int uninstallId, Notification uninstallNotification) {
+        mReturnResult = returnResult;
+        mResultIntent = resultIntent;
+        mActivityResultCode = activityResultCode;
+        mUninstallId = uninstallId;
+        mUninstallNotification = uninstallNotification;
+    }
+
+    public boolean returnResult() {
+        return mReturnResult;
+    }
+
+    public Intent getResultIntent() {
+        return mResultIntent;
+    }
+
+    public int getActivityResultCode() {
+        return mActivityResultCode;
+    }
+
+    public Notification getUninstallNotification() {
+        return mUninstallNotification;
+    }
+
+    public int getUninstallId() {
+        return mUninstallId;
+    }
+
+    @Override
+    public int getStageCode() {
+        return mStage;
+    }
+
+    public static class Builder {
+
+        private final boolean mReturnResult;
+        private int mActivityResultCode = Activity.RESULT_CANCELED;
+        /**
+         * See {@link UninstallFailed#mResultIntent}
+         */
+        private Intent mResultIntent = null;
+        /**
+         * See {@link UninstallFailed#mUninstallNotification}
+         */
+        private Notification mUninstallNotification;
+        /**
+         * See {@link UninstallFailed#mUninstallId}
+         */
+        private int mUninstallId;
+
+        public Builder(boolean returnResult) {
+            mReturnResult = returnResult;
+        }
+
+        public Builder setUninstallNotification(int uninstallId, Notification notification) {
+            mUninstallId = uninstallId;
+            mUninstallNotification = notification;
+            return this;
+        }
+
+        public Builder setResultIntent(Intent intent) {
+            mResultIntent = intent;
+            return this;
+        }
+
+        public Builder setActivityResultCode(int resultCode) {
+            mActivityResultCode = resultCode;
+            return this;
+        }
+
+        public UninstallFailed build() {
+            return new UninstallFailed(mReturnResult, mResultIntent, mActivityResultCode,
+                mUninstallId, mUninstallNotification);
+        }
+    }
+}
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/package-info.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallReady.java
similarity index 64%
copy from packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/package-info.java
copy to packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallReady.java
index fd6a5dd..0108cb4 100644
--- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/package-info.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallReady.java
@@ -5,7 +5,7 @@
  * 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
+ *      https://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,
@@ -14,7 +14,14 @@
  * limitations under the License.
  */
 
-@GraphicsMode(GraphicsMode.Mode.NATIVE)
-package com.android.settingslib.spa.screenshot.widget.preference;
+package com.android.packageinstaller.v2.model.uninstallstagedata;
 
-import org.robolectric.annotation.GraphicsMode;
+public class UninstallReady extends UninstallStage {
+
+    private final int mStage = UninstallStage.STAGE_READY;
+
+    @Override
+    public int getStageCode() {
+        return mStage;
+    }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallSuccess.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallSuccess.java
new file mode 100644
index 0000000..5df6b02
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallSuccess.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2023 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
+ *
+ *      https://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.packageinstaller.v2.model.uninstallstagedata;
+
+import android.content.Intent;
+
+public class UninstallSuccess extends UninstallStage {
+
+    private final int mStage = UninstallStage.STAGE_SUCCESS;
+    private final String mMessage;
+    private final Intent mResultIntent;
+    private final int mActivityResultCode;
+
+    public UninstallSuccess(Intent resultIntent, int activityResultCode, String message) {
+        mResultIntent = resultIntent;
+        mActivityResultCode = activityResultCode;
+        mMessage = message;
+    }
+
+    public String getMessage() {
+        return mMessage;
+    }
+
+    public Intent getResultIntent() {
+        return mResultIntent;
+    }
+
+    public int getActivityResultCode() {
+        return mActivityResultCode;
+    }
+
+    @Override
+    public int getStageCode() {
+        return mStage;
+    }
+
+    public static class Builder {
+
+        private Intent mResultIntent;
+        private int mActivityResultCode;
+        private String mMessage;
+
+        public Builder() {
+        }
+
+        public Builder setResultIntent(Intent intent) {
+            mResultIntent = intent;
+            return this;
+        }
+
+        public Builder setActivityResultCode(int resultCode) {
+            mActivityResultCode = resultCode;
+            return this;
+        }
+
+        public Builder setMessage(String message) {
+            mMessage = message;
+            return this;
+        }
+
+        public UninstallSuccess build() {
+            return new UninstallSuccess(mResultIntent, mActivityResultCode, mMessage);
+        }
+    }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallUninstalling.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallUninstalling.java
new file mode 100644
index 0000000..f5156cb
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallUninstalling.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 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
+ *
+ *      https://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.packageinstaller.v2.model.uninstallstagedata;
+
+public class UninstallUninstalling extends UninstallStage {
+
+    private final int mStage = UninstallStage.STAGE_UNINSTALLING;
+
+    private final CharSequence mAppLabel;
+    private final boolean mIsCloneUser;
+
+    public UninstallUninstalling(CharSequence appLabel, boolean isCloneUser) {
+        mAppLabel = appLabel;
+        mIsCloneUser = isCloneUser;
+    }
+
+    public CharSequence getAppLabel() {
+        return mAppLabel;
+    }
+
+    public boolean isCloneUser() {
+        return mIsCloneUser;
+    }
+
+    @Override
+    public int getStageCode() {
+        return mStage;
+    }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallUserActionRequired.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallUserActionRequired.java
new file mode 100644
index 0000000..b600149
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/uninstallstagedata/UninstallUserActionRequired.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2023 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
+ *
+ *      https://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.packageinstaller.v2.model.uninstallstagedata;
+
+public class UninstallUserActionRequired extends UninstallStage {
+
+    private final int mStage = UninstallStage.STAGE_USER_ACTION_REQUIRED;
+    private final String mTitle;
+    private final String mMessage;
+    private final long mAppDataSize;
+
+    public UninstallUserActionRequired(String title, String message, long appDataSize) {
+        mTitle = title;
+        mMessage = message;
+        mAppDataSize = appDataSize;
+    }
+
+    public String getTitle() {
+        return mTitle;
+    }
+
+    public String getMessage() {
+        return mMessage;
+    }
+
+    public long getAppDataSize() {
+        return mAppDataSize;
+    }
+
+    @Override
+    public int getStageCode() {
+        return mStage;
+    }
+
+    public static class Builder {
+
+        private String mTitle;
+        private String mMessage;
+        private long mAppDataSize = 0;
+
+        public Builder setTitle(String title) {
+            mTitle = title;
+            return this;
+        }
+
+        public Builder setMessage(String message) {
+            mMessage = message;
+            return this;
+        }
+
+        public Builder setAppDataSize(long appDataSize) {
+            mAppDataSize = appDataSize;
+            return this;
+        }
+
+        public UninstallUserActionRequired build() {
+            return new UninstallUserActionRequired(mTitle, mMessage, mAppDataSize);
+        }
+    }
+}
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/package-info.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallActionListener.java
similarity index 71%
rename from packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/package-info.java
rename to packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallActionListener.java
index fd6a5dd..b8a9355 100644
--- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/package-info.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallActionListener.java
@@ -5,7 +5,7 @@
  * 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
+ *      https://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,
@@ -14,7 +14,11 @@
  * limitations under the License.
  */
 
-@GraphicsMode(GraphicsMode.Mode.NATIVE)
-package com.android.settingslib.spa.screenshot.widget.preference;
+package com.android.packageinstaller.v2.ui;
 
-import org.robolectric.annotation.GraphicsMode;
+public interface UninstallActionListener {
+
+    void onPositiveResponse(boolean keepData);
+
+    void onNegativeResponse();
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.java
index 5d47da1..7638e91 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/UninstallLaunch.java
@@ -16,26 +16,47 @@
 
 package com.android.packageinstaller.v2.ui;
 
+import static android.os.Process.INVALID_UID;
 import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
 
+import android.app.Activity;
+import android.app.NotificationManager;
+import android.content.Intent;
 import android.os.Bundle;
+import android.util.Log;
+import android.widget.Toast;
 import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
 import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
 import androidx.lifecycle.ViewModelProvider;
 import com.android.packageinstaller.v2.model.UninstallRepository;
+import com.android.packageinstaller.v2.model.UninstallRepository.CallerInfo;
+import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallAborted;
+import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallFailed;
+import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallStage;
+import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallSuccess;
+import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallUninstalling;
+import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallUserActionRequired;
+import com.android.packageinstaller.v2.ui.fragments.UninstallConfirmationFragment;
+import com.android.packageinstaller.v2.ui.fragments.UninstallErrorFragment;
+import com.android.packageinstaller.v2.ui.fragments.UninstallUninstallingFragment;
 import com.android.packageinstaller.v2.viewmodel.UninstallViewModel;
 import com.android.packageinstaller.v2.viewmodel.UninstallViewModelFactory;
 
-public class UninstallLaunch extends FragmentActivity{
+public class UninstallLaunch extends FragmentActivity implements UninstallActionListener {
 
     public static final String EXTRA_CALLING_PKG_UID =
         UninstallLaunch.class.getPackageName() + ".callingPkgUid";
     public static final String EXTRA_CALLING_ACTIVITY_NAME =
         UninstallLaunch.class.getPackageName() + ".callingActivityName";
     public static final String TAG = UninstallLaunch.class.getSimpleName();
+    private static final String TAG_DIALOG = "dialog";
 
     private UninstallViewModel mUninstallViewModel;
     private UninstallRepository mUninstallRepository;
+    private FragmentManager mFragmentManager;
+    private NotificationManager mNotificationManager;
 
     @Override
     protected void onCreate(@Nullable Bundle savedInstanceState) {
@@ -45,9 +66,102 @@
         // be stale, if e.g. the app was uninstalled while the activity was destroyed.
         super.onCreate(null);
 
+        mFragmentManager = getSupportFragmentManager();
+        mNotificationManager = getSystemService(NotificationManager.class);
+
         mUninstallRepository = new UninstallRepository(getApplicationContext());
         mUninstallViewModel = new ViewModelProvider(this,
             new UninstallViewModelFactory(this.getApplication(), mUninstallRepository)).get(
             UninstallViewModel.class);
+
+        Intent intent = getIntent();
+        CallerInfo callerInfo = new CallerInfo(
+            intent.getStringExtra(EXTRA_CALLING_ACTIVITY_NAME),
+            intent.getIntExtra(EXTRA_CALLING_PKG_UID, INVALID_UID));
+        mUninstallViewModel.preprocessIntent(intent, callerInfo);
+
+        mUninstallViewModel.getCurrentUninstallStage().observe(this,
+            this::onUninstallStageChange);
+    }
+
+    /**
+     * Main controller of the UI. This method shows relevant dialogs / fragments based on the
+     * uninstall stage
+     */
+    private void onUninstallStageChange(UninstallStage uninstallStage) {
+        if (uninstallStage.getStageCode() == UninstallStage.STAGE_ABORTED) {
+            UninstallAborted aborted = (UninstallAborted) uninstallStage;
+            if (aborted.getAbortReason() == UninstallAborted.ABORT_REASON_APP_UNAVAILABLE ||
+                aborted.getAbortReason() == UninstallAborted.ABORT_REASON_USER_NOT_ALLOWED) {
+                UninstallErrorFragment errorDialog = new UninstallErrorFragment(aborted);
+                showDialogInner(errorDialog);
+            } else {
+                setResult(aborted.getActivityResultCode(), null, true);
+            }
+        } else if (uninstallStage.getStageCode() == UninstallStage.STAGE_USER_ACTION_REQUIRED) {
+            UninstallUserActionRequired uar = (UninstallUserActionRequired) uninstallStage;
+            UninstallConfirmationFragment confirmationDialog = new UninstallConfirmationFragment(
+                uar);
+            showDialogInner(confirmationDialog);
+        } else if (uninstallStage.getStageCode() == UninstallStage.STAGE_UNINSTALLING) {
+            // TODO: This shows a fragment whether or not user requests a result or not.
+            //  Originally, if the user does not request a result, we used to show a notification.
+            //  And a fragment if the user requests a result back. Should we consolidate and
+            //  show a fragment always?
+            UninstallUninstalling uninstalling = (UninstallUninstalling) uninstallStage;
+            UninstallUninstallingFragment uninstallingDialog = new UninstallUninstallingFragment(
+                uninstalling);
+            showDialogInner(uninstallingDialog);
+        } else if (uninstallStage.getStageCode() == UninstallStage.STAGE_FAILED) {
+            UninstallFailed failed = (UninstallFailed) uninstallStage;
+            if (!failed.returnResult()) {
+                mNotificationManager.notify(failed.getUninstallId(),
+                    failed.getUninstallNotification());
+            }
+            setResult(failed.getActivityResultCode(), failed.getResultIntent(), true);
+        } else if (uninstallStage.getStageCode() == UninstallStage.STAGE_SUCCESS) {
+            UninstallSuccess success = (UninstallSuccess) uninstallStage;
+            if (success.getMessage() != null) {
+                Toast.makeText(this, success.getMessage(), Toast.LENGTH_LONG).show();
+            }
+            setResult(success.getActivityResultCode(), success.getResultIntent(), true);
+        } else {
+            Log.e(TAG, "Invalid stage: " + uninstallStage.getStageCode());
+            showDialogInner(null);
+        }
+    }
+
+    /**
+     * Replace any visible dialog by the dialog returned by InstallRepository
+     *
+     * @param newDialog The new dialog to display
+     */
+    private void showDialogInner(DialogFragment newDialog) {
+        DialogFragment currentDialog = (DialogFragment) mFragmentManager.findFragmentByTag(
+            TAG_DIALOG);
+        if (currentDialog != null) {
+            currentDialog.dismissAllowingStateLoss();
+        }
+        if (newDialog != null) {
+            newDialog.show(mFragmentManager, TAG_DIALOG);
+        }
+    }
+
+    public void setResult(int resultCode, Intent data, boolean shouldFinish) {
+        super.setResult(resultCode, data);
+        if (shouldFinish) {
+            finish();
+        }
+    }
+
+    @Override
+    public void onPositiveResponse(boolean keepData) {
+        mUninstallViewModel.initiateUninstall(keepData);
+    }
+
+    @Override
+    public void onNegativeResponse() {
+        mUninstallViewModel.cancelInstall();
+        setResult(Activity.RESULT_FIRST_USER, null, true);
     }
 }
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallConfirmationFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallConfirmationFragment.java
new file mode 100644
index 0000000..1b0885e
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallConfirmationFragment.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2023 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
+ *
+ *      https://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.packageinstaller.v2.ui.fragments;
+
+import static android.text.format.Formatter.formatFileSize;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+import com.android.packageinstaller.R;
+import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallUserActionRequired;
+import com.android.packageinstaller.v2.ui.UninstallActionListener;
+
+/**
+ * Dialog to show while requesting user confirmation for uninstalling an app.
+ */
+public class UninstallConfirmationFragment extends DialogFragment {
+
+    private final UninstallUserActionRequired mDialogData;
+    private UninstallActionListener mUninstallActionListener;
+
+    private CheckBox mKeepData;
+
+    public UninstallConfirmationFragment(UninstallUserActionRequired dialogData) {
+        mDialogData = dialogData;
+    }
+
+    @Override
+    public void onAttach(@NonNull Context context) {
+        super.onAttach(context);
+        mUninstallActionListener = (UninstallActionListener) context;
+    }
+
+    @NonNull
+    @Override
+    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+        AlertDialog.Builder builder = new AlertDialog.Builder(requireContext())
+            .setTitle(mDialogData.getTitle())
+            .setPositiveButton(R.string.ok,
+                (dialogInt, which) -> mUninstallActionListener.onPositiveResponse(
+                    mKeepData != null && mKeepData.isChecked()))
+            .setNegativeButton(R.string.cancel,
+                (dialogInt, which) -> mUninstallActionListener.onNegativeResponse());
+
+        long appDataSize = mDialogData.getAppDataSize();
+        if (appDataSize == 0) {
+            builder.setMessage(mDialogData.getMessage());
+        } else {
+            View dialogView = getLayoutInflater().inflate(R.layout.uninstall_content_view, null);
+
+            ((TextView) dialogView.requireViewById(R.id.message)).setText(mDialogData.getMessage());
+            mKeepData = dialogView.requireViewById(R.id.keepData);
+            mKeepData.setVisibility(View.VISIBLE);
+            mKeepData.setText(getString(R.string.uninstall_keep_data,
+                formatFileSize(getContext(), appDataSize)));
+
+            builder.setView(dialogView);
+        }
+        return builder.create();
+    }
+
+    @Override
+    public void onCancel(@NonNull DialogInterface dialog) {
+        super.onCancel(dialog);
+        mUninstallActionListener.onNegativeResponse();
+    }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallErrorFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallErrorFragment.java
new file mode 100644
index 0000000..305daba
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallErrorFragment.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2023 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
+ *
+ *      https://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.packageinstaller.v2.ui.fragments;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+import com.android.packageinstaller.R;
+import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallAborted;
+import com.android.packageinstaller.v2.ui.UninstallActionListener;
+
+/**
+ * Dialog to show when an app cannot be uninstalled
+ */
+public class UninstallErrorFragment extends DialogFragment {
+
+    private final UninstallAborted mDialogData;
+    private UninstallActionListener mUninstallActionListener;
+
+    public UninstallErrorFragment(UninstallAborted dialogData) {
+        mDialogData = dialogData;
+    }
+
+    @Override
+    public void onAttach(@NonNull Context context) {
+        super.onAttach(context);
+        mUninstallActionListener = (UninstallActionListener) context;
+    }
+
+    @NonNull
+    @Override
+    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+        AlertDialog.Builder builder = new AlertDialog.Builder(requireContext())
+            .setMessage(mDialogData.getDialogTextResource())
+            .setNegativeButton(R.string.ok,
+                (dialogInt, which) -> mUninstallActionListener.onNegativeResponse());
+
+        if (mDialogData.getDialogTitleResource() != 0) {
+            builder.setTitle(mDialogData.getDialogTitleResource());
+        }
+        return builder.create();
+    }
+
+    @Override
+    public void onCancel(@NonNull DialogInterface dialog) {
+        super.onCancel(dialog);
+        mUninstallActionListener.onNegativeResponse();
+    }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallUninstallingFragment.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallUninstallingFragment.java
new file mode 100644
index 0000000..23cc421
--- /dev/null
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/fragments/UninstallUninstallingFragment.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2023 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
+ *
+ *      https://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.packageinstaller.v2.ui.fragments;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+import androidx.fragment.app.DialogFragment;
+import com.android.packageinstaller.R;
+import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallUninstalling;
+
+/**
+ * Dialog to show that the app is uninstalling.
+ */
+public class UninstallUninstallingFragment extends DialogFragment {
+
+    UninstallUninstalling mDialogData;
+
+    public UninstallUninstallingFragment(UninstallUninstalling dialogData) {
+        mDialogData = dialogData;
+    }
+
+    @NonNull
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        AlertDialog.Builder builder = new AlertDialog.Builder(requireContext())
+            .setCancelable(false);
+        if (mDialogData.isCloneUser()) {
+            builder.setTitle(requireContext().getString(R.string.uninstalling_cloned_app,
+                mDialogData.getAppLabel()));
+        } else {
+            builder.setTitle(requireContext().getString(R.string.uninstalling_app,
+                mDialogData.getAppLabel()));
+        }
+        Dialog dialog = builder.create();
+        dialog.setCanceledOnTouchOutside(false);
+
+        return dialog;
+    }
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModel.java b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModel.java
index 8187ea5..3f7bce8 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModel.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/viewmodel/UninstallViewModel.java
@@ -17,17 +17,53 @@
 package com.android.packageinstaller.v2.viewmodel;
 
 import android.app.Application;
+import android.content.Intent;
 import androidx.annotation.NonNull;
 import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.MediatorLiveData;
+import androidx.lifecycle.MutableLiveData;
 import com.android.packageinstaller.v2.model.UninstallRepository;
+import com.android.packageinstaller.v2.model.UninstallRepository.CallerInfo;
+import com.android.packageinstaller.v2.model.uninstallstagedata.UninstallStage;
 
 public class UninstallViewModel extends AndroidViewModel {
 
     private static final String TAG = UninstallViewModel.class.getSimpleName();
     private final UninstallRepository mRepository;
+    private final MediatorLiveData<UninstallStage> mCurrentUninstallStage =
+        new MediatorLiveData<>();
 
     public UninstallViewModel(@NonNull Application application, UninstallRepository repository) {
         super(application);
         mRepository = repository;
     }
+
+    public MutableLiveData<UninstallStage> getCurrentUninstallStage() {
+        return mCurrentUninstallStage;
+    }
+
+    public void preprocessIntent(Intent intent, CallerInfo callerInfo) {
+        UninstallStage stage = mRepository.performPreUninstallChecks(intent, callerInfo);
+        if (stage.getStageCode() != UninstallStage.STAGE_ABORTED) {
+            stage = mRepository.generateUninstallDetails();
+        }
+        mCurrentUninstallStage.setValue(stage);
+    }
+
+    public void initiateUninstall(boolean keepData) {
+        mRepository.initiateUninstall(keepData);
+        // Since uninstall is an async operation, we will get the uninstall result later in time.
+        // Result of the uninstall will be set in UninstallRepository#mUninstallResult.
+        // As such, mCurrentUninstallStage will need to add another MutableLiveData
+        // as a data source
+        mCurrentUninstallStage.addSource(mRepository.getUninstallResult(), uninstallStage -> {
+            if (uninstallStage != null) {
+                mCurrentUninstallStage.setValue(uninstallStage);
+            }
+        });
+    }
+
+    public void cancelInstall() {
+        mRepository.cancelInstall();
+    }
 }
diff --git a/packages/SettingsLib/ProfileSelector/res/values/styles.xml b/packages/SettingsLib/ProfileSelector/res/values/styles.xml
index 0b703c9..365dcb2 100644
--- a/packages/SettingsLib/ProfileSelector/res/values/styles.xml
+++ b/packages/SettingsLib/ProfileSelector/res/values/styles.xml
@@ -37,5 +37,6 @@
         <item name="tabIndicatorAnimationDuration">0</item>
         <item name="tabTextAppearance">@style/SettingsLibTabsTextAppearance</item>
         <item name="tabTextColor">@color/settingslib_tabs_text_color</item>
+        <item name="tabRippleColor">@android:color/transparent</item>
     </style>
 </resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/config/robolectric.properties b/packages/SettingsLib/Spa/screenshot/robotests/config/robolectric.properties
index 83d7549..23fdc01 100644
--- a/packages/SettingsLib/Spa/screenshot/robotests/config/robolectric.properties
+++ b/packages/SettingsLib/Spa/screenshot/robotests/config/robolectric.properties
@@ -12,4 +12,5 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 #
-sdk=NEWEST_SDK
\ No newline at end of file
+sdk=NEWEST_SDK
+graphicsMode=NATIVE
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/button/package-info.java b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/button/package-info.java
deleted file mode 100644
index 8e55695..0000000
--- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/button/package-info.java
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * Copyright (C) 2023 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.
- */
-
-@GraphicsMode(GraphicsMode.Mode.NATIVE)
-package com.android.settingslib.spa.screenshot.widget.button;
-
-import org.robolectric.annotation.GraphicsMode;
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/chart/package-info.java b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/chart/package-info.java
deleted file mode 100644
index afe3f07..0000000
--- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/chart/package-info.java
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * Copyright (C) 2023 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.
- */
-
-@GraphicsMode(GraphicsMode.Mode.NATIVE)
-package com.android.settingslib.spa.screenshot.widget.chart;
-
-import org.robolectric.annotation.GraphicsMode;
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/illustration/package-info.java b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/illustration/package-info.java
deleted file mode 100644
index 0089c2e..0000000
--- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/illustration/package-info.java
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * Copyright (C) 2023 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.
- */
-
-@GraphicsMode(GraphicsMode.Mode.NATIVE)
-package com.android.settingslib.spa.screenshot.widget.illustration;
-
-import org.robolectric.annotation.GraphicsMode;
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/ui/package-info.java b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/ui/package-info.java
deleted file mode 100644
index 45210ab..0000000
--- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/ui/package-info.java
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * Copyright (C) 2023 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.
- */
-
-@GraphicsMode(GraphicsMode.Mode.NATIVE)
-package com.android.settingslib.spa.screenshot.widget.ui;
-
-import org.robolectric.annotation.GraphicsMode;
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/common/UserProfilePager.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/common/UserProfilePager.kt
index 223e99e..5679694 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/common/UserProfilePager.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/common/UserProfilePager.kt
@@ -72,7 +72,8 @@
 
 private fun UserManager.showInSettings(userInfo: UserInfo): Int {
     val userProperties = getUserProperties(userInfo.userHandle)
-    return if (userInfo.isQuietModeEnabled && userProperties.hideInSettingsInQuietMode) {
+    return if (userInfo.isQuietModeEnabled && userProperties.showInQuietMode
+            == UserProperties.SHOW_IN_QUIET_MODE_HIDDEN) {
         UserProperties.SHOW_IN_SETTINGS_NO
     } else {
         userProperties.showInSettings
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
index 5dacba5..52b51d7 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
@@ -413,12 +413,8 @@
      */
     @NonNull
     List<MediaDevice> getSelectedMediaDevices() {
-        if (TextUtils.isEmpty(mPackageName)) {
-            Log.w(TAG, "getSelectedMediaDevices() package name is null or empty!");
-            return Collections.emptyList();
-        }
+        RoutingSessionInfo info = getRoutingSessionInfo();
 
-        final RoutingSessionInfo info = getRoutingSessionInfo();
         if (info == null) {
             Log.w(TAG, "getSelectedMediaDevices() cannot find selectable MediaDevice from : "
                     + mPackageName);
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaOutputConstants.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaOutputConstants.java
index 3514932..02ec90d 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/MediaOutputConstants.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaOutputConstants.java
@@ -48,6 +48,17 @@
             "com.android.systemui.action.LAUNCH_MEDIA_OUTPUT_DIALOG";
 
     /**
+     * An intent action to launch a media output dialog without any app or playback metadata, which
+     * only controls system routing.
+     *
+     * <p>System routes are those provided by the system, such as built-in speakers, wired headsets,
+     * bluetooth devices, and other outputs that require the app to feed media samples to the
+     * framework.
+     */
+    public static final String ACTION_LAUNCH_SYSTEM_MEDIA_OUTPUT_DIALOG =
+            "com.android.systemui.action.LAUNCH_SYSTEM_MEDIA_OUTPUT_DIALOG";
+
+    /**
      * An intent action to launch media output broadcast dialog.
      */
     public static final String ACTION_LAUNCH_MEDIA_OUTPUT_BROADCAST_DIALOG =
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ProviderTileTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ProviderTileTest.java
index faccf2f..90140d4 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ProviderTileTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ProviderTileTest.java
@@ -259,13 +259,6 @@
     }
 
     @Test
-    public void isSearchable_noMetadata_isTrue() {
-        final Tile tile = new ProviderTile(mProviderInfo, "category", null);
-
-        assertThat(tile.isSearchable()).isTrue();
-    }
-
-    @Test
     public void isSearchable_notSet_isTrue() {
         final Tile tile = new ProviderTile(mProviderInfo, "category", mMetaData);
 
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/license/LicenseHtmlGeneratorFromXmlTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/license/LicenseHtmlGeneratorFromXmlTest.java
index 0cabab2..542f101 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/license/LicenseHtmlGeneratorFromXmlTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/license/LicenseHtmlGeneratorFromXmlTest.java
@@ -168,10 +168,10 @@
     private static final String EXPECTED_NEW_HTML_STRING = HTML_HEAD_STRING + HTML_NEW_BODY_STRING;
 
     private static final String EXPECTED_OLD_HTML_STRING_WITH_CUSTOM_HEADING =
-            HTML_HEAD_STRING + HTML_CUSTOM_HEADING + "\n" + HTML_OLD_BODY_STRING;
+            HTML_HEAD_STRING + HTML_CUSTOM_HEADING + "\n<br/>\n" + HTML_OLD_BODY_STRING;
 
     private static final String EXPECTED_NEW_HTML_STRING_WITH_CUSTOM_HEADING =
-            HTML_HEAD_STRING + HTML_CUSTOM_HEADING + "\n" + HTML_NEW_BODY_STRING;
+            HTML_HEAD_STRING + HTML_CUSTOM_HEADING + "\n<br/>\n" + HTML_NEW_BODY_STRING;
 
     @Test
     public void testParseValidXmlStream() throws XmlPullParserException, IOException {
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/BannerMessagePreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/BannerMessagePreferenceTest.java
index 721e69d..f0f53d6 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/BannerMessagePreferenceTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/BannerMessagePreferenceTest.java
@@ -39,9 +39,9 @@
 
 import androidx.annotation.ColorRes;
 import androidx.preference.PreferenceViewHolder;
-import com.android.settingslib.widget.preference.banner.R;
 
 import com.android.settingslib.testutils.OverpoweredReflectionHelper;
+import com.android.settingslib.widget.preference.banner.R;
 
 import org.junit.Before;
 import org.junit.Test;
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index e218308..0a71cda 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -1044,6 +1044,7 @@
                   android:exported="true">
             <intent-filter android:priority="1">
                 <action android:name="com.android.systemui.action.LAUNCH_MEDIA_OUTPUT_DIALOG" />
+                <action android:name="com.android.systemui.action.LAUNCH_SYSTEM_MEDIA_OUTPUT_DIALOG" />
                 <action android:name="com.android.systemui.action.LAUNCH_MEDIA_OUTPUT_BROADCAST_DIALOG" />
                 <action android:name="com.android.systemui.action.DISMISS_MEDIA_OUTPUT_DIALOG" />
             </intent-filter>
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index a745ab5..a9dc145 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -143,8 +143,17 @@
 }
 
 flag {
+   name: "theme_overlay_controller_wakefulness_deprecation"
+   namespace: "systemui"
+   description: "Replacing WakefulnessLifecycle by KeyguardTransitionInteractor in "
+        "ThemOverlayController to mitigate flickering when locking the device"
+   bug: "308676488"
+}
+
+flag {
    name: "media_in_scene_container"
    namespace: "systemui"
    description: "Enable media in the scene container framework"
    bug: "296122467"
 }
+
diff --git a/packages/SystemUI/res/anim/instant_fade_out.xml b/packages/SystemUI/res/anim/instant_fade_out.xml
new file mode 100644
index 0000000..800420b
--- /dev/null
+++ b/packages/SystemUI/res/anim/instant_fade_out.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2015 The Android Open Source Project
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+
+<alpha xmlns:android="http://schemas.android.com/apk/res/android"
+    android:fromAlpha="1.0"
+    android:toAlpha="0.0"
+    android:interpolator="@android:interpolator/linear_out_slow_in"
+    android:duration="0"/>
+
diff --git a/packages/SystemUI/res/layout/screen_record_options.xml b/packages/SystemUI/res/layout/screen_record_options.xml
index d9f4b79..8916e42 100644
--- a/packages/SystemUI/res/layout/screen_record_options.xml
+++ b/packages/SystemUI/res/layout/screen_record_options.xml
@@ -47,6 +47,7 @@
             android:layout_weight="0"
             android:layout_gravity="end"
             android:id="@+id/screenrecord_audio_switch"
+            android:contentDescription="@string/screenrecord_audio_label"
             style="@style/ScreenRecord.Switch"
             android:importantForAccessibility="yes"/>
     </LinearLayout>
@@ -79,6 +80,7 @@
             android:minWidth="48dp"
             android:layout_height="48dp"
             android:id="@+id/screenrecord_taps_switch"
+            android:contentDescription="@string/screenrecord_taps_label"
             style="@style/ScreenRecord.Switch"
             android:importantForAccessibility="yes"/>
     </LinearLayout>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index daf6cb3..b20fa89 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1853,51 +1853,51 @@
     <string name="keyboard_shortcut_search_category_current_app">Current app</string>
 
     <!-- User visible title for the keyboard shortcut that triggers the notification shade. [CHAR LIMIT=70] -->
-    <string name="group_system_access_notification_shade">Access notification shade</string>
+    <string name="group_system_access_notification_shade">View notifications</string>
     <!-- User visible title for the keyboard shortcut that takes a full screenshot. [CHAR LIMIT=70] -->
-    <string name="group_system_full_screenshot">Take a full screenshot</string>
+    <string name="group_system_full_screenshot">Take screenshot</string>
     <!-- User visible title for the keyboard shortcut that access list of system / apps shortcuts. [CHAR LIMIT=70] -->
-    <string name="group_system_access_system_app_shortcuts">Access list of system / apps shortcuts</string>
+    <string name="group_system_access_system_app_shortcuts">Show shortcuts</string>
     <!-- User visible title for the keyboard shortcut that goes back to previous state. [CHAR LIMIT=70] -->
-    <string name="group_system_go_back">Back: go back to previous state (back button)</string>
+    <string name="group_system_go_back">Go back</string>
     <!-- User visible title for the keyboard shortcut that takes the user to the home screen. [CHAR LIMIT=70] -->
-    <string name="group_system_access_home_screen">Access home screen</string>
+    <string name="group_system_access_home_screen">Go to home screen</string>
     <!-- User visible title for the keyboard shortcut that triggers overview of open apps. [CHAR LIMIT=70] -->
-    <string name="group_system_overview_open_apps">Overview of open apps</string>
+    <string name="group_system_overview_open_apps">View recent apps</string>
     <!-- User visible title for the keyboard shortcut that cycles through recent apps (forward). [CHAR LIMIT=70] -->
-    <string name="group_system_cycle_forward">Cycle through recent apps (forward)</string>
+    <string name="group_system_cycle_forward">Cycle forward through recent apps</string>
     <!-- User visible title for the keyboard shortcut that cycles through recent apps (back). [CHAR LIMIT=70] -->
-    <string name="group_system_cycle_back">Cycle through recent apps (back)</string>
+    <string name="group_system_cycle_back">Cycle backward through recent apps</string>
     <!-- User visible title for the keyboard shortcut that accesses list of all apps and search. [CHAR LIMIT=70] -->
-    <string name="group_system_access_all_apps_search">Access list of all apps and search (i.e. Search/Launcher)</string>
+    <string name="group_system_access_all_apps_search">Open apps list</string>
     <!-- User visible title for the keyboard shortcut that hides and (re)showes taskbar. [CHAR LIMIT=70] -->
-    <string name="group_system_hide_reshow_taskbar">Hide and (re)show taskbar</string>
-    <!-- User visible title for the keyboard shortcut that accesses system settings. [CHAR LIMIT=70] -->
-    <string name="group_system_access_system_settings">Access system settings</string>
-    <!-- User visible title for the keyboard shortcut that accesses Google Assistant. [CHAR LIMIT=70] -->
-    <string name="group_system_access_google_assistant">Access Google Assistant</string>
+    <string name="group_system_hide_reshow_taskbar">Show taskbar</string>
+    <!-- User visible title for the keyboard shortcut that accesses [system] settings. [CHAR LIMIT=70] -->
+    <string name="group_system_access_system_settings">Open settings</string>
+    <!-- User visible title for the keyboard shortcut that accesses Assistant app. [CHAR LIMIT=70] -->
+    <string name="group_system_access_google_assistant">Open assistant</string>
     <!-- User visible title for the keyboard shortcut that locks screen. [CHAR LIMIT=70] -->
     <string name="group_system_lock_screen">Lock screen</string>
     <!-- User visible title for the keyboard shortcut that pulls up Notes app for quick memo. [CHAR LIMIT=70] -->
-    <string name="group_system_quick_memo">Pull up Notes app for quick memo</string>
+    <string name="group_system_quick_memo">Open notes</string>
 
     <!-- User visible title for the system multitasking keyboard shortcuts list. [CHAR LIMIT=70] -->
     <string name="keyboard_shortcut_group_system_multitasking">System multitasking</string>
     <!-- User visible title for the keyboard shortcut that enters split screen with current app to RHS [CHAR LIMIT=70] -->
-    <string name="system_multitasking_rhs">Enter Split screen with current app to RHS</string>
+    <string name="system_multitasking_rhs">Enter split screen with current app to RHS</string>
     <!-- User visible title for the keyboard shortcut that enters split screen with current app to LHS [CHAR LIMIT=70] -->
-    <string name="system_multitasking_lhs">Enter Split screen with current app to LHS</string>
+    <string name="system_multitasking_lhs">Enter split screen with current app to LHS</string>
     <!-- User visible title for the keyboard shortcut that switches from split screen to full screen [CHAR LIMIT=70] -->
-    <string name="system_multitasking_full_screen">Switch from Split screen to full screen</string>
+    <string name="system_multitasking_full_screen">Switch from split screen to full screen</string>
     <!-- User visible title for the keyboard shortcut that replaces an app from one to another during split screen [CHAR LIMIT=70] -->
-    <string name="system_multitasking_replace">During Split screen: replace an app from one to another</string>
+    <string name="system_multitasking_replace">During split screen: replace an app from one to another</string>
 
     <!-- User visible title for the input keyboard shortcuts list. [CHAR LIMIT=70] -->
     <string name="keyboard_shortcut_group_input">Input</string>
     <!-- User visible title for the keyboard shortcut that switches input language (next language). [CHAR LIMIT=70] -->
-    <string name="input_switch_input_language_next">Switch input language (next language)</string>
+    <string name="input_switch_input_language_next">Switch to next language</string>
     <!-- User visible title for the keyboard shortcut that switches input language (previous language). [CHAR LIMIT=70] -->
-    <string name="input_switch_input_language_previous">Switch input language (previous language)</string>
+    <string name="input_switch_input_language_previous">Switch to previous language</string>
     <!-- User visible title for the keyboard shortcut that accesses emoji. [CHAR LIMIT=70] -->
     <string name="input_access_emoji">Access emoji</string>
     <!-- User visible title for the keyboard shortcut that accesses voice typing. [CHAR LIMIT=70] -->
@@ -1905,8 +1905,8 @@
 
     <!-- User visible title for the system-wide applications keyboard shortcuts list. [CHAR LIMIT=70] -->
     <string name="keyboard_shortcut_group_applications">Applications</string>
-    <!-- User visible title for the keyboard shortcut that takes the user to the assist app. [CHAR LIMIT=70] -->
-    <string name="keyboard_shortcut_group_applications_assist">Assist</string>
+    <!-- User visible title for the keyboard shortcut that takes the user to the assistant app. [CHAR LIMIT=70] -->
+    <string name="keyboard_shortcut_group_applications_assist">Assistant</string>
     <!-- User visible title for the keyboard shortcut that takes the user to the browser app. [CHAR LIMIT=70] -->
     <string name="keyboard_shortcut_group_applications_browser">Browser (Chrome as default)</string>
     <!-- User visible title for the keyboard shortcut that takes the user to the contacts app. [CHAR LIMIT=70] -->
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index befee2b..3db56c5 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -420,6 +420,11 @@
     <!-- Cannot double inherit. Use Theme.SystemUI.QuickSettings in code to match -->
     <style name="BrightnessDialog" parent="@android:style/Theme.DeviceDefault.Dialog">
         <item name="android:windowBackground">@android:color/transparent</item>
+        <item name="android:windowAnimationStyle">@style/Animation.BrightnessDialog</item>
+    </style>
+
+    <style name="Animation.BrightnessDialog">
+        <item name="android:windowExitAnimation">@anim/instant_fade_out</item>
     </style>
 
     <style name="Theme.SystemUI.ContrastDialog" parent="@android:style/Theme.DeviceDefault.Dialog">
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index 4da48f6..706aba3c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -302,4 +302,11 @@
     fun isFinishedInState(state: KeyguardState): Flow<Boolean> {
         return finishedKeyguardState.map { it == state }.distinctUntilChanged()
     }
+
+    /**
+     * Whether we've FINISHED a transition to a state that matches the given predicate. Consider
+     * using [isFinishedInStateWhere] whenever possible instead
+     */
+    fun isFinishedInStateWhereValue(stateMatcher: (KeyguardState) -> Boolean) =
+        stateMatcher(finishedKeyguardState.replayCache.last())
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
index 0c5a14f..48f432e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
@@ -58,8 +58,8 @@
 import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.recyclerview.widget.RecyclerView;
 
-import com.android.systemui.res.R;
 import com.android.systemui.broadcast.BroadcastSender;
+import com.android.systemui.res.R;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
 
 import java.util.concurrent.Executor;
@@ -85,6 +85,13 @@
     final MediaOutputController mMediaOutputController;
     final BroadcastSender mBroadcastSender;
 
+    /**
+     * Signals whether the dialog should NOT show app-related metadata.
+     *
+     * <p>A metadata-less dialog hides the title, subtitle, and app icon in the header.
+     */
+    private final boolean mIncludePlaybackAndAppMetadata;
+
     @VisibleForTesting
     View mDialogView;
     private TextView mHeaderTitle;
@@ -210,8 +217,11 @@
         }
     }
 
-    public MediaOutputBaseDialog(Context context, BroadcastSender broadcastSender,
-            MediaOutputController mediaOutputController) {
+    public MediaOutputBaseDialog(
+            Context context,
+            BroadcastSender broadcastSender,
+            MediaOutputController mediaOutputController,
+            boolean includePlaybackAndAppMetadata) {
         super(context, R.style.Theme_SystemUI_Dialog_Media);
 
         // Save the context that is wrapped with our theme.
@@ -226,6 +236,7 @@
         mListPaddingTop = mContext.getResources().getDimensionPixelSize(
                 R.dimen.media_output_dialog_list_padding_top);
         mExecutor = Executors.newSingleThreadExecutor();
+        mIncludePlaybackAndAppMetadata = includePlaybackAndAppMetadata;
     }
 
     @Override
@@ -354,7 +365,10 @@
             updateDialogBackgroundColor();
             mHeaderIcon.setVisibility(View.GONE);
         }
-        if (appSourceIcon != null) {
+
+        if (!mIncludePlaybackAndAppMetadata) {
+            mAppResourceIcon.setVisibility(View.GONE);
+        } else if (appSourceIcon != null) {
             Icon appIcon = appSourceIcon.toIcon(mContext);
             mAppResourceIcon.setColorFilter(mMediaOutputController.getColorItemContent());
             mAppResourceIcon.setImageIcon(appIcon);
@@ -373,17 +387,24 @@
             mHeaderIcon.setLayoutParams(new LinearLayout.LayoutParams(size + padding, size));
         }
         mAppButton.setText(mMediaOutputController.getAppSourceName());
-        // Update title and subtitle
-        mHeaderTitle.setText(getHeaderText());
-        final CharSequence subTitle = getHeaderSubtitle();
-        if (TextUtils.isEmpty(subTitle)) {
+
+        if (!mIncludePlaybackAndAppMetadata) {
+            mHeaderTitle.setVisibility(View.GONE);
             mHeaderSubtitle.setVisibility(View.GONE);
-            mHeaderTitle.setGravity(Gravity.START | Gravity.CENTER_VERTICAL);
         } else {
-            mHeaderSubtitle.setVisibility(View.VISIBLE);
-            mHeaderSubtitle.setText(subTitle);
-            mHeaderTitle.setGravity(Gravity.NO_GRAVITY);
+            // Update title and subtitle
+            mHeaderTitle.setText(getHeaderText());
+            final CharSequence subTitle = getHeaderSubtitle();
+            if (TextUtils.isEmpty(subTitle)) {
+                mHeaderSubtitle.setVisibility(View.GONE);
+                mHeaderTitle.setGravity(Gravity.START | Gravity.CENTER_VERTICAL);
+            } else {
+                mHeaderSubtitle.setVisibility(View.VISIBLE);
+                mHeaderSubtitle.setText(subTitle);
+                mHeaderTitle.setGravity(Gravity.NO_GRAVITY);
+            }
         }
+
         // Show when remote media session is available or
         //      when the device supports BT LE audio + media is playing
         mStopButton.setVisibility(getStopButtonVisibility());
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
index ac64300..8e0191e 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBroadcastDialog.java
@@ -42,12 +42,10 @@
 import androidx.core.graphics.drawable.IconCompat;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.settingslib.media.BluetoothMediaDevice;
-import com.android.settingslib.media.MediaDevice;
 import com.android.settingslib.qrcode.QrCodeGenerator;
-import com.android.systemui.res.R;
 import com.android.systemui.broadcast.BroadcastSender;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.res.R;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
 
 import com.google.zxing.WriterException;
@@ -237,7 +235,11 @@
 
     MediaOutputBroadcastDialog(Context context, boolean aboveStatusbar,
             BroadcastSender broadcastSender, MediaOutputController mediaOutputController) {
-        super(context, broadcastSender, mediaOutputController);
+        super(
+                context,
+                broadcastSender,
+                mediaOutputController, /* includePlaybackAndAppMetadata */
+                true);
         mAdapter = new MediaOutputAdapter(mMediaOutputController);
         // TODO(b/226710953): Move the part to MediaOutputBaseDialog for every class
         //  that extends MediaOutputBaseDialog
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
index 426a497..375a0ce 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -78,7 +78,6 @@
 import com.android.settingslib.media.LocalMediaManager;
 import com.android.settingslib.media.MediaDevice;
 import com.android.settingslib.utils.ThreadUtils;
-import com.android.systemui.res.R;
 import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.broadcast.BroadcastSender;
@@ -86,6 +85,7 @@
 import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
 import com.android.systemui.monet.ColorScheme;
 import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.res.R;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
@@ -358,7 +358,7 @@
     }
 
     Drawable getAppSourceIconFromPackage() {
-        if (mPackageName.isEmpty()) {
+        if (TextUtils.isEmpty(mPackageName)) {
             return null;
         }
         try {
@@ -372,7 +372,7 @@
     }
 
     String getAppSourceName() {
-        if (mPackageName.isEmpty()) {
+        if (TextUtils.isEmpty(mPackageName)) {
             return null;
         }
         final PackageManager packageManager = mContext.getPackageManager();
@@ -391,7 +391,7 @@
     }
 
     Intent getAppLaunchIntent() {
-        if (mPackageName.isEmpty()) {
+        if (TextUtils.isEmpty(mPackageName)) {
             return null;
         }
         return mContext.getPackageManager().getLaunchIntentForPackage(mPackageName);
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
index 4640a5d..d40699c 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
@@ -27,10 +27,10 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.UiEvent;
 import com.android.internal.logging.UiEventLogger;
-import com.android.systemui.res.R;
 import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.broadcast.BroadcastSender;
 import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.res.R;
 
 /**
  * Dialog for media output transferring.
@@ -40,10 +40,15 @@
     private final DialogLaunchAnimator mDialogLaunchAnimator;
     private final UiEventLogger mUiEventLogger;
 
-    MediaOutputDialog(Context context, boolean aboveStatusbar, BroadcastSender broadcastSender,
-            MediaOutputController mediaOutputController, DialogLaunchAnimator dialogLaunchAnimator,
-            UiEventLogger uiEventLogger) {
-        super(context, broadcastSender, mediaOutputController);
+    MediaOutputDialog(
+            Context context,
+            boolean aboveStatusbar,
+            BroadcastSender broadcastSender,
+            MediaOutputController mediaOutputController,
+            DialogLaunchAnimator dialogLaunchAnimator,
+            UiEventLogger uiEventLogger,
+            boolean includePlaybackAndAppMetadata) {
+        super(context, broadcastSender, mediaOutputController, includePlaybackAndAppMetadata);
         mDialogLaunchAnimator = dialogLaunchAnimator;
         mUiEventLogger = uiEventLogger;
         mAdapter = new MediaOutputAdapter(mMediaOutputController);
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
index 2b38edb..b04a7a4 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogFactory.kt
@@ -61,6 +61,19 @@
 
     /** Creates a [MediaOutputDialog] for the given package. */
     open fun create(packageName: String, aboveStatusBar: Boolean, view: View? = null) {
+        create(packageName, aboveStatusBar, view, includePlaybackAndAppMetadata = true)
+    }
+
+    open fun createDialogForSystemRouting() {
+        create(packageName = null, aboveStatusBar = false, includePlaybackAndAppMetadata = false)
+    }
+
+    private fun create(
+            packageName: String?,
+            aboveStatusBar: Boolean,
+            view: View? = null,
+            includePlaybackAndAppMetadata: Boolean = true
+    ) {
         // Dismiss the previous dialog, if any.
         mediaOutputDialog?.dismiss()
 
@@ -71,7 +84,7 @@
             powerExemptionManager, keyGuardManager, featureFlags, userTracker)
         val dialog =
             MediaOutputDialog(context, aboveStatusBar, broadcastSender, controller,
-                    dialogLaunchAnimator, uiEventLogger)
+                    dialogLaunchAnimator, uiEventLogger, includePlaybackAndAppMetadata)
         mediaOutputDialog = dialog
 
         // Show the dialog.
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogReceiver.kt
index 132bf99..1002cc3 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogReceiver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialogReceiver.kt
@@ -19,7 +19,6 @@
 import android.content.BroadcastReceiver
 import android.content.Context
 import android.content.Intent
-import android.text.TextUtils
 import android.util.Log
 import com.android.settingslib.media.MediaOutputConstants
 import javax.inject.Inject
@@ -35,16 +34,16 @@
     private val mediaOutputBroadcastDialogFactory: MediaOutputBroadcastDialogFactory
 ) : BroadcastReceiver() {
     override fun onReceive(context: Context, intent: Intent) {
-        when {
-            TextUtils.equals(
-                MediaOutputConstants.ACTION_LAUNCH_MEDIA_OUTPUT_DIALOG, intent.action) -> {
+        when (intent.action) {
+            MediaOutputConstants.ACTION_LAUNCH_MEDIA_OUTPUT_DIALOG -> {
                 val packageName: String? =
                     intent.getStringExtra(MediaOutputConstants.EXTRA_PACKAGE_NAME)
                 launchMediaOutputDialogIfPossible(packageName)
             }
-            TextUtils.equals(
-                MediaOutputConstants.ACTION_LAUNCH_MEDIA_OUTPUT_BROADCAST_DIALOG,
-                intent.action) -> {
+            MediaOutputConstants.ACTION_LAUNCH_SYSTEM_MEDIA_OUTPUT_DIALOG -> {
+                mediaOutputDialogFactory.createDialogForSystemRouting()
+            }
+            MediaOutputConstants.ACTION_LAUNCH_MEDIA_OUTPUT_BROADCAST_DIALOG -> {
                 val packageName: String? =
                     intent.getStringExtra(MediaOutputConstants.EXTRA_PACKAGE_NAME)
                 launchMediaOutputBroadcastDialogIfPossible(packageName)
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionDialogDelegate.kt
index 654fffe8..1983a67 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/BaseMediaProjectionPermissionDialogDelegate.kt
@@ -24,6 +24,7 @@
 import android.view.ViewGroup
 import android.view.ViewStub
 import android.view.WindowManager
+import android.view.accessibility.AccessibilityNodeInfo
 import android.widget.AdapterView
 import android.widget.ArrayAdapter
 import android.widget.ImageView
@@ -106,6 +107,19 @@
         screenShareModeSpinner = dialog.requireViewById(R.id.screen_share_mode_spinner)
         screenShareModeSpinner.adapter = adapter
         screenShareModeSpinner.onItemSelectedListener = this
+
+        // disable redundant Touch & Hold accessibility action for Switch Access
+        screenShareModeSpinner.accessibilityDelegate =
+            object : View.AccessibilityDelegate() {
+                override fun onInitializeAccessibilityNodeInfo(
+                    host: View,
+                    info: AccessibilityNodeInfo
+                ) {
+                    info.removeAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK)
+                    super.onInitializeAccessibilityNodeInfo(host, info)
+                }
+            }
+        screenShareModeSpinner.isLongClickable = false
     }
 
     override fun onItemSelected(adapterView: AdapterView<*>?, view: View, pos: Int, id: Long) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
index 0299114..e0eee96 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt
@@ -34,34 +34,38 @@
         viewModel: FooterViewModel,
         clearAllNotifications: View.OnClickListener,
     ): DisposableHandle {
-        // Listen for changes when the view is attached.
+        // Bind the resource IDs
+        footer.setMessageString(viewModel.message.messageId)
+        footer.setMessageIcon(viewModel.message.iconId)
+        footer.setClearAllButtonText(viewModel.clearAllButton.labelId)
+        footer.setClearAllButtonDescription(viewModel.clearAllButton.accessibilityDescriptionId)
+
+        // Bind the click listeners
+        footer.setClearAllButtonClickListener(clearAllNotifications)
+
+        // Listen for visibility changes when the view is attached.
         return footer.repeatWhenAttached {
             lifecycleScope.launch {
-                viewModel.clearAllButton.collect { button ->
-                    if (button.isVisible.isAnimating) {
+                viewModel.clearAllButton.isVisible.collect { isVisible ->
+                    if (isVisible.isAnimating) {
                         footer.setClearAllButtonVisible(
-                            button.isVisible.value,
+                            isVisible.value,
                             /* animate = */ true,
                         ) { _ ->
-                            button.isVisible.stopAnimating()
+                            isVisible.stopAnimating()
                         }
                     } else {
                         footer.setClearAllButtonVisible(
-                            button.isVisible.value,
+                            isVisible.value,
                             /* animate = */ false,
                         )
                     }
-                    footer.setClearAllButtonText(button.labelId)
-                    footer.setClearAllButtonDescription(button.accessibilityDescriptionId)
-                    footer.setClearAllButtonClickListener(clearAllNotifications)
                 }
             }
 
             lifecycleScope.launch {
-                viewModel.message.collect { message ->
-                    footer.setFooterLabelVisible(message.visible)
-                    footer.setMessageString(message.messageId)
-                    footer.setMessageIcon(message.iconId)
+                viewModel.message.isVisible.collect { visible ->
+                    footer.setFooterLabelVisible(visible)
                 }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterButtonViewModel.kt
index ea5abef..244555a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterButtonViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterButtonViewModel.kt
@@ -18,9 +18,10 @@
 
 import android.annotation.StringRes
 import com.android.systemui.util.ui.AnimatedValue
+import kotlinx.coroutines.flow.Flow
 
 data class FooterButtonViewModel(
     @StringRes val labelId: Int,
     @StringRes val accessibilityDescriptionId: Int,
-    val isVisible: AnimatedValue<Boolean>,
+    val isVisible: Flow<AnimatedValue<Boolean>>,
 )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterMessageViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterMessageViewModel.kt
index bc912fb..85cd397 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterMessageViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterMessageViewModel.kt
@@ -18,10 +18,11 @@
 
 import android.annotation.DrawableRes
 import android.annotation.StringRes
+import kotlinx.coroutines.flow.StateFlow
 
 /** A ViewModel for the string message that can be shown in the footer. */
 data class FooterMessageViewModel(
     @StringRes val messageId: Int,
     @DrawableRes val iconId: Int,
-    val visible: Boolean,
+    val isVisible: StateFlow<Boolean>,
 )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
index 721bea1..e6b0abc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt
@@ -30,9 +30,7 @@
 import dagger.Provides
 import java.util.Optional
 import javax.inject.Provider
-import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onStart
 
 /** ViewModel for [FooterView]. */
@@ -41,36 +39,32 @@
     seenNotificationsInteractor: SeenNotificationsInteractor,
     shadeInteractor: ShadeInteractor,
 ) {
-    val clearAllButton: Flow<FooterButtonViewModel> =
-        activeNotificationsInteractor.hasClearableNotifications
-            .sample(
-                combine(
-                        shadeInteractor.isShadeFullyExpanded,
-                        shadeInteractor.isShadeTouchable,
-                        ::Pair
-                    )
-                    .onStart { emit(Pair(false, false)) }
-            ) { hasClearableNotifications, (isShadeFullyExpanded, animationsEnabled) ->
-                val shouldAnimate = isShadeFullyExpanded && animationsEnabled
-                AnimatableEvent(hasClearableNotifications, shouldAnimate)
-            }
-            .toAnimatedValueFlow()
-            .map { visible ->
-                FooterButtonViewModel(
-                    labelId = R.string.clear_all_notifications_text,
-                    accessibilityDescriptionId = R.string.accessibility_clear_all,
-                    isVisible = visible,
-                )
-            }
+    val clearAllButton: FooterButtonViewModel =
+        FooterButtonViewModel(
+            labelId = R.string.clear_all_notifications_text,
+            accessibilityDescriptionId = R.string.accessibility_clear_all,
+            isVisible =
+                activeNotificationsInteractor.hasClearableNotifications
+                    .sample(
+                        combine(
+                                shadeInteractor.isShadeFullyExpanded,
+                                shadeInteractor.isShadeTouchable,
+                                ::Pair
+                            )
+                            .onStart { emit(Pair(false, false)) }
+                    ) { hasClearableNotifications, (isShadeFullyExpanded, animationsEnabled) ->
+                        val shouldAnimate = isShadeFullyExpanded && animationsEnabled
+                        AnimatableEvent(hasClearableNotifications, shouldAnimate)
+                    }
+                    .toAnimatedValueFlow(),
+        )
 
-    val message: Flow<FooterMessageViewModel> =
-        seenNotificationsInteractor.hasFilteredOutSeenNotifications.map { hasFilteredOutNotifs ->
-            FooterMessageViewModel(
-                messageId = R.string.unlock_to_see_notif_text,
-                iconId = R.drawable.ic_friction_lock_closed,
-                visible = hasFilteredOutNotifs,
-            )
-        }
+    val message: FooterMessageViewModel =
+        FooterMessageViewModel(
+            messageId = R.string.unlock_to_see_notif_text,
+            iconId = R.drawable.ic_friction_lock_closed,
+            isVisible = seenNotificationsInteractor.hasFilteredOutSeenNotifications,
+        )
 }
 
 @Module
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
index 4554085..aa0d3ac 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt
@@ -28,6 +28,7 @@
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.NotificationShelf
+import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
 import com.android.systemui.statusbar.notification.footer.ui.view.FooterView
 import com.android.systemui.statusbar.notification.footer.ui.viewbinder.FooterViewBinder
 import com.android.systemui.statusbar.notification.icon.ui.viewbinder.ShelfNotificationIconViewStore
@@ -62,14 +63,17 @@
         viewController: NotificationStackScrollLayoutController
     ) {
         bindShelf(view)
-        bindFooter(view)
-        bindEmptyShade(view)
         bindHideList(viewController, viewModel)
 
-        view.repeatWhenAttached {
-            lifecycleScope.launch {
-                viewModel.isImportantForAccessibility.collect { isImportantForAccessibility ->
-                    view.setImportantForAccessibilityYesNo(isImportantForAccessibility)
+        if (FooterViewRefactor.isEnabled) {
+            bindFooter(view)
+            bindEmptyShade(view)
+
+            view.repeatWhenAttached {
+                lifecycleScope.launch {
+                    viewModel.isImportantForAccessibility.collect { isImportantForAccessibility ->
+                        view.setImportantForAccessibilityYesNo(isImportantForAccessibility)
+                    }
                 }
             }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
index 5a9f5d5..886fa70 100644
--- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
+++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java
@@ -19,6 +19,7 @@
 import static android.util.TypedValue.TYPE_INT_COLOR_ARGB8;
 
 import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_ASLEEP;
+import static com.android.systemui.Flags.themeOverlayControllerWakefulnessDeprecation;
 import static com.android.systemui.theme.ThemeOverlayApplier.COLOR_SOURCE_HOME;
 import static com.android.systemui.theme.ThemeOverlayApplier.COLOR_SOURCE_LOCK;
 import static com.android.systemui.theme.ThemeOverlayApplier.COLOR_SOURCE_PRESET;
@@ -71,12 +72,15 @@
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
+import com.android.systemui.keyguard.shared.model.KeyguardState;
 import com.android.systemui.monet.ColorScheme;
 import com.android.systemui.monet.Style;
 import com.android.systemui.monet.TonalPalette;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
+import com.android.systemui.util.kotlin.JavaAdapter;
 import com.android.systemui.util.settings.SecureSettings;
 
 import com.google.ux.material.libmonet.dynamiccolor.MaterialDynamicColors;
@@ -127,7 +131,6 @@
     private final SecureSettings mSecureSettings;
     private final Executor mMainExecutor;
     private final Handler mBgHandler;
-    private final boolean mIsMonochromaticEnabled;
     private final Context mContext;
     private final boolean mIsMonetEnabled;
     private final boolean mIsFidelityEnabled;
@@ -161,6 +164,8 @@
     private final SparseArray<WallpaperColors> mDeferredWallpaperColors = new SparseArray<>();
     private final SparseIntArray mDeferredWallpaperColorsFlags = new SparseIntArray();
     private final WakefulnessLifecycle mWakefulnessLifecycle;
+    private final JavaAdapter mJavaAdapter;
+    private final KeyguardTransitionInteractor mKeyguardTransitionInteractor;
     private final UiModeManager mUiModeManager;
     private DynamicScheme mDynamicSchemeDark;
     private DynamicScheme mDynamicSchemeLight;
@@ -200,8 +205,12 @@
                 return;
             }
             boolean currentUser = userId == mUserTracker.getUserId();
-            if (currentUser && !mAcceptColorEvents
-                    && mWakefulnessLifecycle.getWakefulness() != WAKEFULNESS_ASLEEP) {
+            boolean isAsleep = themeOverlayControllerWakefulnessDeprecation()
+                    ? mKeyguardTransitionInteractor.isFinishedInStateWhereValue(
+                        state -> KeyguardState.Companion.deviceIsAsleepInState(state))
+                    : mWakefulnessLifecycle.getWakefulness() != WAKEFULNESS_ASLEEP;
+
+            if (currentUser && !mAcceptColorEvents && isAsleep) {
                 mDeferredWallpaperColors.put(userId, wallpaperColors);
                 mDeferredWallpaperColorsFlags.put(userId, which);
                 Log.i(TAG, "colors received; processing deferred until screen off: "
@@ -395,9 +404,10 @@
             FeatureFlags featureFlags,
             @Main Resources resources,
             WakefulnessLifecycle wakefulnessLifecycle,
+            JavaAdapter javaAdapter,
+            KeyguardTransitionInteractor keyguardTransitionInteractor,
             UiModeManager uiModeManager) {
         mContext = context;
-        mIsMonochromaticEnabled = featureFlags.isEnabled(Flags.MONOCHROMATIC_THEME);
         mIsMonetEnabled = featureFlags.isEnabled(Flags.MONET);
         mIsFidelityEnabled = featureFlags.isEnabled(Flags.COLOR_FIDELITY);
         mDeviceProvisionedController = deviceProvisionedController;
@@ -412,6 +422,8 @@
         mUserTracker = userTracker;
         mResources = resources;
         mWakefulnessLifecycle = wakefulnessLifecycle;
+        mJavaAdapter = javaAdapter;
+        mKeyguardTransitionInteractor = keyguardTransitionInteractor;
         mUiModeManager = uiModeManager;
         dumpManager.registerDumpable(TAG, this);
     }
@@ -494,21 +506,34 @@
         }
         mWallpaperManager.addOnColorsChangedListener(mOnColorsChangedListener, null,
                 UserHandle.USER_ALL);
-        mWakefulnessLifecycle.addObserver(new WakefulnessLifecycle.Observer() {
-            @Override
-            public void onFinishedGoingToSleep() {
-                final int userId = mUserTracker.getUserId();
-                final WallpaperColors colors = mDeferredWallpaperColors.get(userId);
-                if (colors != null) {
-                    int flags = mDeferredWallpaperColorsFlags.get(userId);
 
-                    mDeferredWallpaperColors.put(userId, null);
-                    mDeferredWallpaperColorsFlags.put(userId, 0);
+        Runnable whenAsleepHandler = () -> {
+            final int userId = mUserTracker.getUserId();
+            final WallpaperColors colors = mDeferredWallpaperColors.get(userId);
+            if (colors != null) {
+                int flags = mDeferredWallpaperColorsFlags.get(userId);
 
-                    handleWallpaperColors(colors, flags, userId);
-                }
+                mDeferredWallpaperColors.put(userId, null);
+                mDeferredWallpaperColorsFlags.put(userId, 0);
+
+                handleWallpaperColors(colors, flags, userId);
             }
-        });
+        };
+
+        if (themeOverlayControllerWakefulnessDeprecation()) {
+            mJavaAdapter.alwaysCollectFlow(
+                    mKeyguardTransitionInteractor.isFinishedInState(KeyguardState.DOZING),
+                    isFinishedInDozing -> {
+                        if (isFinishedInDozing) whenAsleepHandler.run();
+                    });
+        } else {
+            mWakefulnessLifecycle.addObserver(new WakefulnessLifecycle.Observer() {
+                @Override
+                public void onFinishedGoingToSleep() {
+                    whenAsleepHandler.run();
+                }
+            });
+        }
     }
 
     private void reevaluateSystemTheme(boolean forceReload) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
index 9dfb5a5..e082ca8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
@@ -47,13 +47,13 @@
 import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast;
 import com.android.settingslib.bluetooth.LocalBluetoothManager;
 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
-import com.android.systemui.res.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.broadcast.BroadcastSender;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
 import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.res.R;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
 
@@ -305,7 +305,11 @@
 
         MediaOutputBaseDialogImpl(Context context, BroadcastSender broadcastSender,
                 MediaOutputController mediaOutputController) {
-            super(context, broadcastSender, mediaOutputController);
+            super(
+                    context,
+                    broadcastSender,
+                    mediaOutputController, /* includePlaybackAndAppMetadata */
+                    true);
 
             mAdapter = mMediaOutputBaseAdapter;
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
index 379136b..d5dc502 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
@@ -49,13 +49,13 @@
 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
 import com.android.settingslib.media.LocalMediaManager;
 import com.android.settingslib.media.MediaDevice;
-import com.android.systemui.res.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.broadcast.BroadcastSender;
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
 import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.res.R;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
 
@@ -394,8 +394,14 @@
 
     @NonNull
     private MediaOutputDialog makeTestDialog(MediaOutputController controller) {
-        return new MediaOutputDialog(mContext, false, mBroadcastSender,
-                controller, mDialogLaunchAnimator, mUiEventLogger);
+        return new MediaOutputDialog(
+                mContext,
+                false,
+                mBroadcastSender,
+                controller,
+                mDialogLaunchAnimator,
+                mUiEventLogger,
+                true);
     }
 
     private void withTestDialog(MediaOutputController controller, Consumer<MediaOutputDialog> c) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
index 94dcf7a..0ba820f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt
@@ -116,27 +116,27 @@
     @Test
     fun testMessageVisible_whenFilteredNotifications() =
         testComponent.runTest {
-            val message by collectLastValue(footerViewModel.message)
+            val visible by collectLastValue(footerViewModel.message.isVisible)
 
             activeNotificationListRepository.hasFilteredOutSeenNotifications.value = true
 
-            assertThat(message?.visible).isTrue()
+            assertThat(visible).isTrue()
         }
 
     @Test
     fun testMessageVisible_whenNoFilteredNotifications() =
         testComponent.runTest {
-            val message by collectLastValue(footerViewModel.message)
+            val visible by collectLastValue(footerViewModel.message.isVisible)
 
             activeNotificationListRepository.hasFilteredOutSeenNotifications.value = false
 
-            assertThat(message?.visible).isFalse()
+            assertThat(visible).isFalse()
         }
 
     @Test
     fun testClearAllButtonVisible_whenHasClearableNotifs() =
         testComponent.runTest {
-            val button by collectLastValue(footerViewModel.clearAllButton)
+            val visible by collectLastValue(footerViewModel.clearAllButton.isVisible)
 
             activeNotificationListRepository.notifStats.value =
                 NotifStats(
@@ -148,13 +148,13 @@
                 )
             runCurrent()
 
-            assertThat(button?.isVisible?.value).isTrue()
+            assertThat(visible?.value).isTrue()
         }
 
     @Test
     fun testClearAllButtonVisible_whenHasNoClearableNotifs() =
         testComponent.runTest {
-            val button by collectLastValue(footerViewModel.clearAllButton)
+            val visible by collectLastValue(footerViewModel.clearAllButton.isVisible)
 
             activeNotificationListRepository.notifStats.value =
                 NotifStats(
@@ -166,13 +166,13 @@
                 )
             runCurrent()
 
-            assertThat(button?.isVisible?.value).isFalse()
+            assertThat(visible?.value).isFalse()
         }
 
     @Test
     fun testClearAllButtonAnimating_whenShadeExpandedAndTouchable() =
         testComponent.runTest {
-            val button by collectLastValue(footerViewModel.clearAllButton)
+            val visible by collectLastValue(footerViewModel.clearAllButton.isVisible)
             runCurrent()
 
             // WHEN shade is expanded
@@ -200,13 +200,13 @@
             runCurrent()
 
             // THEN button visibility should animate
-            assertThat(button?.isVisible?.isAnimating).isTrue()
+            assertThat(visible?.isAnimating).isTrue()
         }
 
     @Test
     fun testClearAllButtonAnimating_whenShadeNotExpanded() =
         testComponent.runTest {
-            val button by collectLastValue(footerViewModel.clearAllButton)
+            val visible by collectLastValue(footerViewModel.clearAllButton.isVisible)
             runCurrent()
 
             // WHEN shade is collapsed
@@ -234,6 +234,6 @@
             runCurrent()
 
             // THEN button visibility should not animate
-            assertThat(button?.isVisible?.isAnimating).isFalse()
+            assertThat(visible?.isAnimating).isFalse()
         }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
index c454b45..1123688 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java
@@ -61,10 +61,12 @@
 import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor;
 import com.android.systemui.monet.Style;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener;
+import com.android.systemui.util.kotlin.JavaAdapter;
 import com.android.systemui.util.settings.SecureSettings;
 
 import com.google.common.util.concurrent.MoreExecutors;
@@ -88,7 +90,10 @@
 
     private static final int USER_SYSTEM = UserHandle.USER_SYSTEM;
     private static final int USER_SECONDARY = 10;
-
+    @Mock
+    private JavaAdapter mJavaAdapter;
+    @Mock
+    private KeyguardTransitionInteractor mKeyguardTransitionInteractor;
     private ThemeOverlayController mThemeOverlayController;
     @Mock
     private Executor mBgExecutor;
@@ -150,11 +155,12 @@
                 .thenReturn(Color.YELLOW);
         when(mResources.getColor(eq(android.R.color.system_neutral2_500), any()))
                 .thenReturn(Color.BLACK);
+
         mThemeOverlayController = new ThemeOverlayController(mContext,
                 mBroadcastDispatcher, mBgHandler, mMainExecutor, mBgExecutor, mThemeOverlayApplier,
                 mSecureSettings, mWallpaperManager, mUserManager, mDeviceProvisionedController,
                 mUserTracker, mDumpManager, mFeatureFlags, mResources, mWakefulnessLifecycle,
-                mUiModeManager) {
+                mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager) {
             @VisibleForTesting
             protected boolean isNightMode() {
                 return false;
@@ -736,7 +742,7 @@
                 mBroadcastDispatcher, mBgHandler, executor, executor, mThemeOverlayApplier,
                 mSecureSettings, mWallpaperManager, mUserManager, mDeviceProvisionedController,
                 mUserTracker, mDumpManager, mFeatureFlags, mResources, mWakefulnessLifecycle,
-                mUiModeManager) {
+                mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager) {
             @VisibleForTesting
             protected boolean isNightMode() {
                 return false;
@@ -776,7 +782,7 @@
                 mBroadcastDispatcher, mBgHandler, executor, executor, mThemeOverlayApplier,
                 mSecureSettings, mWallpaperManager, mUserManager, mDeviceProvisionedController,
                 mUserTracker, mDumpManager, mFeatureFlags, mResources, mWakefulnessLifecycle,
-                mUiModeManager) {
+                mJavaAdapter, mKeyguardTransitionInteractor, mUiModeManager) {
             @VisibleForTesting
             protected boolean isNightMode() {
                 return false;
diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
index 041cd75..7187895 100644
--- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java
@@ -34,7 +34,7 @@
 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK;
 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK;
 
-import static com.android.window.flags.Flags.removeCaptureDisplay;
+import static com.android.window.flags.Flags.deleteCaptureDisplay;
 
 import android.accessibilityservice.AccessibilityGestureEvent;
 import android.accessibilityservice.AccessibilityService;
@@ -1443,7 +1443,7 @@
             return;
         }
         final long identity = Binder.clearCallingIdentity();
-        if (removeCaptureDisplay()) {
+        if (deleteCaptureDisplay()) {
             try {
                 ScreenCapture.ScreenCaptureListener screenCaptureListener = new
                         ScreenCapture.ScreenCaptureListener(
@@ -1485,7 +1485,7 @@
 
     private void sendScreenshotSuccess(ScreenshotHardwareBuffer screenshotBuffer,
             RemoteCallback callback) {
-        if (removeCaptureDisplay()) {
+        if (deleteCaptureDisplay()) {
             mMainHandler.post(PooledLambda.obtainRunnable((nonArg) -> {
                 final HardwareBuffer hardwareBuffer = screenshotBuffer.getHardwareBuffer();
                 final ParcelableColorSpace colorSpace =
diff --git a/services/companion/java/com/android/server/companion/virtual/OWNERS b/services/companion/java/com/android/server/companion/virtual/OWNERS
index 5295ec8..4fe0592 100644
--- a/services/companion/java/com/android/server/companion/virtual/OWNERS
+++ b/services/companion/java/com/android/server/companion/virtual/OWNERS
@@ -2,7 +2,7 @@
 
 set noparent
 
-ogunwale@google.com
-michaelwr@google.com
+marvinramin@google.com
 vladokom@google.com
-marvinramin@google.com
\ No newline at end of file
+ogunwale@google.com
+michaelwr@google.com
\ No newline at end of file
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index c64fb23..54c8ed3 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -4852,8 +4852,10 @@
         } else {
             Slog.wtf(TAG, "Mismatched or missing ProcessRecord: " + app + ". Pid: " + pid
                     + ". Uid: " + uid);
-            killProcess(pid);
-            killProcessGroup(uid, pid);
+            if (pid > 0) {
+                killProcess(pid);
+                killProcessGroup(uid, pid);
+            }
             mProcessList.noteAppKill(pid, uid,
                     ApplicationExitInfo.REASON_INITIALIZATION_FAILURE,
                     ApplicationExitInfo.SUBREASON_UNKNOWN,
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index e7ea0be..9701fc8 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -107,6 +107,7 @@
 import android.media.AudioRecordingConfiguration;
 import android.media.AudioRoutesInfo;
 import android.media.AudioSystem;
+import android.media.AudioTrack;
 import android.media.BluetoothProfileConnectionInfo;
 import android.media.IAudioDeviceVolumeDispatcher;
 import android.media.IAudioFocusDispatcher;
@@ -133,7 +134,9 @@
 import android.media.IStrategyPreferredDevicesDispatcher;
 import android.media.IStreamAliasingDispatcher;
 import android.media.IVolumeController;
-import android.media.LoudnessCodecFormat;
+import android.media.LoudnessCodecConfigurator;
+import android.media.LoudnessCodecInfo;
+import android.media.MediaCodec;
 import android.media.MediaMetrics;
 import android.media.MediaRecorder.AudioSource;
 import android.media.PlayerBase;
@@ -347,7 +350,7 @@
     }
 
     /*package*/ boolean isPlatformAutomotive() {
-        return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
+        return mPlatformType == AudioSystem.PLATFORM_AUTOMOTIVE;
     }
 
     /** The controller for the volume UI. */
@@ -941,6 +944,8 @@
 
     private final SoundDoseHelper mSoundDoseHelper;
 
+    private final LoudnessCodecHelper mLoudnessCodecHelper;
+
     private final Object mSupportedSystemUsagesLock = new Object();
     @GuardedBy("mSupportedSystemUsagesLock")
     private @AttributeSystemUsage int[] mSupportedSystemUsages =
@@ -1275,6 +1280,8 @@
         readPersistedSettings();
         readUserRestrictions();
 
+        mLoudnessCodecHelper = new LoudnessCodecHelper(this);
+
         mPlaybackMonitor =
                 new PlaybackActivityMonitor(context, MAX_STREAM_VOLUME[AudioSystem.STREAM_ALARM],
                         device -> onMuteAwaitConnectionTimeout(device));
@@ -4366,6 +4373,8 @@
             mSoundDoseHelper.scheduleMusicActiveCheck();
         }
 
+        mLoudnessCodecHelper.updateCodecParameters(configs);
+
         // Update playback active state for all apps in audio mode stack.
         // When the audio mode owner becomes active, replace any delayed MSG_UPDATE_AUDIO_MODE
         // and request an audio mode update immediately. Upon any other change, queue the message
@@ -10562,44 +10571,43 @@
 
     @Override
     public void registerLoudnessCodecUpdatesDispatcher(ILoudnessCodecUpdatesDispatcher dispatcher) {
-        // TODO: implement
+        mLoudnessCodecHelper.registerLoudnessCodecUpdatesDispatcher(dispatcher);
     }
 
     @Override
     public void unregisterLoudnessCodecUpdatesDispatcher(
             ILoudnessCodecUpdatesDispatcher dispatcher) {
-        // TODO: implement
+        mLoudnessCodecHelper.unregisterLoudnessCodecUpdatesDispatcher(dispatcher);
     }
 
+    /** @see LoudnessCodecConfigurator#setAudioTrack(AudioTrack) */
     @Override
-    public void startLoudnessCodecUpdates(int piid) {
-        // TODO: implement
+    public void startLoudnessCodecUpdates(int piid, List<LoudnessCodecInfo> codecInfoList) {
+        mLoudnessCodecHelper.startLoudnessCodecUpdates(piid, codecInfoList);
     }
 
+    /** @see LoudnessCodecConfigurator#setAudioTrack(AudioTrack) */
     @Override
     public void stopLoudnessCodecUpdates(int piid) {
-        // TODO: implement
+        mLoudnessCodecHelper.stopLoudnessCodecUpdates(piid);
     }
 
+    /** @see LoudnessCodecConfigurator#addMediaCodec(MediaCodec) */
     @Override
-    public void addLoudnesssCodecFormat(int piid, LoudnessCodecFormat format) {
-        // TODO: implement
+    public void addLoudnessCodecInfo(int piid, LoudnessCodecInfo codecInfo) {
+        mLoudnessCodecHelper.addLoudnessCodecInfo(piid, codecInfo);
     }
 
+    /** @see LoudnessCodecConfigurator#removeMediaCodec(MediaCodec) */
     @Override
-    public void addLoudnesssCodecFormatList(int piid, List<LoudnessCodecFormat> format) {
-        // TODO: implement
+    public void removeLoudnessCodecInfo(int piid, LoudnessCodecInfo codecInfo) {
+        mLoudnessCodecHelper.removeLoudnessCodecInfo(piid, codecInfo);
     }
 
+    /** @see LoudnessCodecConfigurator#getLoudnessCodecParams(AudioTrack, MediaCodec) */
     @Override
-    public void removeLoudnessCodecFormat(int piid, LoudnessCodecFormat format) {
-        // TODO: implement
-    }
-
-    @Override
-    public PersistableBundle getLoudnessParams(int piid, LoudnessCodecFormat format) {
-        // TODO: implement
-        return null;
+    public PersistableBundle getLoudnessParams(int piid, LoudnessCodecInfo codecInfo) {
+        return mLoudnessCodecHelper.getLoudnessParams(piid, codecInfo);
     }
 
     //==========================================================================================
diff --git a/services/core/java/com/android/server/audio/LoudnessCodecHelper.java b/services/core/java/com/android/server/audio/LoudnessCodecHelper.java
new file mode 100644
index 0000000..3c67e9d
--- /dev/null
+++ b/services/core/java/com/android/server/audio/LoudnessCodecHelper.java
@@ -0,0 +1,513 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.audio;
+
+import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_CARKIT;
+import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_HEADPHONES;
+import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_HEARING_AID;
+import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_WATCH;
+import static android.media.AudioPlaybackConfiguration.PLAYER_DEVICEID_INVALID;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.media.AudioDeviceInfo;
+import android.media.AudioPlaybackConfiguration;
+import android.media.AudioSystem;
+import android.media.ILoudnessCodecUpdatesDispatcher;
+import android.media.LoudnessCodecInfo;
+import android.media.permission.ClearCallingIdentityContext;
+import android.media.permission.SafeCloseable;
+import android.os.Binder;
+import android.os.PersistableBundle;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import android.os.SystemProperties;
+import android.util.Log;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Class to handle the updates in loudness parameters and responsible to generate parameters that
+ * can be set directly on a MediaCodec.
+ */
+public class LoudnessCodecHelper {
+    private static final String TAG = "AS.LoudnessCodecHelper";
+
+    private static final boolean DEBUG = false;
+
+    /**
+     * Property containing a string to set for a custom built in speaker SPL range as defined by
+     * CTA2075. The options that can be set are:
+     *   - "small": for max SPL with test signal < 75 dB,
+     *   - "medium": for max SPL with test signal between 70 and 90 dB,
+     *   - "large": for max SPL with test signal > 85 dB.
+     */
+    private static final String SYSTEM_PROPERTY_SPEAKER_SPL_RANGE_SIZE =
+            "audio.loudness.builtin-speaker-spl-range-size";
+
+    private static final int SPL_RANGE_UNKNOWN = 0;
+    private static final int SPL_RANGE_SMALL = 1;
+    private static final int SPL_RANGE_MEDIUM = 2;
+    private static final int SPL_RANGE_LARGE = 3;
+
+    /** The possible transducer SPL ranges as defined in CTA2075 */
+    @IntDef({
+            SPL_RANGE_UNKNOWN,
+            SPL_RANGE_SMALL,
+            SPL_RANGE_MEDIUM,
+            SPL_RANGE_LARGE
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface DeviceSplRange {}
+
+    private static final class LoudnessRemoteCallbackList extends
+            RemoteCallbackList<ILoudnessCodecUpdatesDispatcher> {
+        private final LoudnessCodecHelper mLoudnessCodecHelper;
+        LoudnessRemoteCallbackList(LoudnessCodecHelper loudnessCodecHelper) {
+            mLoudnessCodecHelper = loudnessCodecHelper;
+        }
+
+        @Override
+        public void onCallbackDied(ILoudnessCodecUpdatesDispatcher callback, Object cookie) {
+            Integer pid = null;
+            if (cookie instanceof Integer) {
+                pid = (Integer) cookie;
+            }
+            if (pid != null) {
+                mLoudnessCodecHelper.removePid(pid);
+            }
+            super.onCallbackDied(callback, cookie);
+        }
+    }
+
+    private final LoudnessRemoteCallbackList mLoudnessUpdateDispatchers =
+            new LoudnessRemoteCallbackList(this);
+
+    private final Object mLock = new Object();
+
+    /** Contains for each started piid the set corresponding to unique registered audio codecs. */
+    @GuardedBy("mLock")
+    private final SparseArray<Set<LoudnessCodecInfo>> mStartedPiids = new SparseArray<>();
+
+    /** Contains the current device id assignment for each piid. */
+    @GuardedBy("mLock")
+    private final SparseIntArray mPiidToDeviceIdCache = new SparseIntArray();
+
+    /** Maps each piid to the owner process of the player. */
+    @GuardedBy("mLock")
+    private final SparseIntArray mPiidToPidCache = new SparseIntArray();
+
+    private final AudioService mAudioService;
+
+    /** Contains the properties necessary to compute the codec loudness related parameters. */
+    private static final class LoudnessCodecInputProperties {
+        private final int mMetadataType;
+
+        private final boolean mIsDownmixing;
+
+        @DeviceSplRange
+        private final int mDeviceSplRange;
+
+        static final class Builder {
+            private int mMetadataType;
+
+            private boolean mIsDownmixing;
+
+            @DeviceSplRange
+            private int mDeviceSplRange;
+
+            Builder setMetadataType(int metadataType) {
+                mMetadataType = metadataType;
+                return this;
+            }
+            Builder setIsDownmixing(boolean isDownmixing) {
+                mIsDownmixing = isDownmixing;
+                return this;
+            }
+            Builder setDeviceSplRange(@DeviceSplRange int deviceSplRange) {
+                mDeviceSplRange = deviceSplRange;
+                return this;
+            }
+
+            LoudnessCodecInputProperties build() {
+                return new LoudnessCodecInputProperties(mMetadataType,
+                        mIsDownmixing, mDeviceSplRange);
+            }
+        }
+
+        private LoudnessCodecInputProperties(int metadataType,
+                                             boolean isDownmixing,
+                                             @DeviceSplRange int deviceSplRange) {
+            mMetadataType = metadataType;
+            mIsDownmixing = isDownmixing;
+            mDeviceSplRange = deviceSplRange;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj == null) {
+                return false;
+            }
+            // type check and cast
+            if (getClass() != obj.getClass()) {
+                return false;
+            }
+            final LoudnessCodecInputProperties lcip = (LoudnessCodecInputProperties) obj;
+            return mMetadataType == lcip.mMetadataType
+                    && mIsDownmixing == lcip.mIsDownmixing
+                    && mDeviceSplRange == lcip.mDeviceSplRange;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mMetadataType, mIsDownmixing, mDeviceSplRange);
+        }
+
+        @Override
+        public String toString() {
+            return "Loudness properties:"
+                    + " device SPL range: " + splRangeToString(mDeviceSplRange)
+                    + " down-mixing: " + mIsDownmixing
+                    + " metadata type: " + mMetadataType;
+        }
+
+        PersistableBundle createLoudnessParameters() {
+            // TODO: create bundle with new parameters
+            return new PersistableBundle();
+        }
+
+    }
+
+    @GuardedBy("mLock")
+    private final HashMap<LoudnessCodecInputProperties, PersistableBundle> mCachedProperties =
+            new HashMap<>();
+
+    LoudnessCodecHelper(@NonNull AudioService audioService) {
+        mAudioService = Objects.requireNonNull(audioService);
+    }
+
+    void registerLoudnessCodecUpdatesDispatcher(ILoudnessCodecUpdatesDispatcher dispatcher) {
+        mLoudnessUpdateDispatchers.register(dispatcher, Binder.getCallingPid());
+    }
+
+    void unregisterLoudnessCodecUpdatesDispatcher(
+            ILoudnessCodecUpdatesDispatcher dispatcher) {
+        mLoudnessUpdateDispatchers.unregister(dispatcher);
+    }
+
+    void startLoudnessCodecUpdates(int piid, List<LoudnessCodecInfo> codecInfoList) {
+        if (DEBUG) {
+            Log.d(TAG, "startLoudnessCodecUpdates: piid " + piid + " codecInfos " + codecInfoList);
+        }
+        Set<LoudnessCodecInfo> infoSet;
+        synchronized (mLock) {
+            if (mStartedPiids.contains(piid)) {
+                Log.w(TAG, "Already started loudness updates for piid " + piid);
+                return;
+            }
+            infoSet = new HashSet<>(codecInfoList);
+            mStartedPiids.put(piid, infoSet);
+
+            mPiidToPidCache.put(piid, Binder.getCallingPid());
+        }
+
+        try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+            mAudioService.getActivePlaybackConfigurations().stream().filter(
+                    conf -> conf.getPlayerInterfaceId() == piid).findFirst().ifPresent(
+                            apc -> updateCodecParametersForConfiguration(apc, infoSet));
+        }
+    }
+
+    void stopLoudnessCodecUpdates(int piid) {
+        if (DEBUG) {
+            Log.d(TAG, "stopLoudnessCodecUpdates: piid " + piid);
+        }
+        synchronized (mLock) {
+            if (!mStartedPiids.contains(piid)) {
+                Log.w(TAG, "Loudness updates are already stopped for piid " + piid);
+                return;
+            }
+            mStartedPiids.remove(piid);
+            mPiidToDeviceIdCache.delete(piid);
+            mPiidToPidCache.delete(piid);
+        }
+    }
+
+    void addLoudnessCodecInfo(int piid, LoudnessCodecInfo info) {
+        if (DEBUG) {
+            Log.d(TAG, "addLoudnessCodecInfo: piid " + piid + " info " + info);
+        }
+
+        Set<LoudnessCodecInfo> infoSet;
+        synchronized (mLock) {
+            if (!mStartedPiids.contains(piid)) {
+                Log.w(TAG, "Cannot add new loudness info for stopped piid " + piid);
+                return;
+            }
+
+            infoSet = mStartedPiids.get(piid);
+            infoSet.add(info);
+        }
+
+        try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+            mAudioService.getActivePlaybackConfigurations().stream().filter(
+                    conf -> conf.getPlayerInterfaceId() == piid).findFirst().ifPresent(
+                            apc -> updateCodecParametersForConfiguration(apc, Set.of(info)));
+        }
+    }
+
+    void removeLoudnessCodecInfo(int piid, LoudnessCodecInfo codecInfo) {
+        if (DEBUG) {
+            Log.d(TAG, "removeLoudnessCodecInfo: piid " + piid + " info " + codecInfo);
+        }
+        synchronized (mLock) {
+            if (!mStartedPiids.contains(piid)) {
+                Log.w(TAG, "Cannot remove loudness info for stopped piid " + piid);
+                return;
+            }
+            final Set<LoudnessCodecInfo> infoSet = mStartedPiids.get(piid);
+            infoSet.remove(codecInfo);
+        }
+    }
+
+    void removePid(int pid) {
+        if (DEBUG) {
+            Log.d(TAG, "Removing pid " + pid + " from receiving updates");
+        }
+        synchronized (mLock) {
+            for (int i = 0; i < mPiidToPidCache.size(); ++i) {
+                int piid = mPiidToPidCache.keyAt(i);
+                if (mPiidToPidCache.get(piid) == pid) {
+                    if (DEBUG) {
+                        Log.d(TAG, "Removing piid  " + piid);
+                    }
+                    mStartedPiids.delete(piid);
+                    mPiidToDeviceIdCache.delete(piid);
+                }
+            }
+        }
+    }
+
+    PersistableBundle getLoudnessParams(int piid, LoudnessCodecInfo codecInfo) {
+        if (DEBUG) {
+            Log.d(TAG, "getLoudnessParams: piid " + piid + " codecInfo " + codecInfo);
+        }
+        try (SafeCloseable ignored = ClearCallingIdentityContext.create()) {
+            final List<AudioPlaybackConfiguration> configs =
+                    mAudioService.getActivePlaybackConfigurations();
+
+            for (final AudioPlaybackConfiguration apc : configs) {
+                if (apc.getPlayerInterfaceId() == piid) {
+                    final AudioDeviceInfo info = apc.getAudioDeviceInfo();
+                    if (info == null) {
+                        Log.i(TAG, "Player with piid " + piid + " is not assigned any device");
+                        break;
+                    }
+                    synchronized (mLock) {
+                        return getCodecBundle_l(info, codecInfo);
+                    }
+                }
+            }
+        }
+
+        // return empty Bundle
+        return new PersistableBundle();
+    }
+
+    /** Method to be called whenever there is a changed in the active playback configurations. */
+    void updateCodecParameters(List<AudioPlaybackConfiguration> configs) {
+        if (DEBUG) {
+            Log.d(TAG, "updateCodecParameters: configs " + configs);
+        }
+
+        List<AudioPlaybackConfiguration> updateApcList = new ArrayList<>();
+        synchronized (mLock) {
+            for (final AudioPlaybackConfiguration apc : configs) {
+                int piid = apc.getPlayerInterfaceId();
+                int cachedDeviceId = mPiidToDeviceIdCache.get(piid, PLAYER_DEVICEID_INVALID);
+                AudioDeviceInfo deviceInfo = apc.getAudioDeviceInfo();
+                if (deviceInfo == null) {
+                    if (DEBUG) {
+                        Log.d(TAG, "No device info for piid: " + piid);
+                    }
+                    if (cachedDeviceId != PLAYER_DEVICEID_INVALID) {
+                        mPiidToDeviceIdCache.delete(piid);
+                        if (DEBUG) {
+                            Log.d(TAG, "Remove cached device id for piid: " + piid);
+                        }
+                    }
+                    continue;
+                }
+                if (cachedDeviceId == deviceInfo.getId()) {
+                    // deviceId did not change
+                    if (DEBUG) {
+                        Log.d(TAG, "DeviceId " + cachedDeviceId + " for piid: " + piid
+                                + " did not change");
+                    }
+                    continue;
+                }
+                mPiidToDeviceIdCache.put(piid, deviceInfo.getId());
+                if (mStartedPiids.contains(piid)) {
+                    updateApcList.add(apc);
+                }
+            }
+        }
+
+        updateApcList.forEach(apc -> updateCodecParametersForConfiguration(apc, null));
+    }
+
+    /** Updates and dispatches the new loudness parameters for the {@code codecInfos} set.
+     *
+     * @param apc the player configuration for which the loudness parameters are updated.
+     * @param codecInfos the codec info for which the parameters are updated. If {@code null},
+     *                   send updates for all the started codecs assigned to {@code apc}
+     */
+    private void updateCodecParametersForConfiguration(AudioPlaybackConfiguration apc,
+            Set<LoudnessCodecInfo> codecInfos) {
+        if (DEBUG) {
+            Log.d(TAG, "updateCodecParametersForConfiguration apc:" + apc + " codecInfos: "
+                    + codecInfos);
+        }
+        final PersistableBundle allBundles = new PersistableBundle();
+        final int piid = apc.getPlayerInterfaceId();
+        synchronized (mLock) {
+            if (codecInfos == null) {
+                codecInfos = mStartedPiids.get(piid);
+            }
+
+            final AudioDeviceInfo deviceInfo = apc.getAudioDeviceInfo();
+            if (codecInfos != null && deviceInfo != null) {
+                for (LoudnessCodecInfo info : codecInfos) {
+                    allBundles.putPersistableBundle(Integer.toString(info.mediaCodecHashCode),
+                            getCodecBundle_l(deviceInfo, info));
+                }
+            }
+        }
+
+        if (!allBundles.isDefinitelyEmpty()) {
+            if (DEBUG) {
+                Log.d(TAG, "Dispatching for piid: " + piid + " bundle: " + allBundles);
+            }
+            dispatchNewLoudnessParameters(piid, allBundles);
+        }
+    }
+
+    private void dispatchNewLoudnessParameters(int piid, PersistableBundle bundle) {
+        if (DEBUG) {
+            Log.d(TAG, "dispatchNewLoudnessParameters: piid " + piid);
+        }
+        final int nbDispatchers = mLoudnessUpdateDispatchers.beginBroadcast();
+        for (int i = 0; i < nbDispatchers; ++i) {
+            try {
+                mLoudnessUpdateDispatchers.getBroadcastItem(i)
+                        .dispatchLoudnessCodecParameterChange(piid, bundle);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error dispatching for piid: " + piid + " bundle: " + bundle , e);
+            }
+        }
+        mLoudnessUpdateDispatchers.finishBroadcast();
+    }
+
+    @GuardedBy("mLock")
+    private PersistableBundle getCodecBundle_l(AudioDeviceInfo deviceInfo,
+                                             LoudnessCodecInfo codecInfo) {
+        LoudnessCodecInputProperties.Builder builder = new LoudnessCodecInputProperties.Builder();
+        LoudnessCodecInputProperties prop = builder.setDeviceSplRange(getDeviceSplRange(deviceInfo))
+                .setIsDownmixing(codecInfo.isDownmixing)
+                .setMetadataType(codecInfo.metadataType)
+                .build();
+
+        if (mCachedProperties.containsKey(prop)) {
+            return mCachedProperties.get(prop);
+        }
+        final PersistableBundle codecBundle = prop.createLoudnessParameters();
+        mCachedProperties.put(prop, codecBundle);
+        return codecBundle;
+    }
+
+    @DeviceSplRange
+    private int getDeviceSplRange(AudioDeviceInfo deviceInfo) {
+        final int internalDeviceType = deviceInfo.getInternalType();
+        if (internalDeviceType == AudioSystem.DEVICE_OUT_SPEAKER) {
+            final String splRange = SystemProperties.get(
+                    SYSTEM_PROPERTY_SPEAKER_SPL_RANGE_SIZE, "unknown");
+            if (!splRange.equals("unknown")) {
+                return stringToSplRange(splRange);
+            }
+
+            @DeviceSplRange int result = SPL_RANGE_SMALL;  // default for phone/tablet/watch
+            if (mAudioService.isPlatformAutomotive() || mAudioService.isPlatformTelevision()) {
+                result = SPL_RANGE_MEDIUM;
+            }
+
+            return result;
+        } else if (internalDeviceType == AudioSystem.DEVICE_OUT_USB_HEADSET
+                || internalDeviceType == AudioSystem.DEVICE_OUT_WIRED_HEADPHONE
+                || internalDeviceType == AudioSystem.DEVICE_OUT_WIRED_HEADSET
+                || (AudioSystem.isBluetoothDevice(internalDeviceType)
+                && mAudioService.getBluetoothAudioDeviceCategory(deviceInfo.getAddress(),
+                AudioSystem.isBluetoothLeDevice(internalDeviceType))
+                == AUDIO_DEVICE_CATEGORY_HEADPHONES)) {
+            return SPL_RANGE_LARGE;
+        } else if (AudioSystem.isBluetoothDevice(internalDeviceType)) {
+            final int audioDeviceType = mAudioService.getBluetoothAudioDeviceCategory(
+                    deviceInfo.getAddress(), AudioSystem.isBluetoothLeDevice(internalDeviceType));
+            if (audioDeviceType == AUDIO_DEVICE_CATEGORY_CARKIT) {
+                return SPL_RANGE_MEDIUM;
+            } else if (audioDeviceType == AUDIO_DEVICE_CATEGORY_WATCH) {
+                return SPL_RANGE_SMALL;
+            } else if (audioDeviceType == AUDIO_DEVICE_CATEGORY_HEARING_AID) {
+                return SPL_RANGE_SMALL;
+            }
+        }
+
+        return SPL_RANGE_UNKNOWN;
+    }
+
+    private static String splRangeToString(@DeviceSplRange int splRange) {
+        switch (splRange) {
+            case SPL_RANGE_LARGE: return "large";
+            case SPL_RANGE_MEDIUM: return "medium";
+            case SPL_RANGE_SMALL: return "small";
+            default: return "unknown";
+        }
+    }
+
+    @DeviceSplRange
+    private static int stringToSplRange(String splRange) {
+        switch (splRange) {
+            case "large": return SPL_RANGE_LARGE;
+            case "medium": return SPL_RANGE_MEDIUM;
+            case "small": return SPL_RANGE_SMALL;
+            default: return SPL_RANGE_UNKNOWN;
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/devicestate/DeviceStateProvider.java b/services/core/java/com/android/server/devicestate/DeviceStateProvider.java
index af33de0..50ab3f8 100644
--- a/services/core/java/com/android/server/devicestate/DeviceStateProvider.java
+++ b/services/core/java/com/android/server/devicestate/DeviceStateProvider.java
@@ -63,13 +63,27 @@
      */
     int SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_DISABLED = 5;
 
+    /**
+     * Indicating that the supported device states have changed because an external display was
+     * added.
+     */
+    int SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_ADDED = 6;
+
+    /**
+     * Indicating that the supported device states have changed because an external display was
+     * removed.
+     */
+    int SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_REMOVED = 7;
+
     @IntDef(prefix = { "SUPPORTED_DEVICE_STATES_CHANGED_" }, value = {
             SUPPORTED_DEVICE_STATES_CHANGED_DEFAULT,
             SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED,
             SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_NORMAL,
             SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_CRITICAL,
             SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_ENABLED,
-            SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_DISABLED
+            SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_DISABLED,
+            SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_ADDED,
+            SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_REMOVED
     })
     @Retention(RetentionPolicy.SOURCE)
     @interface SupportedStatesUpdatedReason {}
diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java
index ba321ae..db636d6 100644
--- a/services/core/java/com/android/server/display/LogicalDisplay.java
+++ b/services/core/java/com/android/server/display/LogicalDisplay.java
@@ -17,6 +17,8 @@
 package com.android.server.display;
 
 import static com.android.server.display.DisplayDeviceInfo.TOUCH_NONE;
+import static com.android.server.wm.utils.DisplayInfoOverrides.WM_OVERRIDE_FIELDS;
+import static com.android.server.wm.utils.DisplayInfoOverrides.copyDisplayInfoFields;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -33,6 +35,7 @@
 
 import com.android.server.display.layout.Layout;
 import com.android.server.display.mode.DisplayModeDirector;
+import com.android.server.wm.utils.DisplayInfoOverrides;
 import com.android.server.wm.utils.InsetUtils;
 
 import java.io.PrintWriter;
@@ -252,24 +255,8 @@
     public DisplayInfo getDisplayInfoLocked() {
         if (mInfo.get() == null) {
             DisplayInfo info = new DisplayInfo();
-            info.copyFrom(mBaseDisplayInfo);
-            if (mOverrideDisplayInfo != null) {
-                info.appWidth = mOverrideDisplayInfo.appWidth;
-                info.appHeight = mOverrideDisplayInfo.appHeight;
-                info.smallestNominalAppWidth = mOverrideDisplayInfo.smallestNominalAppWidth;
-                info.smallestNominalAppHeight = mOverrideDisplayInfo.smallestNominalAppHeight;
-                info.largestNominalAppWidth = mOverrideDisplayInfo.largestNominalAppWidth;
-                info.largestNominalAppHeight = mOverrideDisplayInfo.largestNominalAppHeight;
-                info.logicalWidth = mOverrideDisplayInfo.logicalWidth;
-                info.logicalHeight = mOverrideDisplayInfo.logicalHeight;
-                info.physicalXDpi = mOverrideDisplayInfo.physicalXDpi;
-                info.physicalYDpi = mOverrideDisplayInfo.physicalYDpi;
-                info.rotation = mOverrideDisplayInfo.rotation;
-                info.displayCutout = mOverrideDisplayInfo.displayCutout;
-                info.logicalDensityDpi = mOverrideDisplayInfo.logicalDensityDpi;
-                info.roundedCorners = mOverrideDisplayInfo.roundedCorners;
-                info.displayShape = mOverrideDisplayInfo.displayShape;
-            }
+            copyDisplayInfoFields(info, mBaseDisplayInfo, mOverrideDisplayInfo,
+                    WM_OVERRIDE_FIELDS);
             mInfo.set(info);
         }
         return mInfo.get();
diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
index aa80612..5cfbf26 100644
--- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
+++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
@@ -86,6 +86,10 @@
             Flags.FLAG_BRIGHTNESS_INT_RANGE_USER_PERCEPTION,
             Flags::brightnessIntRangeUserPerception);
 
+    private final FlagState mVsyncProximityVote = new FlagState(
+            Flags.FLAG_ENABLE_EXTERNAL_VSYNC_PROXIMITY_VOTE,
+            Flags::enableExternalVsyncProximityVote);
+
     /** Returns whether connected display management is enabled or not. */
     public boolean isConnectedDisplayManagementEnabled() {
         return mConnectedDisplayManagementFlagState.isEnabled();
@@ -170,6 +174,10 @@
         return mBrightnessIntRangeUserPerceptionFlagState.isEnabled();
     }
 
+    public boolean isExternalVsyncProximityVoteEnabled() {
+        return mVsyncProximityVote.isEnabled();
+    }
+
     /**
      * dumps all flagstates
      * @param pw printWriter
@@ -188,6 +196,7 @@
         pw.println(" " + mPowerThrottlingClamperFlagState);
         pw.println(" " + mSmallAreaDetectionFlagState);
         pw.println(" " + mBrightnessIntRangeUserPerceptionFlagState);
+        pw.println(" " + mVsyncProximityVote);
     }
 
     private static class FlagState {
diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig
index e28b415..d95bdae 100644
--- a/services/core/java/com/android/server/display/feature/display_flags.aconfig
+++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig
@@ -112,3 +112,11 @@
     bug: "183655602"
     is_fixed_read_only: true
 }
+
+flag {
+    name: "enable_external_vsync_proximity_vote"
+    namespace: "display_manager"
+    description: "Feature flag for external vsync proximity vote"
+    bug: "284866750"
+    is_fixed_read_only: true
+}
diff --git a/services/core/java/com/android/server/display/mode/BaseModeRefreshRateVote.java b/services/core/java/com/android/server/display/mode/BaseModeRefreshRateVote.java
new file mode 100644
index 0000000..c04df64
--- /dev/null
+++ b/services/core/java/com/android/server/display/mode/BaseModeRefreshRateVote.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2023 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.display.mode;
+
+import java.util.Objects;
+
+class BaseModeRefreshRateVote implements Vote {
+
+    /**
+     * The preferred refresh rate selected by the app. It is used to validate that the summary
+     * refresh rate ranges include this value, and are not restricted by a lower priority vote.
+     */
+    final float mAppRequestBaseModeRefreshRate;
+
+    BaseModeRefreshRateVote(float baseModeRefreshRate) {
+        mAppRequestBaseModeRefreshRate = baseModeRefreshRate;
+    }
+
+    @Override
+    public void updateSummary(DisplayModeDirector.VoteSummary summary) {
+        if (summary.appRequestBaseModeRefreshRate == 0f
+                && mAppRequestBaseModeRefreshRate > 0f) {
+            summary.appRequestBaseModeRefreshRate = mAppRequestBaseModeRefreshRate;
+        }
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof BaseModeRefreshRateVote that)) return false;
+        return Float.compare(that.mAppRequestBaseModeRefreshRate,
+                mAppRequestBaseModeRefreshRate) == 0;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mAppRequestBaseModeRefreshRate);
+    }
+
+    @Override
+    public String toString() {
+        return "BaseModeRefreshRateVote{ mAppRequestBaseModeRefreshRate="
+                + mAppRequestBaseModeRefreshRate + " }";
+    }
+}
diff --git a/services/core/java/com/android/server/display/mode/CombinedVote.java b/services/core/java/com/android/server/display/mode/CombinedVote.java
new file mode 100644
index 0000000..f24fe3a
--- /dev/null
+++ b/services/core/java/com/android/server/display/mode/CombinedVote.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2023 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.display.mode;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+class CombinedVote implements Vote {
+    final List<Vote> mVotes;
+
+    CombinedVote(List<Vote> votes) {
+        mVotes = Collections.unmodifiableList(votes);
+    }
+
+    @Override
+    public void updateSummary(DisplayModeDirector.VoteSummary summary) {
+        mVotes.forEach(vote -> vote.updateSummary(summary));
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof CombinedVote that)) return false;
+        return Objects.equals(mVotes, that.mVotes);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mVotes);
+    }
+
+    @Override
+    public String toString() {
+        return "CombinedVote{ mVotes=" + mVotes + " }";
+    }
+}
diff --git a/services/core/java/com/android/server/display/mode/DisableRefreshRateSwitchingVote.java b/services/core/java/com/android/server/display/mode/DisableRefreshRateSwitchingVote.java
new file mode 100644
index 0000000..2fc5590
--- /dev/null
+++ b/services/core/java/com/android/server/display/mode/DisableRefreshRateSwitchingVote.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2023 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.display.mode;
+
+import java.util.Objects;
+
+class DisableRefreshRateSwitchingVote implements Vote {
+
+    /**
+     * Whether refresh rate switching should be disabled (i.e. the refresh rate range is
+     * a single value).
+     */
+    final boolean mDisableRefreshRateSwitching;
+
+    DisableRefreshRateSwitchingVote(boolean disableRefreshRateSwitching) {
+        mDisableRefreshRateSwitching = disableRefreshRateSwitching;
+    }
+
+    @Override
+    public void updateSummary(DisplayModeDirector.VoteSummary summary) {
+        summary.disableRefreshRateSwitching =
+                summary.disableRefreshRateSwitching || mDisableRefreshRateSwitching;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof DisableRefreshRateSwitchingVote that)) return false;
+        return mDisableRefreshRateSwitching == that.mDisableRefreshRateSwitching;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mDisableRefreshRateSwitching);
+    }
+
+    @Override
+    public String toString() {
+        return "DisableRefreshRateSwitchingVote{ mDisableRefreshRateSwitching="
+                + mDisableRefreshRateSwitching + " }";
+    }
+}
diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
index 9587f55..8eb03ec 100644
--- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
@@ -262,7 +262,7 @@
         mVotesStorage.setLoggingEnabled(loggingEnabled);
     }
 
-    private static final class VoteSummary {
+    static final class VoteSummary {
         public float minPhysicalRefreshRate;
         public float maxPhysicalRefreshRate;
         public float minRenderFrameRate;
@@ -274,7 +274,12 @@
         public boolean disableRefreshRateSwitching;
         public float appRequestBaseModeRefreshRate;
 
-        VoteSummary() {
+        public List<SupportedModesVote.SupportedMode> supportedModes;
+
+        final boolean mIsDisplayResolutionRangeVotingEnabled;
+
+        VoteSummary(boolean isDisplayResolutionRangeVotingEnabled) {
+            mIsDisplayResolutionRangeVotingEnabled = isDisplayResolutionRangeVotingEnabled;
             reset();
         }
 
@@ -322,46 +327,7 @@
                 continue;
             }
 
-            // For physical refresh rates, just use the tightest bounds of all the votes.
-            // The refresh rate cannot be lower than the minimal render frame rate.
-            final float minPhysicalRefreshRate = Math.max(vote.refreshRateRanges.physical.min,
-                    vote.refreshRateRanges.render.min);
-            summary.minPhysicalRefreshRate = Math.max(summary.minPhysicalRefreshRate,
-                    minPhysicalRefreshRate);
-            summary.maxPhysicalRefreshRate = Math.min(summary.maxPhysicalRefreshRate,
-                    vote.refreshRateRanges.physical.max);
-
-            // Same goes to render frame rate, but frame rate cannot exceed the max physical
-            // refresh rate
-            final float maxRenderFrameRate = Math.min(vote.refreshRateRanges.render.max,
-                    vote.refreshRateRanges.physical.max);
-            summary.minRenderFrameRate = Math.max(summary.minRenderFrameRate,
-                    vote.refreshRateRanges.render.min);
-            summary.maxRenderFrameRate = Math.min(summary.maxRenderFrameRate, maxRenderFrameRate);
-
-            // For display size, disable refresh rate switching and base mode refresh rate use only
-            // the first vote we come across (i.e. the highest priority vote that includes the
-            // attribute).
-            if (vote.height > 0 && vote.width > 0) {
-                if (summary.width == Vote.INVALID_SIZE && summary.height == Vote.INVALID_SIZE) {
-                    summary.width = vote.width;
-                    summary.height = vote.height;
-                    summary.minWidth = vote.minWidth;
-                    summary.minHeight = vote.minHeight;
-                } else if (mIsDisplayResolutionRangeVotingEnabled) {
-                    summary.width = Math.min(summary.width, vote.width);
-                    summary.height = Math.min(summary.height, vote.height);
-                    summary.minWidth = Math.max(summary.minWidth, vote.minWidth);
-                    summary.minHeight = Math.max(summary.minHeight, vote.minHeight);
-                }
-            }
-            if (!summary.disableRefreshRateSwitching && vote.disableRefreshRateSwitching) {
-                summary.disableRefreshRateSwitching = true;
-            }
-            if (summary.appRequestBaseModeRefreshRate == 0f
-                    && vote.appRequestBaseModeRefreshRate > 0f) {
-                summary.appRequestBaseModeRefreshRate = vote.appRequestBaseModeRefreshRate;
-            }
+            vote.updateSummary(summary);
 
             if (mLoggingEnabled) {
                 Slog.w(TAG, "Vote summary for priority " + Vote.priorityToString(priority)
@@ -443,7 +409,7 @@
 
             ArrayList<Display.Mode> availableModes = new ArrayList<>();
             availableModes.add(defaultMode);
-            VoteSummary primarySummary = new VoteSummary();
+            VoteSummary primarySummary = new VoteSummary(mIsDisplayResolutionRangeVotingEnabled);
             int lowestConsideredPriority = Vote.MIN_PRIORITY;
             int highestConsideredPriority = Vote.MAX_PRIORITY;
 
@@ -526,7 +492,7 @@
                                 + "]");
             }
 
-            VoteSummary appRequestSummary = new VoteSummary();
+            VoteSummary appRequestSummary = new VoteSummary(mIsDisplayResolutionRangeVotingEnabled);
             summarizeVotes(
                     votes,
                     Vote.APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF,
diff --git a/services/core/java/com/android/server/display/mode/RefreshRateVote.java b/services/core/java/com/android/server/display/mode/RefreshRateVote.java
new file mode 100644
index 0000000..173b3c5
--- /dev/null
+++ b/services/core/java/com/android/server/display/mode/RefreshRateVote.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2023 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.display.mode;
+
+import java.util.Objects;
+
+
+/**
+ * Information about the refresh rate frame rate ranges DM would like to set the display to.
+ */
+abstract class RefreshRateVote implements Vote {
+    final float mMinRefreshRate;
+
+    final float mMaxRefreshRate;
+
+    RefreshRateVote(float minRefreshRate, float maxRefreshRate) {
+        mMinRefreshRate = minRefreshRate;
+        mMaxRefreshRate = maxRefreshRate;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof RefreshRateVote that)) return false;
+        return Float.compare(that.mMinRefreshRate, mMinRefreshRate) == 0
+                && Float.compare(that.mMaxRefreshRate, mMaxRefreshRate) == 0;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mMinRefreshRate, mMaxRefreshRate);
+    }
+
+    @Override
+    public String toString() {
+        return "RefreshRateVote{  mMinRefreshRate=" + mMinRefreshRate
+                + ", mMaxRefreshRate=" + mMaxRefreshRate + " }";
+    }
+
+    static class RenderVote extends RefreshRateVote {
+        RenderVote(float minRefreshRate, float maxRefreshRate) {
+            super(minRefreshRate, maxRefreshRate);
+        }
+
+        /**
+         * Summary:        minRender            minPhysical                    maxRender
+         *                    v                     v                             v
+         * -------------------|---------------------"-----------------------------|---------
+         *             ^                ^                   ^*             ^           ^
+         *  Vote: min(ignored)     min(applied)  min(applied+physical)  max(applied)  max(ignored)
+         */
+        @Override
+        public void updateSummary(DisplayModeDirector.VoteSummary summary) {
+            summary.minRenderFrameRate = Math.max(summary.minRenderFrameRate, mMinRefreshRate);
+            summary.maxRenderFrameRate = Math.min(summary.maxRenderFrameRate, mMaxRefreshRate);
+            // Physical refresh rate cannot be lower than the minimal render frame rate.
+            summary.minPhysicalRefreshRate = Math.max(summary.minPhysicalRefreshRate,
+                    mMinRefreshRate);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (!(o instanceof RefreshRateVote.RenderVote)) return false;
+            return super.equals(o);
+        }
+
+        @Override
+        public String toString() {
+            return "RenderVote{ " + super.toString() + " }";
+        }
+    }
+
+    static class PhysicalVote extends RefreshRateVote {
+        PhysicalVote(float minRefreshRate, float maxRefreshRate) {
+            super(minRefreshRate, maxRefreshRate);
+        }
+
+        /**
+         * Summary:        minPhysical                   maxRender               maxPhysical
+         *                    v                             v                        v
+         * -------------------"-----------------------------|----------------------"----------
+         *             ^             ^             ^*                 ^                   ^
+         *  Vote: min(ignored) min(applied)  max(applied+render)     max(applied)  max(ignored)
+         */
+        @Override
+        public void updateSummary(DisplayModeDirector.VoteSummary summary) {
+            summary.minPhysicalRefreshRate = Math.max(summary.minPhysicalRefreshRate,
+                    mMinRefreshRate);
+            summary.maxPhysicalRefreshRate = Math.min(summary.maxPhysicalRefreshRate,
+                    mMaxRefreshRate);
+            // Render frame rate cannot exceed the max physical refresh rate
+            summary.maxRenderFrameRate = Math.min(summary.maxRenderFrameRate, mMaxRefreshRate);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (!(o instanceof RefreshRateVote.PhysicalVote)) return false;
+            return super.equals(o);
+        }
+
+        @Override
+        public String toString() {
+            return "PhysicalVote{ " + super.toString() + " }";
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/display/mode/SizeVote.java b/services/core/java/com/android/server/display/mode/SizeVote.java
new file mode 100644
index 0000000..a9b18a5
--- /dev/null
+++ b/services/core/java/com/android/server/display/mode/SizeVote.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2023 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.display.mode;
+
+import java.util.Objects;
+
+class SizeVote implements Vote {
+
+    /**
+     * The requested width of the display in pixels;
+     */
+    final int mWidth;
+
+    /**
+     * The requested height of the display in pixels;
+     */
+    final int mHeight;
+
+    /**
+     * Min requested width of the display in pixels;
+     */
+    final int mMinWidth;
+
+    /**
+     * Min requested height of the display in pixels;
+     */
+    final int mMinHeight;
+
+    SizeVote(int width, int height, int minWidth, int minHeight) {
+        mWidth = width;
+        mHeight = height;
+        mMinWidth = minWidth;
+        mMinHeight = minHeight;
+    }
+
+    @Override
+    public void updateSummary(DisplayModeDirector.VoteSummary summary) {
+        if (mHeight > 0 && mWidth > 0) {
+            // For display size, disable refresh rate switching and base mode refresh rate use
+            // only the first vote we come across (i.e. the highest priority vote that includes
+            // the attribute).
+            if (summary.width == Vote.INVALID_SIZE && summary.height == Vote.INVALID_SIZE) {
+                summary.width = mWidth;
+                summary.height = mHeight;
+                summary.minWidth = mMinWidth;
+                summary.minHeight = mMinHeight;
+            } else if (summary.mIsDisplayResolutionRangeVotingEnabled) {
+                summary.width = Math.min(summary.width, mWidth);
+                summary.height = Math.min(summary.height, mHeight);
+                summary.minWidth = Math.max(summary.minWidth, mMinWidth);
+                summary.minHeight = Math.max(summary.minHeight, mMinHeight);
+            }
+        }
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof SizeVote sizeVote)) return false;
+        return mWidth == sizeVote.mWidth && mHeight == sizeVote.mHeight
+                && mMinWidth == sizeVote.mMinWidth && mMinHeight == sizeVote.mMinHeight;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mWidth, mHeight, mMinWidth, mMinHeight);
+    }
+
+    @Override
+    public String toString() {
+        return "SizeVote{ mWidth=" + mWidth + ", mHeight=" + mHeight
+                + ", mMinWidth=" + mMinWidth + ", mMinHeight=" + mMinHeight + " }";
+    }
+}
diff --git a/services/core/java/com/android/server/display/mode/SupportedModesVote.java b/services/core/java/com/android/server/display/mode/SupportedModesVote.java
new file mode 100644
index 0000000..b31461f
--- /dev/null
+++ b/services/core/java/com/android/server/display/mode/SupportedModesVote.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2023 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.display.mode;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+class SupportedModesVote implements Vote {
+
+    final List<SupportedMode> mSupportedModes;
+
+    SupportedModesVote(List<SupportedMode> supportedModes) {
+        mSupportedModes = Collections.unmodifiableList(supportedModes);
+    }
+
+    /**
+     * Summary should have subset of supported modes.
+     * If Vote1.supportedModes=(A,B), Vote2.supportedModes=(B,C) then summary.supportedModes=(B)
+     * If summary.supportedModes==null then there is no restriction on supportedModes
+     */
+    @Override
+    public void updateSummary(DisplayModeDirector.VoteSummary summary) {
+        if (summary.supportedModes == null) {
+            summary.supportedModes = new ArrayList<>(mSupportedModes);
+        } else {
+            summary.supportedModes.retainAll(mSupportedModes);
+        }
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof SupportedModesVote that)) return false;
+        return mSupportedModes.equals(that.mSupportedModes);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mSupportedModes);
+    }
+
+    @Override
+    public String toString() {
+        return "SupportedModesVote{ mSupportedModes=" + mSupportedModes + " }";
+    }
+
+    static class SupportedMode {
+        final float mPeakRefreshRate;
+        final float mVsyncRate;
+
+
+        SupportedMode(float peakRefreshRate, float vsyncRate) {
+            mPeakRefreshRate = peakRefreshRate;
+            mVsyncRate = vsyncRate;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (!(o instanceof SupportedMode that)) return false;
+            return Float.compare(that.mPeakRefreshRate, mPeakRefreshRate) == 0
+                    && Float.compare(that.mVsyncRate, mVsyncRate) == 0;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mPeakRefreshRate, mVsyncRate);
+        }
+
+        @Override
+        public String toString() {
+            return "SupportedMode{ mPeakRefreshRate=" + mPeakRefreshRate
+                    + ", mVsyncRate=" + mVsyncRate + " }";
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/display/mode/Vote.java b/services/core/java/com/android/server/display/mode/Vote.java
index b6a6069..c1cdd69 100644
--- a/services/core/java/com/android/server/display/mode/Vote.java
+++ b/services/core/java/com/android/server/display/mode/Vote.java
@@ -16,15 +16,13 @@
 
 package com.android.server.display.mode;
 
-import android.view.SurfaceControl;
+import java.util.List;
 
-import java.util.Objects;
-
-final class Vote {
+interface Vote {
     // DEFAULT_RENDER_FRAME_RATE votes for render frame rate [0, DEFAULT]. As the lowest
     // priority vote, it's overridden by all other considerations. It acts to set a default
     // frame rate for a device.
-    static final int PRIORITY_DEFAULT_RENDER_FRAME_RATE = 0;
+    int PRIORITY_DEFAULT_RENDER_FRAME_RATE = 0;
 
     // PRIORITY_FLICKER_REFRESH_RATE votes for a single refresh rate like [60,60], [90,90] or
     // null. It is used to set a preferred refresh rate value in case the higher priority votes
@@ -32,21 +30,21 @@
     static final int PRIORITY_FLICKER_REFRESH_RATE = 1;
 
     // High-brightness-mode may need a specific range of refresh-rates to function properly.
-    static final int PRIORITY_HIGH_BRIGHTNESS_MODE = 2;
+    int PRIORITY_HIGH_BRIGHTNESS_MODE = 2;
 
     // SETTING_MIN_RENDER_FRAME_RATE is used to propose a lower bound of the render frame rate.
     // It votes [minRefreshRate, Float.POSITIVE_INFINITY]
-    static final int PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE = 3;
+    int PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE = 3;
 
     // User setting preferred display resolution.
-    static final int PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE = 4;
+    int PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE = 4;
 
     // APP_REQUEST_RENDER_FRAME_RATE_RANGE is used to for internal apps to limit the render
     // frame rate in certain cases, mostly to preserve power.
     // @see android.view.WindowManager.LayoutParams#preferredMinRefreshRate
     // @see android.view.WindowManager.LayoutParams#preferredMaxRefreshRate
     // It votes to [preferredMinRefreshRate, preferredMaxRefreshRate].
-    static final int PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE = 5;
+    int PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE = 5;
 
     // We split the app request into different priorities in case we can satisfy one desire
     // without the other.
@@ -72,181 +70,100 @@
     // The preferred refresh rate is set on the main surface of the app outside of
     // DisplayModeDirector.
     // @see com.android.server.wm.WindowState#updateFrameRateSelectionPriorityIfNeeded
-    static final int PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE = 6;
+    int PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE = 6;
 
-    static final int PRIORITY_APP_REQUEST_SIZE = 7;
+    int PRIORITY_APP_REQUEST_SIZE = 7;
 
     // SETTING_PEAK_RENDER_FRAME_RATE has a high priority and will restrict the bounds of the
     // rest of low priority voters. It votes [0, max(PEAK, MIN)]
-    static final int PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE = 8;
+    int PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE = 8;
 
     // Restrict all displays to 60Hz when external display is connected. It votes [59Hz, 61Hz].
-    static final int PRIORITY_SYNCHRONIZED_REFRESH_RATE = 9;
+    int PRIORITY_SYNCHRONIZED_REFRESH_RATE = 9;
 
     // Restrict displays max available resolution and refresh rates. It votes [0, LIMIT]
-    static final int PRIORITY_LIMIT_MODE = 10;
+    int PRIORITY_LIMIT_MODE = 10;
 
     // To avoid delay in switching between 60HZ -> 90HZ when activating LHBM, set refresh
     // rate to max value (same as for PRIORITY_UDFPS) on lock screen
-    static final int PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE = 11;
+    int PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE = 11;
 
     // For concurrent displays we want to limit refresh rate on all displays
-    static final int PRIORITY_LAYOUT_LIMITED_FRAME_RATE = 12;
+    int PRIORITY_LAYOUT_LIMITED_FRAME_RATE = 12;
 
     // LOW_POWER_MODE force the render frame rate to [0, 60HZ] if
     // Settings.Global.LOW_POWER_MODE is on.
-    static final int PRIORITY_LOW_POWER_MODE = 13;
+    int PRIORITY_LOW_POWER_MODE = 13;
 
     // PRIORITY_FLICKER_REFRESH_RATE_SWITCH votes for disabling refresh rate switching. If the
     // higher priority voters' result is a range, it will fix the rate to a single choice.
     // It's used to avoid refresh rate switches in certain conditions which may result in the
     // user seeing the display flickering when the switches occur.
-    static final int PRIORITY_FLICKER_REFRESH_RATE_SWITCH = 14;
+    int PRIORITY_FLICKER_REFRESH_RATE_SWITCH = 14;
 
     // Force display to [0, 60HZ] if skin temperature is at or above CRITICAL.
-    static final int PRIORITY_SKIN_TEMPERATURE = 15;
+    int PRIORITY_SKIN_TEMPERATURE = 15;
 
     // The proximity sensor needs the refresh rate to be locked in order to function, so this is
     // set to a high priority.
-    static final int PRIORITY_PROXIMITY = 16;
+    int PRIORITY_PROXIMITY = 16;
 
     // The Under-Display Fingerprint Sensor (UDFPS) needs the refresh rate to be locked in order
     // to function, so this needs to be the highest priority of all votes.
-    static final int PRIORITY_UDFPS = 17;
+    int PRIORITY_UDFPS = 17;
 
     // Whenever a new priority is added, remember to update MIN_PRIORITY, MAX_PRIORITY, and
     // APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF, as well as priorityToString.
 
-    static final int MIN_PRIORITY = PRIORITY_DEFAULT_RENDER_FRAME_RATE;
-    static final int MAX_PRIORITY = PRIORITY_UDFPS;
+    int MIN_PRIORITY = PRIORITY_DEFAULT_RENDER_FRAME_RATE;
+    int MAX_PRIORITY = PRIORITY_UDFPS;
 
     // The cutoff for the app request refresh rate range. Votes with priorities lower than this
     // value will not be considered when constructing the app request refresh rate range.
-    static final int APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF =
+    int APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF =
             PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE;
 
     /**
      * A value signifying an invalid width or height in a vote.
      */
-    static final int INVALID_SIZE = -1;
+    int INVALID_SIZE = -1;
 
-    /**
-     * The requested width of the display in pixels, or INVALID_SIZE;
-     */
-    public final int width;
-    /**
-     * The requested height of the display in pixels, or INVALID_SIZE;
-     */
-    public final int height;
-    /**
-     * Min requested width of the display in pixels, or 0;
-     */
-    public final int minWidth;
-    /**
-     * Min requested height of the display in pixels, or 0;
-     */
-    public final int minHeight;
-    /**
-     * Information about the refresh rate frame rate ranges DM would like to set the display to.
-     */
-    public final SurfaceControl.RefreshRateRanges refreshRateRanges;
-
-    /**
-     * Whether refresh rate switching should be disabled (i.e. the refresh rate range is
-     * a single value).
-     */
-    public final boolean disableRefreshRateSwitching;
-
-    /**
-     * The preferred refresh rate selected by the app. It is used to validate that the summary
-     * refresh rate ranges include this value, and are not restricted by a lower priority vote.
-     */
-    public final float appRequestBaseModeRefreshRate;
+    void updateSummary(DisplayModeDirector.VoteSummary summary);
 
     static Vote forPhysicalRefreshRates(float minRefreshRate, float maxRefreshRate) {
-        return new Vote(/* minWidth= */ 0, /* minHeight= */ 0,
-                /* width= */ INVALID_SIZE, /* height= */ INVALID_SIZE,
-                /* minPhysicalRefreshRate= */ minRefreshRate,
-                /* maxPhysicalRefreshRate= */ maxRefreshRate,
-                /* minRenderFrameRate= */ 0,
-                /* maxRenderFrameRate= */ Float.POSITIVE_INFINITY,
-                /* disableRefreshRateSwitching= */ minRefreshRate == maxRefreshRate,
-                /* baseModeRefreshRate= */ 0f);
+        return new CombinedVote(
+                List.of(
+                        new RefreshRateVote.PhysicalVote(minRefreshRate, maxRefreshRate),
+                        new DisableRefreshRateSwitchingVote(minRefreshRate == maxRefreshRate)
+                )
+        );
     }
 
     static Vote forRenderFrameRates(float minFrameRate, float maxFrameRate) {
-        return new Vote(/* minWidth= */ 0, /* minHeight= */ 0,
-                /* width= */ INVALID_SIZE, /* height= */ INVALID_SIZE,
-                /* minPhysicalRefreshRate= */ 0,
-                /* maxPhysicalRefreshRate= */ Float.POSITIVE_INFINITY,
-                minFrameRate,
-                maxFrameRate,
-                /* disableRefreshRateSwitching= */ false,
-                /* baseModeRefreshRate= */ 0f);
+        return new RefreshRateVote.RenderVote(minFrameRate, maxFrameRate);
     }
 
     static Vote forSize(int width, int height) {
-        return new Vote(/* minWidth= */ width, /* minHeight= */ height,
-                width, height,
-                /* minPhysicalRefreshRate= */ 0,
-                /* maxPhysicalRefreshRate= */ Float.POSITIVE_INFINITY,
-                /* minRenderFrameRate= */ 0,
-                /* maxRenderFrameRate= */ Float.POSITIVE_INFINITY,
-                /* disableRefreshRateSwitching= */ false,
-                /* baseModeRefreshRate= */ 0f);
+        return new SizeVote(width, height, width, height);
     }
 
     static Vote forSizeAndPhysicalRefreshRatesRange(int minWidth, int minHeight,
             int width, int height, float minRefreshRate, float maxRefreshRate) {
-        return new Vote(minWidth, minHeight,
-                width, height,
-                minRefreshRate,
-                maxRefreshRate,
-                /* minRenderFrameRate= */ 0,
-                /* maxRenderFrameRate= */ Float.POSITIVE_INFINITY,
-                /* disableRefreshRateSwitching= */ minRefreshRate == maxRefreshRate,
-                /* baseModeRefreshRate= */ 0f);
+        return new CombinedVote(
+                List.of(
+                        new SizeVote(width, height, minWidth, minHeight),
+                        new RefreshRateVote.PhysicalVote(minRefreshRate, maxRefreshRate),
+                        new DisableRefreshRateSwitchingVote(minRefreshRate == maxRefreshRate)
+                )
+        );
     }
 
     static Vote forDisableRefreshRateSwitching() {
-        return new Vote(/* minWidth= */ 0, /* minHeight= */ 0,
-                /* width= */ INVALID_SIZE, /* height= */ INVALID_SIZE,
-                /* minPhysicalRefreshRate= */ 0,
-                /* maxPhysicalRefreshRate= */ Float.POSITIVE_INFINITY,
-                /* minRenderFrameRate= */ 0,
-                /* maxRenderFrameRate= */ Float.POSITIVE_INFINITY,
-                /* disableRefreshRateSwitching= */ true,
-                /* baseModeRefreshRate= */ 0f);
+        return new DisableRefreshRateSwitchingVote(true);
     }
 
     static Vote forBaseModeRefreshRate(float baseModeRefreshRate) {
-        return new Vote(/* minWidth= */ 0, /* minHeight= */ 0,
-                /* width= */ INVALID_SIZE, /* height= */ INVALID_SIZE,
-                /* minPhysicalRefreshRate= */ 0,
-                /* maxPhysicalRefreshRate= */ Float.POSITIVE_INFINITY,
-                /* minRenderFrameRate= */ 0,
-                /* maxRenderFrameRate= */ Float.POSITIVE_INFINITY,
-                /* disableRefreshRateSwitching= */ false,
-                /* baseModeRefreshRate= */ baseModeRefreshRate);
-    }
-
-    private Vote(int minWidth, int minHeight,
-            int width, int height,
-            float minPhysicalRefreshRate,
-            float maxPhysicalRefreshRate,
-            float minRenderFrameRate,
-            float maxRenderFrameRate,
-            boolean disableRefreshRateSwitching,
-            float baseModeRefreshRate) {
-        this.minWidth = minWidth;
-        this.minHeight = minHeight;
-        this.width = width;
-        this.height = height;
-        this.refreshRateRanges = new SurfaceControl.RefreshRateRanges(
-                new SurfaceControl.RefreshRateRange(minPhysicalRefreshRate, maxPhysicalRefreshRate),
-                new SurfaceControl.RefreshRateRange(minRenderFrameRate, maxRenderFrameRate));
-        this.disableRefreshRateSwitching = disableRefreshRateSwitching;
-        this.appRequestBaseModeRefreshRate = baseModeRefreshRate;
+        return new BaseModeRefreshRateVote(baseModeRefreshRate);
     }
 
     static String priorityToString(int priority) {
@@ -291,33 +208,4 @@
                 return Integer.toString(priority);
         }
     }
-
-    @Override
-    public String toString() {
-        return "Vote: {"
-                + "minWidth: " + minWidth + ", minHeight: " + minHeight
-                + ", width: " + width + ", height: " + height
-                + ", refreshRateRanges: " + refreshRateRanges
-                + ", disableRefreshRateSwitching: " + disableRefreshRateSwitching
-                + ", appRequestBaseModeRefreshRate: "  + appRequestBaseModeRefreshRate + "}";
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(minWidth, minHeight, width, height, refreshRateRanges,
-                disableRefreshRateSwitching, appRequestBaseModeRefreshRate);
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (!(o instanceof Vote)) return false;
-        final var vote = (Vote) o;
-        return  minWidth == vote.minWidth && minHeight == vote.minHeight
-                && width == vote.width && height == vote.height
-                && disableRefreshRateSwitching == vote.disableRefreshRateSwitching
-                && Float.compare(vote.appRequestBaseModeRefreshRate,
-                        appRequestBaseModeRefreshRate) == 0
-                && refreshRateRanges.equals(vote.refreshRateRanges);
-    }
 }
diff --git a/services/core/java/com/android/server/display/mode/VotesStorage.java b/services/core/java/com/android/server/display/mode/VotesStorage.java
index 49c587a..95fb8fc 100644
--- a/services/core/java/com/android/server/display/mode/VotesStorage.java
+++ b/services/core/java/com/android/server/display/mode/VotesStorage.java
@@ -157,13 +157,19 @@
         }
     }
 
-    private int getMaxPhysicalRefreshRate(@Nullable Vote vote) {
+    private static int getMaxPhysicalRefreshRate(@Nullable Vote vote) {
         if (vote == null) {
             return -1;
-        } else if (vote.refreshRateRanges.physical.max == Float.POSITIVE_INFINITY) {
-            return 1000; // for visualisation, otherwise e.g. -1 -> 60 will be unnoticeable
+        } else if (vote instanceof RefreshRateVote.PhysicalVote physicalVote) {
+            return (int) physicalVote.mMaxRefreshRate;
+        } else if (vote instanceof CombinedVote combinedVote) {
+            return combinedVote.mVotes.stream()
+                    .filter(v -> v instanceof RefreshRateVote.PhysicalVote)
+                    .map(pv -> (int) (((RefreshRateVote.PhysicalVote) pv).mMaxRefreshRate))
+                    .min(Integer::compare)
+                    .orElse(1000); // for visualisation
         }
-        return (int) vote.refreshRateRanges.physical.max;
+        return 1000; // for visualisation, otherwise e.g. -1 -> 60 will be unnoticeable
     }
 
     interface Listener {
diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
index 6a43697..4821fbe 100644
--- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
+++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java
@@ -2486,6 +2486,13 @@
         private void onRequestFailedOnHandler(@NonNull MediaRoute2Provider provider,
                 long uniqueRequestId, int reason) {
             if (handleSessionCreationRequestFailed(provider, uniqueRequestId, reason)) {
+                Slog.w(
+                        TAG,
+                        TextUtils.formatSimple(
+                                "onRequestFailedOnHandler | Finished handling session creation"
+                                    + " request failed for provider: %s, uniqueRequestId: %d,"
+                                    + " reason: %d",
+                                provider.getUniqueId(), uniqueRequestId, reason));
                 return;
             }
 
@@ -2515,6 +2522,12 @@
 
             if (matchingRequest == null) {
                 // The failure is not about creating a session.
+                Slog.w(
+                        TAG,
+                        TextUtils.formatSimple(
+                                "handleSessionCreationRequestFailed | No matching request found for"
+                                    + " provider: %s, uniqueRequestId: %d, reason: %d",
+                                provider.getUniqueId(), uniqueRequestId, reason));
                 return false;
             }
 
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 1b640fc..2707b45 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -1765,8 +1765,7 @@
             if (Intent.ACTION_LOCALE_CHANGED.equals(intent.getAction())) {
                 // update system notification channels
                 SystemNotificationChannels.createAll(context);
-                mZenModeHelper.updateDefaultZenRules(Binder.getCallingUid(),
-                        isCallerIsSystemOrSystemUi());
+                mZenModeHelper.updateDefaultZenRules(Binder.getCallingUid());
                 mPreferencesHelper.onLocaleChanged(context, ActivityManager.getCurrentUser());
             }
         }
@@ -5316,7 +5315,9 @@
 
             return mZenModeHelper.addAutomaticZenRule(rulePkg, automaticZenRule,
                     "addAutomaticZenRule", Binder.getCallingUid(),
-                    isCallerIsSystemOrSystemUi());
+                    // TODO: b/308670715: Distinguish FROM_APP from FROM_USER
+                    isCallerIsSystemOrSystemUi() ? ZenModeHelper.FROM_SYSTEM_OR_SYSTEMUI
+                            : ZenModeHelper.FROM_APP);
         }
 
         @Override
@@ -5334,7 +5335,9 @@
 
             return mZenModeHelper.updateAutomaticZenRule(id, automaticZenRule,
                     "updateAutomaticZenRule", Binder.getCallingUid(),
-                    isCallerIsSystemOrSystemUi());
+                    // TODO: b/308670715: Distinguish FROM_APP from FROM_USER
+                    isCallerIsSystemOrSystemUi() ? ZenModeHelper.FROM_SYSTEM_OR_SYSTEMUI
+                            : ZenModeHelper.FROM_APP);
         }
 
         @Override
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 2ef0ca6..89d8200 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -27,6 +27,7 @@
 
 import static com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE;
 
+import android.annotation.IntDef;
 import android.annotation.Nullable;
 import android.annotation.SuppressLint;
 import android.annotation.UserIdInt;
@@ -73,6 +74,7 @@
 import android.provider.Settings.Global;
 import android.service.notification.Condition;
 import android.service.notification.ConditionProviderService;
+import android.service.notification.ZenDeviceEffects;
 import android.service.notification.ZenModeConfig;
 import android.service.notification.ZenModeConfig.ZenRule;
 import android.service.notification.ZenModeProto;
@@ -105,6 +107,8 @@
 
 import java.io.IOException;
 import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -129,6 +133,21 @@
     @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
     static final long SEND_ACTIVATION_AZR_STATUSES = 308673617L;
 
+    /** A rule addition or update that is initiated by the System or SystemUI. */
+    static final int FROM_SYSTEM_OR_SYSTEMUI = 1;
+    /** A rule addition or update that is initiated by the user (through system settings). */
+    static final int FROM_USER = 2;
+    /** A rule addition or update that is initiated by an app (via NotificationManager APIs). */
+    static final int FROM_APP = 3;
+
+    @IntDef(prefix = { "FROM_" }, value = {
+            FROM_SYSTEM_OR_SYSTEMUI,
+            FROM_USER,
+            FROM_APP
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface ChangeOrigin {}
+
     // pkg|userId => uid
     @VisibleForTesting protected final ArrayMap<String, Integer> mRulesUidCache = new ArrayMap<>();
 
@@ -378,7 +397,7 @@
     }
 
     public String addAutomaticZenRule(String pkg, AutomaticZenRule automaticZenRule,
-            String reason, int callingUid, boolean fromSystemOrSystemUi) {
+            String reason, int callingUid, @ChangeOrigin int origin) {
         if (!ZenModeConfig.SYSTEM_AUTHORITY.equals(pkg)) {
             PackageItemInfo component = getServiceInfo(automaticZenRule.getOwner());
             if (component == null) {
@@ -412,10 +431,10 @@
             }
             newConfig = mConfig.copy();
             ZenRule rule = new ZenRule();
-            populateZenRule(pkg, automaticZenRule, rule, true);
+            populateZenRule(pkg, automaticZenRule, rule, true, origin);
             newConfig.automaticRules.put(rule.id, rule);
             if (setConfigLocked(newConfig, reason, rule.component, true, callingUid,
-                    fromSystemOrSystemUi)) {
+                    origin == FROM_SYSTEM_OR_SYSTEMUI)) {
                 return rule.id;
             } else {
                 throw new AndroidRuntimeException("Could not create rule");
@@ -424,7 +443,7 @@
     }
 
     public boolean updateAutomaticZenRule(String ruleId, AutomaticZenRule automaticZenRule,
-            String reason, int callingUid, boolean fromSystemOrSystemUi) {
+            String reason, int callingUid, @ChangeOrigin int origin) {
         ZenModeConfig newConfig;
         synchronized (mConfigLock) {
             if (mConfig == null) return false;
@@ -452,9 +471,9 @@
                 }
             }
 
-            populateZenRule(rule.pkg, automaticZenRule, rule, false);
+            populateZenRule(rule.pkg, automaticZenRule, rule, false, origin);
             return setConfigLocked(newConfig, reason, rule.component, true, callingUid,
-                    fromSystemOrSystemUi);
+                    origin == FROM_SYSTEM_OR_SYSTEMUI);
         }
     }
 
@@ -790,7 +809,7 @@
         }
     }
 
-    protected void updateDefaultZenRules(int callingUid, boolean fromSystemOrSystemUi) {
+    protected void updateDefaultZenRules(int callingUid) {
         updateDefaultAutomaticRuleNames();
         synchronized (mConfigLock) {
             for (ZenRule defaultRule : mDefaultConfig.automaticRules.values()) {
@@ -807,7 +826,7 @@
                         // update default rule (if locale changed, name of rule will change)
                         currRule.name = defaultRule.name;
                         updateAutomaticZenRule(defaultRule.id, zenRuleToAutomaticZenRule(currRule),
-                                "locale changed", callingUid, fromSystemOrSystemUi);
+                                "locale changed", callingUid, FROM_SYSTEM_OR_SYSTEMUI);
                     }
                 }
             }
@@ -850,7 +869,11 @@
     }
 
     private static void populateZenRule(String pkg, AutomaticZenRule automaticZenRule, ZenRule rule,
-            boolean isNew) {
+            boolean isNew, @ChangeOrigin int origin) {
+        // TODO: b/308671593,b/311406021 - Handle origins more precisely:
+        //  - FROM_USER can override anything and updates bitmask of user-modified fields;
+        //  - FROM_SYSTEM_OR_SYSTEMUI can override anything and preserves bitmask;
+        //  - FROM_APP can only update if not user-modified.
         if (rule.enabled != automaticZenRule.isEnabled()) {
             rule.snoozing = false;
         }
@@ -861,7 +884,10 @@
         rule.modified = automaticZenRule.isModified();
         rule.zenPolicy = automaticZenRule.getZenPolicy();
         if (Flags.modesApi()) {
-            rule.zenDeviceEffects = automaticZenRule.getDeviceEffects();
+            rule.zenDeviceEffects = fixZenDeviceEffects(
+                    rule.zenDeviceEffects,
+                    automaticZenRule.getDeviceEffects(),
+                    origin);
         }
         rule.zenMode = NotificationManager.zenModeFromInterruptionFilter(
                 automaticZenRule.getInterruptionFilter(), Global.ZEN_MODE_OFF);
@@ -882,6 +908,50 @@
         }
     }
 
+    /** "
+     * Fix" {@link ZenDeviceEffects} that are being stored as part of a new or updated ZenRule.
+     *
+     * <ul>
+     *     <li> Apps cannot turn on hidden effects (those tagged as {@code @hide}) since they are
+     *     intended for platform-specific rules (e.g. wearables). If it's a new rule, we blank them
+     *     out; if it's an update, we preserve the previous values.
+     * </ul>
+     */
+    @Nullable
+    private static ZenDeviceEffects fixZenDeviceEffects(@Nullable ZenDeviceEffects oldEffects,
+            @Nullable ZenDeviceEffects newEffects, @ChangeOrigin int origin) {
+        // TODO: b/308671593,b/311406021 - Handle origins more precisely:
+        //  - FROM_USER can override anything and updates bitmask of user-modified fields;
+        //  - FROM_SYSTEM_OR_SYSTEMUI can override anything and preserves bitmask;
+        //  - FROM_APP can only update if not user-modified.
+        if (origin == FROM_SYSTEM_OR_SYSTEMUI || origin == FROM_USER) {
+            return newEffects;
+        }
+
+        if (newEffects == null) {
+            return null;
+        }
+        if (oldEffects != null) {
+            return new ZenDeviceEffects.Builder(newEffects)
+                    .setShouldDisableAutoBrightness(oldEffects.shouldDisableAutoBrightness())
+                    .setShouldDisableTapToWake(oldEffects.shouldDisableTapToWake())
+                    .setShouldDisableTiltToWake(oldEffects.shouldDisableTiltToWake())
+                    .setShouldDisableTouch(oldEffects.shouldDisableTouch())
+                    .setShouldMinimizeRadioUsage(oldEffects.shouldMinimizeRadioUsage())
+                    .setShouldMaximizeDoze(oldEffects.shouldMaximizeDoze())
+                    .build();
+        } else {
+            return new ZenDeviceEffects.Builder(newEffects)
+                    .setShouldDisableAutoBrightness(false)
+                    .setShouldDisableTapToWake(false)
+                    .setShouldDisableTiltToWake(false)
+                    .setShouldDisableTouch(false)
+                    .setShouldMinimizeRadioUsage(false)
+                    .setShouldMaximizeDoze(false)
+                    .build();
+        }
+    }
+
     private static AutomaticZenRule zenRuleToAutomaticZenRule(ZenRule rule) {
         AutomaticZenRule azr;
         if (Flags.modesApi()) {
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 8556317..f90bf4b 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -1957,6 +1957,19 @@
         return userTypeDetails.getStatusBarIcon();
     }
 
+    @Override
+    public @StringRes int getProfileLabelResId(@UserIdInt int userId) {
+        checkQueryOrInteractPermissionIfCallerInOtherProfileGroup(userId,
+                "getProfileLabelResId");
+        final UserInfo userInfo = getUserInfoNoChecks(userId);
+        final UserTypeDetails userTypeDetails = getUserTypeDetails(userInfo);
+        if (userInfo == null || userTypeDetails == null) {
+            return Resources.ID_NULL;
+        }
+        final int userIndex = userInfo.profileBadge;
+        return userTypeDetails.getLabel(userIndex);
+    }
+
     public boolean isProfile(@UserIdInt int userId) {
         checkQueryOrInteractPermissionIfCallerInOtherProfileGroup(userId, "isProfile");
         return isProfileUnchecked(userId);
diff --git a/services/core/java/com/android/server/pm/UserTypeDetails.java b/services/core/java/com/android/server/pm/UserTypeDetails.java
index 7bdcd68..56c400a0 100644
--- a/services/core/java/com/android/server/pm/UserTypeDetails.java
+++ b/services/core/java/com/android/server/pm/UserTypeDetails.java
@@ -54,8 +54,15 @@
     /** Whether users of this type can be created. */
     private final boolean mEnabled;
 
-    // TODO(b/142482943): Currently unused and not set. Hook this up.
-    private final int mLabel;
+    /**
+     * Resource IDs ({@link StringRes}) of the user's labels. This might be used to label a
+     * user/profile in tabbed views, etc.
+     * The values are resource IDs referring to the strings not the strings themselves.
+     *
+     * <p>This is an array because, in general, there may be multiple users of the same user type.
+     * In this case, the user is indexed according to its {@link UserInfo#profileBadge}.
+     */
+    private final @Nullable int[] mLabels;
 
     /**
      * Maximum number of this user type allowed on the device.
@@ -160,8 +167,8 @@
     private final @NonNull UserProperties mDefaultUserProperties;
 
     private UserTypeDetails(@NonNull String name, boolean enabled, int maxAllowed,
-            @UserInfoFlag int baseType, @UserInfoFlag int defaultUserInfoPropertyFlags, int label,
-            int maxAllowedPerParent,
+            @UserInfoFlag int baseType, @UserInfoFlag int defaultUserInfoPropertyFlags,
+            @Nullable int[] labels, int maxAllowedPerParent,
             int iconBadge, int badgePlain, int badgeNoBackground,
             int statusBarIcon,
             @Nullable int[] badgeLabels, @Nullable int[] badgeColors,
@@ -181,12 +188,11 @@
         this.mDefaultSystemSettings = defaultSystemSettings;
         this.mDefaultSecureSettings = defaultSecureSettings;
         this.mDefaultCrossProfileIntentFilters = defaultCrossProfileIntentFilters;
-
         this.mIconBadge = iconBadge;
         this.mBadgePlain = badgePlain;
         this.mBadgeNoBackground = badgeNoBackground;
         this.mStatusBarIcon = statusBarIcon;
-        this.mLabel = label;
+        this.mLabels = labels;
         this.mBadgeLabels = badgeLabels;
         this.mBadgeColors = badgeColors;
         this.mDarkThemeBadgeColors = darkThemeBadgeColors;
@@ -234,9 +240,16 @@
         return mDefaultUserInfoPropertyFlags | mBaseType;
     }
 
-    // TODO(b/142482943) Hook this up; it is currently unused.
-    public int getLabel() {
-        return mLabel;
+    /**
+     * Returns the resource ID corresponding to the badgeIndexth label name where the badgeIndex is
+     * expected to be the {@link UserInfo#profileBadge} of the user. If badgeIndex exceeds the
+     * number of labels, returns the label for the highest index.
+     */
+    public @StringRes int getLabel(int badgeIndex) {
+        if (mLabels == null || mLabels.length == 0 || badgeIndex < 0) {
+            return Resources.ID_NULL;
+        }
+        return mLabels[Math.min(badgeIndex, mLabels.length - 1)];
     }
 
     /** Returns whether users of this user type should be badged. */
@@ -358,7 +371,6 @@
         pw.print(prefix); pw.print("mMaxAllowedPerParent: "); pw.println(mMaxAllowedPerParent);
         pw.print(prefix); pw.print("mDefaultUserInfoFlags: ");
         pw.println(UserInfo.flagsToString(mDefaultUserInfoPropertyFlags));
-        pw.print(prefix); pw.print("mLabel: "); pw.println(mLabel);
         mDefaultUserProperties.println(pw, prefix);
 
         final String restrictionsPrefix = prefix + "    ";
@@ -392,6 +404,8 @@
         pw.println(mBadgeColors != null ? mBadgeColors.length : "0(null)");
         pw.print(prefix); pw.print("mDarkThemeBadgeColors.length: ");
         pw.println(mDarkThemeBadgeColors != null ? mDarkThemeBadgeColors.length : "0(null)");
+        pw.print(prefix); pw.print("mLabels.length: ");
+        pw.println(mLabels != null ? mLabels.length : "0(null)");
     }
 
     /** Builder for a {@link UserTypeDetails}; see that class for documentation. */
@@ -408,7 +422,7 @@
         private @Nullable List<DefaultCrossProfileIntentFilter> mDefaultCrossProfileIntentFilters =
                 null;
         private int mEnabled = 1;
-        private int mLabel = Resources.ID_NULL;
+        private @Nullable int[] mLabels = null;
         private @Nullable int[] mBadgeLabels = null;
         private @Nullable int[] mBadgeColors = null;
         private @Nullable int[] mDarkThemeBadgeColors = null;
@@ -488,8 +502,8 @@
             return this;
         }
 
-        public Builder setLabel(int label) {
-            mLabel = label;
+        public Builder setLabels(@StringRes int ... labels) {
+            mLabels = labels;
             return this;
         }
 
@@ -562,7 +576,7 @@
                     mMaxAllowed,
                     mBaseType,
                     mDefaultUserInfoPropertyFlags,
-                    mLabel,
+                    mLabels,
                     mMaxAllowedPerParent,
                     mIconBadge,
                     mBadgePlain,
diff --git a/services/core/java/com/android/server/pm/UserTypeFactory.java b/services/core/java/com/android/server/pm/UserTypeFactory.java
index 7da76c1..4ef8cb7 100644
--- a/services/core/java/com/android/server/pm/UserTypeFactory.java
+++ b/services/core/java/com/android/server/pm/UserTypeFactory.java
@@ -128,7 +128,7 @@
                 .setName(USER_TYPE_PROFILE_CLONE)
                 .setBaseType(FLAG_PROFILE)
                 .setMaxAllowedPerParent(1)
-                .setLabel(0)
+                .setLabels(R.string.profile_label_clone)
                 .setIconBadge(com.android.internal.R.drawable.ic_clone_icon_badge)
                 .setBadgePlain(com.android.internal.R.drawable.ic_clone_badge)
                 // Clone doesn't use BadgeNoBackground, so just set to BadgePlain as a placeholder.
@@ -154,6 +154,10 @@
                                 UserProperties.CROSS_PROFILE_INTENT_FILTER_ACCESS_LEVEL_SYSTEM)
                         .setCrossProfileIntentResolutionStrategy(UserProperties
                                 .CROSS_PROFILE_INTENT_RESOLUTION_STRATEGY_NO_FILTERING)
+                        .setShowInQuietMode(
+                                UserProperties.SHOW_IN_QUIET_MODE_DEFAULT)
+                        .setShowInSharingSurfaces(
+                                UserProperties.SHOW_IN_SHARING_SURFACES_WITH_PARENT)
                         .setMediaSharedWithParent(true)
                         .setCredentialShareableWithParent(true)
                         .setDeleteAppWithParent(true));
@@ -169,7 +173,10 @@
                 .setBaseType(FLAG_PROFILE)
                 .setDefaultUserInfoPropertyFlags(FLAG_MANAGED_PROFILE)
                 .setMaxAllowedPerParent(1)
-                .setLabel(0)
+                .setLabels(
+                        R.string.profile_label_work,
+                        R.string.profile_label_work_2,
+                        R.string.profile_label_work_3)
                 .setIconBadge(com.android.internal.R.drawable.ic_corp_icon_badge_case)
                 .setBadgePlain(com.android.internal.R.drawable.ic_corp_badge_case)
                 .setBadgeNoBackground(com.android.internal.R.drawable.ic_corp_badge_no_background)
@@ -193,6 +200,10 @@
                         .setStartWithParent(true)
                         .setShowInLauncher(UserProperties.SHOW_IN_LAUNCHER_SEPARATE)
                         .setShowInSettings(UserProperties.SHOW_IN_SETTINGS_SEPARATE)
+                        .setShowInQuietMode(
+                                UserProperties.SHOW_IN_QUIET_MODE_PAUSED)
+                        .setShowInSharingSurfaces(
+                                UserProperties.SHOW_IN_SHARING_SURFACES_SEPARATE)
                         .setAuthAlwaysRequiredToDisableQuietMode(false)
                         .setCredentialShareableWithParent(true));
     }
@@ -209,7 +220,10 @@
                 .setName(USER_TYPE_PROFILE_TEST)
                 .setBaseType(FLAG_PROFILE)
                 .setMaxAllowedPerParent(2)
-                .setLabel(0)
+                .setLabels(
+                        R.string.profile_label_test,
+                        R.string.profile_label_test,
+                        R.string.profile_label_test)
                 .setIconBadge(com.android.internal.R.drawable.ic_test_icon_badge_experiment)
                 .setBadgePlain(com.android.internal.R.drawable.ic_test_badge_experiment)
                 .setBadgeNoBackground(com.android.internal.R.drawable.ic_test_badge_no_background)
@@ -240,7 +254,7 @@
                 .setBaseType(FLAG_PROFILE)
                 .setMaxAllowed(1)
                 .setEnabled(UserManager.isCommunalProfileEnabled() ? 1 : 0)
-                .setLabel(0)
+                .setLabels(R.string.profile_label_communal)
                 .setIconBadge(com.android.internal.R.drawable.ic_test_icon_badge_experiment)
                 .setBadgePlain(com.android.internal.R.drawable.ic_test_badge_experiment)
                 .setBadgeNoBackground(com.android.internal.R.drawable.ic_test_badge_no_background)
@@ -276,7 +290,7 @@
                 .setName(USER_TYPE_PROFILE_PRIVATE)
                 .setBaseType(FLAG_PROFILE)
                 .setMaxAllowedPerParent(1)
-                .setLabel(0)
+                .setLabels(R.string.profile_label_private)
                 .setIconBadge(com.android.internal.R.drawable.ic_private_profile_icon_badge)
                 .setBadgePlain(com.android.internal.R.drawable.ic_private_profile_badge)
                 // Private Profile doesn't use BadgeNoBackground, so just set to BadgePlain
@@ -298,7 +312,10 @@
                         .setMediaSharedWithParent(false)
                         .setShowInLauncher(UserProperties.SHOW_IN_LAUNCHER_SEPARATE)
                         .setShowInSettings(UserProperties.SHOW_IN_SETTINGS_SEPARATE)
-                        .setHideInSettingsInQuietMode(true)
+                        .setShowInQuietMode(
+                                UserProperties.SHOW_IN_QUIET_MODE_HIDDEN)
+                        .setShowInSharingSurfaces(
+                                UserProperties.SHOW_IN_SHARING_SURFACES_SEPARATE)
                         .setCrossProfileIntentFilterAccessControl(
                                 UserProperties.CROSS_PROFILE_INTENT_FILTER_ACCESS_LEVEL_SYSTEM)
                         .setInheritDevicePolicy(UserProperties.INHERIT_DEVICE_POLICY_FROM_PARENT));
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 986735f..73c4224 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -3537,7 +3537,8 @@
                     mDisplayManager.setBrightness(screenDisplayId, adjustedLinearBrightness);
 
                     Intent intent = new Intent(Intent.ACTION_SHOW_BRIGHTNESS_DIALOG);
-                    intent.addFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION);
+                    intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION
+                            | Intent.FLAG_ACTIVITY_NO_USER_ACTION);
                     intent.putExtra(EXTRA_FROM_BRIGHTNESS_KEY, true);
                     startActivityAsUser(intent, UserHandle.CURRENT_OR_SELF);
                     logKeyboardSystemsEvent(event, KeyboardLogEvent.getBrightnessEvent(keyCode));
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index eea13f1..eb40104 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -8239,20 +8239,20 @@
 
         @GuardedBy("mBsi")
         private void ensureMultiStateCounters(long timestampMs) {
-            if (mProcStateTimeMs != null) {
-                return;
+            if (mProcStateTimeMs == null) {
+                mProcStateTimeMs =
+                        new TimeInFreqMultiStateCounter(mBsi.mOnBatteryTimeBase,
+                                PROC_STATE_TIME_COUNTER_STATE_COUNT,
+                                mBsi.mCpuScalingPolicies.getScalingStepCount(),
+                                timestampMs);
             }
-
-            mProcStateTimeMs =
-                    new TimeInFreqMultiStateCounter(mBsi.mOnBatteryTimeBase,
-                            PROC_STATE_TIME_COUNTER_STATE_COUNT,
-                            mBsi.mCpuScalingPolicies.getScalingStepCount(),
-                            timestampMs);
-            mProcStateScreenOffTimeMs =
-                    new TimeInFreqMultiStateCounter(mBsi.mOnBatteryScreenOffTimeBase,
-                            PROC_STATE_TIME_COUNTER_STATE_COUNT,
-                            mBsi.mCpuScalingPolicies.getScalingStepCount(),
-                            timestampMs);
+            if (mProcStateScreenOffTimeMs == null) {
+                mProcStateScreenOffTimeMs =
+                        new TimeInFreqMultiStateCounter(mBsi.mOnBatteryScreenOffTimeBase,
+                                PROC_STATE_TIME_COUNTER_STATE_COUNT,
+                                mBsi.mCpuScalingPolicies.getScalingStepCount(),
+                                timestampMs);
+            }
         }
 
         @GuardedBy("mBsi")
diff --git a/services/core/java/com/android/server/webkit/SystemImpl.java b/services/core/java/com/android/server/webkit/SystemImpl.java
index 68f554c..ea8a801 100644
--- a/services/core/java/com/android/server/webkit/SystemImpl.java
+++ b/services/core/java/com/android/server/webkit/SystemImpl.java
@@ -16,6 +16,8 @@
 
 package com.android.server.webkit;
 
+import static android.webkit.Flags.updateServiceV2;
+
 import android.app.ActivityManager;
 import android.app.AppGlobals;
 import android.content.Context;
@@ -237,18 +239,30 @@
 
     @Override
     public int getMultiProcessSetting(Context context) {
-        return Settings.Global.getInt(context.getContentResolver(),
-                                      Settings.Global.WEBVIEW_MULTIPROCESS, 0);
+        if (updateServiceV2()) {
+            throw new IllegalStateException(
+                    "getMultiProcessSetting shouldn't be called if update_service_v2 flag is set.");
+        }
+        return Settings.Global.getInt(
+                context.getContentResolver(), Settings.Global.WEBVIEW_MULTIPROCESS, 0);
     }
 
     @Override
     public void setMultiProcessSetting(Context context, int value) {
-        Settings.Global.putInt(context.getContentResolver(),
-                               Settings.Global.WEBVIEW_MULTIPROCESS, value);
+        if (updateServiceV2()) {
+            throw new IllegalStateException(
+                    "setMultiProcessSetting shouldn't be called if update_service_v2 flag is set.");
+        }
+        Settings.Global.putInt(
+                context.getContentResolver(), Settings.Global.WEBVIEW_MULTIPROCESS, value);
     }
 
     @Override
     public void notifyZygote(boolean enableMultiProcess) {
+        if (updateServiceV2()) {
+            throw new IllegalStateException(
+                    "notifyZygote shouldn't be called if update_service_v2 flag is set.");
+        }
         WebViewZygote.setMultiprocessEnabled(enableMultiProcess);
     }
 
diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateService.java b/services/core/java/com/android/server/webkit/WebViewUpdateService.java
index b3672ec..b12da61 100644
--- a/services/core/java/com/android/server/webkit/WebViewUpdateService.java
+++ b/services/core/java/com/android/server/webkit/WebViewUpdateService.java
@@ -157,8 +157,13 @@
         public void onShellCommand(FileDescriptor in, FileDescriptor out,
                 FileDescriptor err, String[] args, ShellCallback callback,
                 ResultReceiver resultReceiver) {
-            (new WebViewUpdateServiceShellCommand(this)).exec(
-                    this, in, out, err, args, callback, resultReceiver);
+            if (updateServiceV2()) {
+                (new WebViewUpdateServiceShellCommand2(this))
+                        .exec(this, in, out, err, args, callback, resultReceiver);
+            } else {
+                (new WebViewUpdateServiceShellCommand(this))
+                        .exec(this, in, out, err, args, callback, resultReceiver);
+            }
         }
 
 
@@ -275,18 +280,31 @@
 
         @Override // Binder call
         public boolean isMultiProcessEnabled() {
+            if (updateServiceV2()) {
+                throw new IllegalStateException(
+                        "isMultiProcessEnabled shouldn't be called if update_service_v2 flag is"
+                                + " set.");
+            }
             return WebViewUpdateService.this.mImpl.isMultiProcessEnabled();
         }
 
         @Override // Binder call
         public void enableMultiProcess(boolean enable) {
-            if (getContext().checkCallingPermission(
-                        android.Manifest.permission.WRITE_SECURE_SETTINGS)
+            if (updateServiceV2()) {
+                throw new IllegalStateException(
+                        "enableMultiProcess shouldn't be called if update_service_v2 flag is set.");
+            }
+            if (getContext()
+                            .checkCallingPermission(
+                                    android.Manifest.permission.WRITE_SECURE_SETTINGS)
                     != PackageManager.PERMISSION_GRANTED) {
-                String msg = "Permission Denial: enableMultiProcess() from pid="
-                        + Binder.getCallingPid()
-                        + ", uid=" + Binder.getCallingUid()
-                        + " requires " + android.Manifest.permission.WRITE_SECURE_SETTINGS;
+                String msg =
+                        "Permission Denial: enableMultiProcess() from pid="
+                                + Binder.getCallingPid()
+                                + ", uid="
+                                + Binder.getCallingUid()
+                                + " requires "
+                                + android.Manifest.permission.WRITE_SECURE_SETTINGS;
                 Slog.w(TAG, msg);
                 throw new SecurityException(msg);
             }
diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java
index e618c7e..89cb4c8 100644
--- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java
+++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java
@@ -23,6 +23,7 @@
 import android.os.AsyncTask;
 import android.os.Trace;
 import android.os.UserHandle;
+import android.text.TextUtils;
 import android.util.Slog;
 import android.webkit.UserPackage;
 import android.webkit.WebViewFactory;
@@ -70,10 +71,6 @@
         WebViewPackageMissingException(String message) {
             super(message);
         }
-
-        WebViewPackageMissingException(Exception e) {
-            super(e);
-        }
     }
 
     private static final int WAIT_TIMEOUT_MS = 1000; // KEY_DISPATCHING_TIMEOUT is 5000.
@@ -85,9 +82,6 @@
     private static final int VALIDITY_INCORRECT_SIGNATURE = 3;
     private static final int VALIDITY_NO_LIBRARY_FLAG = 4;
 
-    private static final int MULTIPROCESS_SETTING_ON_VALUE = Integer.MAX_VALUE;
-    private static final int MULTIPROCESS_SETTING_OFF_VALUE = Integer.MIN_VALUE;
-
     private final SystemInterface mSystemInterface;
     private final Context mContext;
 
@@ -166,7 +160,6 @@
 
     @Override
     public void prepareWebViewInSystemServer() {
-        mSystemInterface.notifyZygote(isMultiProcessEnabled());
         try {
             synchronized (mLock) {
                 mCurrentWebViewPackage = findPreferredWebViewPackage();
@@ -366,14 +359,10 @@
 
         // Once we've notified the system that the provider has changed and started RELRO creation,
         // try to restart the zygote so that it will be ready when apps use it.
-        if (isMultiProcessEnabled()) {
-            AsyncTask.THREAD_POOL_EXECUTOR.execute(this::startZygoteWhenReady);
-        }
+        AsyncTask.THREAD_POOL_EXECUTOR.execute(this::startZygoteWhenReady);
     }
 
-    /**
-     * Fetch only the currently valid WebView packages.
-     **/
+    /** Fetch only the currently valid WebView packages. */
     @Override
     public WebViewProviderInfo[] getValidWebViewPackages() {
         ProviderAndPackageInfo[] providersAndPackageInfos = getValidWebViewPackagesAndInfos();
@@ -632,62 +621,56 @@
 
     @Override
     public boolean isMultiProcessEnabled() {
-        int settingValue = mSystemInterface.getMultiProcessSetting(mContext);
-        if (mSystemInterface.isMultiProcessDefaultEnabled()) {
-            // Multiprocess should be enabled unless the user has turned it off manually.
-            return settingValue > MULTIPROCESS_SETTING_OFF_VALUE;
-        } else {
-            // Multiprocess should not be enabled, unless the user has turned it on manually.
-            return settingValue >= MULTIPROCESS_SETTING_ON_VALUE;
-        }
+        throw new IllegalStateException(
+                "isMultiProcessEnabled shouldn't be called if update_service_v2 flag is set.");
     }
 
     @Override
     public void enableMultiProcess(boolean enable) {
-        PackageInfo current = getCurrentWebViewPackage();
-        mSystemInterface.setMultiProcessSetting(mContext,
-                enable ? MULTIPROCESS_SETTING_ON_VALUE : MULTIPROCESS_SETTING_OFF_VALUE);
-        mSystemInterface.notifyZygote(enable);
-        if (current != null) {
-            mSystemInterface.killPackageDependents(current.packageName);
-        }
+        throw new IllegalStateException(
+                "enableMultiProcess shouldn't be called if update_service_v2 flag is set.");
     }
 
-    /**
-     * Dump the state of this Service.
-     */
+    /** Dump the state of this Service. */
     @Override
     public void dumpState(PrintWriter pw) {
         pw.println("Current WebView Update Service state");
-        pw.println(String.format("  Multiprocess enabled: %b", isMultiProcessEnabled()));
         synchronized (mLock) {
             if (mCurrentWebViewPackage == null) {
                 pw.println("  Current WebView package is null");
             } else {
-                pw.println(String.format("  Current WebView package (name, version): (%s, %s)",
-                        mCurrentWebViewPackage.packageName,
-                        mCurrentWebViewPackage.versionName));
+                pw.println(
+                        TextUtils.formatSimple(
+                                "  Current WebView package (name, version): (%s, %s)",
+                                mCurrentWebViewPackage.packageName,
+                                mCurrentWebViewPackage.versionName));
             }
-            pw.println(String.format("  Minimum targetSdkVersion: %d",
-                    UserPackage.MINIMUM_SUPPORTED_SDK));
-            pw.println(String.format("  Minimum WebView version code: %d",
-                    mMinimumVersionCode));
-            pw.println(String.format("  Number of relros started: %d",
-                    mNumRelroCreationsStarted));
-            pw.println(String.format("  Number of relros finished: %d",
-                        mNumRelroCreationsFinished));
-            pw.println(String.format("  WebView package dirty: %b", mWebViewPackageDirty));
-            pw.println(String.format("  Any WebView package installed: %b",
-                    mAnyWebViewInstalled));
+            pw.println(
+                    TextUtils.formatSimple(
+                            "  Minimum targetSdkVersion: %d", UserPackage.MINIMUM_SUPPORTED_SDK));
+            pw.println(
+                    TextUtils.formatSimple(
+                            "  Minimum WebView version code: %d", mMinimumVersionCode));
+            pw.println(
+                    TextUtils.formatSimple(
+                            "  Number of relros started: %d", mNumRelroCreationsStarted));
+            pw.println(
+                    TextUtils.formatSimple(
+                            "  Number of relros finished: %d", mNumRelroCreationsFinished));
+            pw.println(TextUtils.formatSimple("  WebView package dirty: %b", mWebViewPackageDirty));
+            pw.println(
+                    TextUtils.formatSimple(
+                            "  Any WebView package installed: %b", mAnyWebViewInstalled));
 
             try {
                 PackageInfo preferredWebViewPackage = findPreferredWebViewPackage();
-                pw.println(String.format(
-                        "  Preferred WebView package (name, version): (%s, %s)",
-                        preferredWebViewPackage.packageName,
-                        preferredWebViewPackage.versionName));
+                pw.println(
+                        TextUtils.formatSimple(
+                                "  Preferred WebView package (name, version): (%s, %s)",
+                                preferredWebViewPackage.packageName,
+                                preferredWebViewPackage.versionName));
             } catch (WebViewPackageMissingException e) {
-                pw.println(String.format("  Preferred WebView package: none"));
+                pw.println("  Preferred WebView package: none");
             }
 
             dumpAllPackageInformationLocked(pw);
@@ -703,29 +686,36 @@
             PackageInfo systemUserPackageInfo =
                     userPackages.get(UserHandle.USER_SYSTEM).getPackageInfo();
             if (systemUserPackageInfo == null) {
-                pw.println(String.format("    %s is NOT installed.", provider.packageName));
+                pw.println(
+                        TextUtils.formatSimple("    %s is NOT installed.", provider.packageName));
                 continue;
             }
 
             int validity = validityResult(provider, systemUserPackageInfo);
-            String packageDetails = String.format(
-                    "versionName: %s, versionCode: %d, targetSdkVersion: %d",
-                    systemUserPackageInfo.versionName,
-                    systemUserPackageInfo.getLongVersionCode(),
-                    systemUserPackageInfo.applicationInfo.targetSdkVersion);
+            String packageDetails =
+                    TextUtils.formatSimple(
+                            "versionName: %s, versionCode: %d, targetSdkVersion: %d",
+                            systemUserPackageInfo.versionName,
+                            systemUserPackageInfo.getLongVersionCode(),
+                            systemUserPackageInfo.applicationInfo.targetSdkVersion);
             if (validity == VALIDITY_OK) {
-                boolean installedForAllUsers = isInstalledAndEnabledForAllUsers(
-                        mSystemInterface.getPackageInfoForProviderAllUsers(mContext, provider));
-                pw.println(String.format(
-                        "    Valid package %s (%s) is %s installed/enabled for all users",
-                        systemUserPackageInfo.packageName,
-                        packageDetails,
-                        installedForAllUsers ? "" : "NOT"));
+                boolean installedForAllUsers =
+                        isInstalledAndEnabledForAllUsers(
+                                mSystemInterface.getPackageInfoForProviderAllUsers(
+                                        mContext, provider));
+                pw.println(
+                        TextUtils.formatSimple(
+                                "    Valid package %s (%s) is %s installed/enabled for all users",
+                                systemUserPackageInfo.packageName,
+                                packageDetails,
+                                installedForAllUsers ? "" : "NOT"));
             } else {
-                pw.println(String.format("    Invalid package %s (%s), reason: %s",
-                        systemUserPackageInfo.packageName,
-                        packageDetails,
-                        getInvalidityReason(validity)));
+                pw.println(
+                        TextUtils.formatSimple(
+                                "    Invalid package %s (%s), reason: %s",
+                                systemUserPackageInfo.packageName,
+                                packageDetails,
+                                getInvalidityReason(validity)));
             }
         }
     }
diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceShellCommand2.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceShellCommand2.java
new file mode 100644
index 0000000..ce95b18
--- /dev/null
+++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceShellCommand2.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.webkit;
+
+import android.os.RemoteException;
+import android.os.ShellCommand;
+import android.text.TextUtils;
+import android.webkit.IWebViewUpdateService;
+
+import java.io.PrintWriter;
+
+class WebViewUpdateServiceShellCommand2 extends ShellCommand {
+    final IWebViewUpdateService mInterface;
+
+    WebViewUpdateServiceShellCommand2(IWebViewUpdateService service) {
+        mInterface = service;
+    }
+
+    @Override
+    public int onCommand(String cmd) {
+        if (cmd == null) {
+            return handleDefaultCommands(cmd);
+        }
+
+        final PrintWriter pw = getOutPrintWriter();
+        try {
+            switch (cmd) {
+                case "set-webview-implementation":
+                    return setWebViewImplementation();
+                default:
+                    return handleDefaultCommands(cmd);
+            }
+        } catch (RemoteException e) {
+            pw.println("Remote exception: " + e);
+        }
+        return -1;
+    }
+
+    private int setWebViewImplementation() throws RemoteException {
+        final PrintWriter pw = getOutPrintWriter();
+        String shellChosenPackage = getNextArg();
+        if (shellChosenPackage == null) {
+            pw.println("Failed to switch, no PACKAGE provided.");
+            pw.println("");
+            helpSetWebViewImplementation();
+            return 1;
+        }
+        String newPackage = mInterface.changeProviderAndSetting(shellChosenPackage);
+        if (shellChosenPackage.equals(newPackage)) {
+            pw.println("Success");
+            return 0;
+        } else {
+            pw.println(
+                    TextUtils.formatSimple(
+                            "Failed to switch to %s, the WebView implementation is now provided by"
+                                    + " %s.",
+                            shellChosenPackage, newPackage));
+            return 1;
+        }
+    }
+
+    public void helpSetWebViewImplementation() {
+        PrintWriter pw = getOutPrintWriter();
+        pw.println("  set-webview-implementation PACKAGE");
+        pw.println("    Set the WebView implementation to the specified package.");
+    }
+
+    @Override
+    public void onHelp() {
+        PrintWriter pw = getOutPrintWriter();
+        pw.println("WebView updater commands:");
+        pw.println("  help");
+        pw.println("    Print this help text.");
+        pw.println("");
+        helpSetWebViewImplementation();
+        pw.println();
+    }
+}
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index d90d4ff..8498368 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -694,7 +694,7 @@
     private boolean mCurrentLaunchCanTurnScreenOn = true;
 
     /** Whether our surface was set to be showing in the last call to {@link #prepareSurfaces} */
-    private boolean mLastSurfaceShowing;
+    boolean mLastSurfaceShowing;
 
     /**
      * The activity is opaque and fills the entire space of this task.
@@ -2565,7 +2565,7 @@
                     }
                 }
                 if (abort) {
-                    surface.remove(false /* prepareAnimation */);
+                    surface.remove(false /* prepareAnimation */, false /* hasImeSurface */);
                 }
             } else {
                 ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Surface returned was null: %s",
@@ -2898,6 +2898,7 @@
 
         final StartingSurfaceController.StartingSurface surface;
         final boolean animate;
+        final boolean hasImeSurface;
         if (mStartingData != null) {
             if (mStartingData.mWaitForSyncTransactionCommit
                     || mTransitionController.isCollecting(this)) {
@@ -2907,6 +2908,7 @@
             }
             animate = prepareAnimation && mStartingData.needRevealAnimation()
                     && mStartingWindow.isVisibleByPolicy();
+            hasImeSurface = mStartingData.hasImeSurface();
             ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Schedule remove starting %s startingWindow=%s"
                             + " animate=%b Callers=%s", this, mStartingWindow, animate,
                     Debug.getCallers(5));
@@ -2926,7 +2928,7 @@
                     this);
             return;
         }
-        surface.remove(animate);
+        surface.remove(animate, hasImeSurface);
     }
 
     /**
@@ -5380,11 +5382,13 @@
                 // Finish should only ever commit visibility=false, so we can check full containment
                 // rather than just direct membership.
                 inFinishingTransition = mTransitionController.inFinishingTransition(this);
-                if (!inFinishingTransition && (visible || !mDisplayContent.isSleeping())) {
+                if (!inFinishingTransition) {
                     if (visible) {
-                        mTransitionController.onVisibleWithoutCollectingTransition(this,
-                                Debug.getCallers(1, 1));
-                    } else {
+                        if (!mDisplayContent.isSleeping() || canShowWhenLocked()) {
+                            mTransitionController.onVisibleWithoutCollectingTransition(this,
+                                    Debug.getCallers(1, 1));
+                        }
+                    } else if (!mDisplayContent.isSleeping()) {
                         Slog.w(TAG, "Set invisible without transition " + this);
                     }
                 }
@@ -6434,20 +6438,22 @@
 
     void stopIfPossible() {
         if (DEBUG_SWITCH) Slog.d(TAG_SWITCH, "Stopping: " + this);
-        final Task rootTask = getRootTask();
+        if (finishing) {
+            Slog.e(TAG, "Request to stop a finishing activity: " + this);
+            destroyIfPossible("stopIfPossible-finishing");
+            return;
+        }
         if (isNoHistory()) {
-            if (!finishing) {
-                if (!rootTask.shouldSleepActivities()) {
-                    ProtoLog.d(WM_DEBUG_STATES, "no-history finish of %s", this);
-                    if (finishIfPossible("stop-no-history", false /* oomAdj */)
-                            != FINISH_RESULT_CANCELLED) {
-                        resumeKeyDispatchingLocked();
-                        return;
-                    }
-                } else {
-                    ProtoLog.d(WM_DEBUG_STATES, "Not finishing noHistory %s on stop "
-                            + "because we're just sleeping", this);
+            if (!task.shouldSleepActivities()) {
+                ProtoLog.d(WM_DEBUG_STATES, "no-history finish of %s", this);
+                if (finishIfPossible("stop-no-history", false /* oomAdj */)
+                        != FINISH_RESULT_CANCELLED) {
+                    resumeKeyDispatchingLocked();
+                    return;
                 }
+            } else {
+                ProtoLog.d(WM_DEBUG_STATES, "Not finishing noHistory %s on stop "
+                        + "because we're just sleeping", this);
             }
         }
 
diff --git a/services/core/java/com/android/server/wm/ActivityRecordInputSink.java b/services/core/java/com/android/server/wm/ActivityRecordInputSink.java
index c61d863..1a19787 100644
--- a/services/core/java/com/android/server/wm/ActivityRecordInputSink.java
+++ b/services/core/java/com/android/server/wm/ActivityRecordInputSink.java
@@ -88,9 +88,11 @@
                         || activityBelowInTask.isUid(mActivityRecord.getUid()));
         if (allowPassthrough || !mIsCompatEnabled || mActivityRecord.isInTransition()
                 || !mActivityRecord.mActivityRecordInputSinkEnabled) {
+            // Set to non-touchable, so the touch events can pass through.
             mInputWindowHandleWrapper.setInputConfigMasked(InputConfig.NOT_TOUCHABLE,
                     InputConfig.NOT_TOUCHABLE);
         } else {
+            // Set to touchable, so it can block by intercepting the touch events.
             mInputWindowHandleWrapper.setInputConfigMasked(0, InputConfig.NOT_TOUCHABLE);
         }
         mInputWindowHandleWrapper.setDisplayId(mActivityRecord.getDisplayId());
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 90eeed2..a21b9b4 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -55,6 +55,7 @@
 import static com.android.server.wm.ActivityRecord.State.PAUSED;
 import static com.android.server.wm.ActivityRecord.State.PAUSING;
 import static com.android.server.wm.ActivityRecord.State.RESTARTING_PROCESS;
+import static com.android.server.wm.ActivityRecord.State.RESUMED;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_ALL;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_CLEANUP;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_IDLE;
@@ -1681,7 +1682,7 @@
             ArrayList<ActivityRecord> activities = null;
             for (int i = mStoppingActivities.size() - 1; i >= 0; i--) {
                 final ActivityRecord r = mStoppingActivities.get(i);
-                if (r.getTask() == task) {
+                if (!r.finishing && r.isState(RESUMED) && r.getTask() == task) {
                     if (activities == null) {
                         activities = new ArrayList<>();
                     }
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index 8cc197c..39e900a 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -319,8 +319,6 @@
                     return BackgroundStartPrivileges.NONE;
                 case ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED:
                     // no explicit choice by the app - let us decide what to do
-                    Slog.i(TAG, "balRequireOptInByPendingIntentCreator = "
-                            + balRequireOptInByPendingIntentCreator());
                     if (!balRequireOptInByPendingIntentCreator()) {
                         // if feature is disabled allow
                         return BackgroundStartPrivileges.ALLOW_BAL;
@@ -331,7 +329,6 @@
                                 DEFAULT_RESCIND_BAL_PRIVILEGES_FROM_PENDING_INTENT_CREATOR,
                                 callingPackage,
                                 UserHandle.getUserHandleForUid(callingUid));
-                        Slog.i(TAG, "changeEnabled = " + changeEnabled);
                         return changeEnabled ? BackgroundStartPrivileges.NONE
                                 : BackgroundStartPrivileges.ALLOW_BAL;
                     }
@@ -340,7 +337,6 @@
                     boolean changeEnabled = CompatChanges.isChangeEnabled(
                             DEFAULT_RESCIND_BAL_PRIVILEGES_FROM_PENDING_INTENT_CREATOR,
                             callingUid);
-                    Slog.i(TAG, "changeEnabled = " + changeEnabled);
                     return changeEnabled ? BackgroundStartPrivileges.NONE
                             : BackgroundStartPrivileges.ALLOW_BAL;
                 default:
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 2c224e4..07cbd58 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -69,8 +69,6 @@
 import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE;
 import static android.view.WindowManager.LayoutParams.LAST_APPLICATION_WINDOW;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_UNRESTRICTED_GESTURE_EXCLUSION;
-import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN;
-import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
 import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
 import static android.view.WindowManager.LayoutParams.TYPE_BOOT_PROGRESS;
@@ -158,6 +156,8 @@
 import static com.android.server.wm.WindowState.EXCLUSION_RIGHT;
 import static com.android.server.wm.WindowState.RESIZE_HANDLE_WIDTH_IN_DP;
 import static com.android.server.wm.WindowStateAnimator.READY_TO_SHOW;
+import static com.android.server.wm.utils.DisplayInfoOverrides.WM_OVERRIDE_FIELDS;
+import static com.android.server.wm.utils.DisplayInfoOverrides.copyDisplayInfoFields;
 import static com.android.server.wm.utils.RegionUtils.forEachRectReverse;
 import static com.android.server.wm.utils.RegionUtils.rectListToRegion;
 import static com.android.window.flags.Flags.explicitRefreshRateHints;
@@ -465,11 +465,20 @@
     boolean mDisplayScalingDisabled;
     final Display mDisplay;
     private final DisplayInfo mDisplayInfo = new DisplayInfo();
+
+    /**
+     * Contains the last DisplayInfo override that was sent to DisplayManager or null if we haven't
+     * set an override yet
+     */
+    @Nullable
+    private DisplayInfo mLastDisplayInfoOverride;
+
     private final DisplayMetrics mDisplayMetrics = new DisplayMetrics();
     private final DisplayPolicy mDisplayPolicy;
     private final DisplayRotation mDisplayRotation;
     @Nullable final DisplayRotationCompatPolicy mDisplayRotationCompatPolicy;
     DisplayFrames mDisplayFrames;
+    private final DisplayUpdater mDisplayUpdater;
 
     private boolean mInTouchMode;
 
@@ -623,7 +632,7 @@
     @VisibleForTesting
     final DeviceStateController mDeviceStateController;
     final Consumer<DeviceStateController.DeviceState> mDeviceStateConsumer;
-    private final PhysicalDisplaySwitchTransitionLauncher mDisplaySwitchTransitionLauncher;
+    final PhysicalDisplaySwitchTransitionLauncher mDisplaySwitchTransitionLauncher;
     final RemoteDisplayChangeController mRemoteDisplayChangeController;
 
     /** Windows added since {@link #mCurrentFocus} was set to null. Used for ANR blaming. */
@@ -1144,6 +1153,7 @@
         mDisplay = display;
         mDisplayId = display.getDisplayId();
         mCurrentUniqueDisplayId = display.getUniqueId();
+        mDisplayUpdater = new ImmediateDisplayUpdater(this);
         mOffTokenAcquirer = mRootWindowContainer.mDisplayOffTokenAcquirer;
         mWallpaperController = new WallpaperController(mWmService, this);
         mWallpaperController.resetLargestDisplay(display);
@@ -1917,28 +1927,6 @@
         return true;
     }
 
-    /** Returns {@code true} if the IME is possible to show on the launching activity. */
-    boolean mayImeShowOnLaunchingActivity(@NonNull ActivityRecord r) {
-        final WindowState win = r.findMainWindow(false /* exclude starting window */);
-        if (win == null) {
-            return false;
-        }
-        // See InputMethodManagerService#shouldRestoreImeVisibility that we expecting the IME
-        // should be hidden when the window set the hidden softInputMode.
-        final int softInputMode = win.mAttrs.softInputMode;
-        switch (softInputMode & WindowManager.LayoutParams.SOFT_INPUT_MASK_STATE) {
-            case SOFT_INPUT_STATE_ALWAYS_HIDDEN:
-            case SOFT_INPUT_STATE_HIDDEN:
-                return false;
-        }
-        final boolean useIme = r.getWindow(
-                w -> WindowManager.LayoutParams.mayUseInputMethod(w.mAttrs.flags)) != null;
-        if (!useIme) {
-            return false;
-        }
-        return r.mLastImeShown || (r.mStartingData != null && r.mStartingData.hasImeSurface());
-    }
-
     /** Returns {@code true} if the top activity is transformed with the new rotation of display. */
     boolean hasTopFixedRotationLaunchingApp() {
         return mFixedRotationLaunchingApp != null
@@ -2301,8 +2289,7 @@
 
         computeSizeRanges(mDisplayInfo, rotated, dw, dh, mDisplayMetrics.density, outConfig);
 
-        mWmService.mDisplayManagerInternal.setDisplayInfoOverrideFromWindowManager(mDisplayId,
-                mDisplayInfo);
+        setDisplayInfoOverride();
 
         if (isDefaultDisplay) {
             mCompatibleScreenScale = CompatibilityInfo.computeCompatibleScaling(mDisplayMetrics,
@@ -2314,6 +2301,20 @@
         return mDisplayInfo;
     }
 
+    /**
+     * Sets the current DisplayInfo in DisplayContent as an override to DisplayManager
+     */
+    private void setDisplayInfoOverride() {
+        mWmService.mDisplayManagerInternal.setDisplayInfoOverrideFromWindowManager(mDisplayId,
+                mDisplayInfo);
+
+        if (mLastDisplayInfoOverride == null) {
+            mLastDisplayInfoOverride = new DisplayInfo();
+        }
+
+        mLastDisplayInfoOverride.copyFrom(mDisplayInfo);
+    }
+
     DisplayCutout calculateDisplayCutoutForRotation(int rotation) {
         return mDisplayCutoutCache.getOrCompute(
                 mIsSizeForced ? mBaseDisplayCutout : mInitialDisplayCutout, rotation)
@@ -2885,12 +2886,15 @@
         return orientation;
     }
 
-    void updateDisplayInfo() {
+    void updateDisplayInfo(@NonNull DisplayInfo newDisplayInfo) {
         // Check if display metrics changed and update base values if needed.
-        updateBaseDisplayMetricsIfNeeded();
+        updateBaseDisplayMetricsIfNeeded(newDisplayInfo);
 
-        mDisplay.getDisplayInfo(mDisplayInfo);
-        mDisplay.getMetrics(mDisplayMetrics);
+        // Update mDisplayInfo with (newDisplayInfo + mLastDisplayInfoOverride) as
+        // updateBaseDisplayMetricsIfNeeded could have updated mLastDisplayInfoOverride
+        copyDisplayInfoFields(/* out= */ mDisplayInfo, /* base= */ newDisplayInfo,
+                /* override= */ mLastDisplayInfoOverride, /* fields= */ WM_OVERRIDE_FIELDS);
+        mDisplayInfo.getAppMetrics(mDisplayMetrics, mDisplay.getDisplayAdjustments());
 
         onDisplayInfoChanged();
         onDisplayChanged(this);
@@ -2976,9 +2980,9 @@
      * If display metrics changed, overrides are not set and it's not just a rotation - update base
      * values.
      */
-    private void updateBaseDisplayMetricsIfNeeded() {
+    private void updateBaseDisplayMetricsIfNeeded(DisplayInfo newDisplayInfo) {
         // Get real display metrics without overrides from WM.
-        mWmService.mDisplayManagerInternal.getNonOverrideDisplayInfo(mDisplayId, mDisplayInfo);
+        mDisplayInfo.copyFrom(newDisplayInfo);
         final int currentRotation = getRotation();
         final int orientation = mDisplayInfo.rotation;
         final boolean rotated = (orientation == ROTATION_90 || orientation == ROTATION_270);
@@ -3010,7 +3014,7 @@
                 // metrics are updated as rotation settings might depend on them
                 mWmService.mDisplayWindowSettings.applySettingsToDisplayLocked(this,
                         /* includeRotationSettings */ false);
-                mDisplaySwitchTransitionLauncher.requestDisplaySwitchTransitionIfNeeded(mDisplayId,
+                mDisplayUpdater.onDisplayContentDisplayPropertiesPreChanged(mDisplayId,
                         mInitialDisplayWidth, mInitialDisplayHeight, newWidth, newHeight);
                 mDisplayRotation.physicalDisplayChanged();
                 mDisplayPolicy.physicalDisplayChanged();
@@ -3046,8 +3050,8 @@
 
             if (physicalDisplayChanged) {
                 mDisplayPolicy.physicalDisplayUpdated();
-                mDisplaySwitchTransitionLauncher.onDisplayUpdated(currentRotation, getRotation(),
-                        getDisplayAreaInfo());
+                mDisplayUpdater.onDisplayContentDisplayPropertiesPostChanged(currentRotation,
+                        getRotation(), getDisplayAreaInfo());
             }
         }
     }
@@ -5494,8 +5498,7 @@
             mDisplayReady = true;
 
             if (mWmService.mDisplayManagerInternal != null) {
-                mWmService.mDisplayManagerInternal
-                        .setDisplayInfoOverrideFromWindowManager(mDisplayId, getDisplayInfo());
+                setDisplayInfoOverride();
                 configureDisplayPolicy();
             }
 
@@ -6138,9 +6141,17 @@
         return mMetricsLogger;
     }
 
-    void onDisplayChanged() {
+    /**
+     * Triggers an update of DisplayInfo from DisplayManager
+     * @param onDisplayChangeApplied callback that is called when the changes are applied
+     */
+    void requestDisplayUpdate(@NonNull Runnable onDisplayChangeApplied) {
+        mDisplayUpdater.updateDisplayInfo(onDisplayChangeApplied);
+    }
+
+    void onDisplayInfoUpdated(@NonNull DisplayInfo newDisplayInfo) {
         final int lastDisplayState = mDisplayInfo.state;
-        updateDisplayInfo();
+        updateDisplayInfo(newDisplayInfo);
 
         // The window policy is responsible for stopping activities on the default display.
         final int displayId = mDisplay.getDisplayId();
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 708ee7f..b862d7c 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -1737,11 +1737,12 @@
     void onOverlayChanged() {
         updateCurrentUserResources();
         // Update the latest display size, cutout.
-        mDisplayContent.updateDisplayInfo();
-        onConfigurationChanged();
-        if (!CLIENT_TRANSIENT) {
-            mSystemGestures.onConfigurationChanged();
-        }
+        mDisplayContent.requestDisplayUpdate(() -> {
+            onConfigurationChanged();
+            if (!CLIENT_TRANSIENT) {
+                mSystemGestures.onConfigurationChanged();
+            }
+        });
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/DisplayUpdater.java b/services/core/java/com/android/server/wm/DisplayUpdater.java
new file mode 100644
index 0000000..e611177
--- /dev/null
+++ b/services/core/java/com/android/server/wm/DisplayUpdater.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2023 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 android.annotation.NonNull;
+import android.view.Surface;
+import android.window.DisplayAreaInfo;
+
+/**
+ * Interface for a helper class that manages updates of DisplayInfo coming from DisplayManager
+ */
+interface DisplayUpdater {
+    /**
+     * Reads the latest display parameters from the display manager and returns them in a callback.
+     * If there are pending display updates, it will wait for them to finish first and only then it
+     * will call the callback with the latest display parameters.
+     *
+     * @param callback is called when all pending display updates are finished
+     */
+    void updateDisplayInfo(@NonNull Runnable callback);
+
+    /**
+     * Called when physical display has changed and before DisplayContent has applied new display
+     * properties
+     */
+    default void onDisplayContentDisplayPropertiesPreChanged(int displayId, int initialDisplayWidth,
+            int initialDisplayHeight, int newWidth, int newHeight) {
+    }
+
+    /**
+     * Called after physical display has changed and after DisplayContent applied new display
+     * properties
+     */
+    default void onDisplayContentDisplayPropertiesPostChanged(
+            @Surface.Rotation int previousRotation, @Surface.Rotation int newRotation,
+            @NonNull DisplayAreaInfo newDisplayAreaInfo) {
+    }
+}
diff --git a/services/core/java/com/android/server/wm/ImmediateDisplayUpdater.java b/services/core/java/com/android/server/wm/ImmediateDisplayUpdater.java
new file mode 100644
index 0000000..72e8fcb
--- /dev/null
+++ b/services/core/java/com/android/server/wm/ImmediateDisplayUpdater.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2023 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 android.annotation.NonNull;
+import android.view.DisplayInfo;
+import android.window.DisplayAreaInfo;
+
+/**
+ * DisplayUpdater that immediately applies new DisplayInfo properties
+ */
+public class ImmediateDisplayUpdater implements DisplayUpdater {
+
+    private final DisplayContent mDisplayContent;
+    private final DisplayInfo mDisplayInfo = new DisplayInfo();
+
+    public ImmediateDisplayUpdater(@NonNull DisplayContent displayContent) {
+        mDisplayContent = displayContent;
+    }
+
+    @Override
+    public void updateDisplayInfo(Runnable callback) {
+        mDisplayContent.mWmService.mDisplayManagerInternal.getNonOverrideDisplayInfo(
+                mDisplayContent.mDisplayId, mDisplayInfo);
+        mDisplayContent.onDisplayInfoUpdated(mDisplayInfo);
+        callback.run();
+    }
+
+    @Override
+    public void onDisplayContentDisplayPropertiesPreChanged(int displayId, int initialDisplayWidth,
+            int initialDisplayHeight, int newWidth, int newHeight) {
+        mDisplayContent.mDisplaySwitchTransitionLauncher.requestDisplaySwitchTransitionIfNeeded(
+                displayId, initialDisplayWidth, initialDisplayHeight, newWidth, newHeight);
+    }
+
+    @Override
+    public void onDisplayContentDisplayPropertiesPostChanged(int previousRotation, int newRotation,
+            DisplayAreaInfo newDisplayAreaInfo) {
+        mDisplayContent.mDisplaySwitchTransitionLauncher.onDisplayUpdated(previousRotation,
+                newRotation,
+                newDisplayAreaInfo);
+    }
+}
diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java
index 0c235ba..d65d778 100644
--- a/services/core/java/com/android/server/wm/RootWindowContainer.java
+++ b/services/core/java/com/android/server/wm/RootWindowContainer.java
@@ -2716,15 +2716,20 @@
         synchronized (mService.mGlobalLock) {
             final DisplayContent displayContent = getDisplayContent(displayId);
             if (displayContent != null) {
-                displayContent.onDisplayChanged();
+                displayContent.requestDisplayUpdate(() -> clearDisplayInfoCaches(displayId));
+            } else {
+                clearDisplayInfoCaches(displayId);
             }
-            // Drop any cached DisplayInfos associated with this display id - the values are now
-            // out of date given this display changed event.
-            mWmService.mPossibleDisplayInfoMapper.removePossibleDisplayInfos(displayId);
-            updateDisplayImePolicyCache();
         }
     }
 
+    private void clearDisplayInfoCaches(int displayId) {
+        // Drop any cached DisplayInfos associated with this display id - the values are now
+        // out of date given this display changed event.
+        mWmService.mPossibleDisplayInfoMapper.removePossibleDisplayInfos(displayId);
+        updateDisplayImePolicyCache();
+    }
+
     void updateDisplayImePolicyCache() {
         ArrayMap<Integer, Integer> displayImePolicyMap = new ArrayMap<>();
         forAllDisplays(dc -> displayImePolicyMap.put(dc.getDisplayId(), dc.getImePolicy()));
diff --git a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
index 5c84cb0..e7bffdf 100644
--- a/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
+++ b/services/core/java/com/android/server/wm/ScreenRotationAnimation.java
@@ -32,7 +32,7 @@
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
 import static com.android.server.wm.utils.CoordinateTransforms.computeRotationMatrix;
-import static com.android.window.flags.Flags.removeCaptureDisplay;
+import static com.android.window.flags.Flags.deleteCaptureDisplay;
 
 import android.animation.ArgbEvaluator;
 import android.content.Context;
@@ -171,7 +171,7 @@
 
         try {
             final ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer;
-            if (isSizeChanged && !removeCaptureDisplay()) {
+            if (isSizeChanged && !deleteCaptureDisplay()) {
                 final DisplayAddress address = displayInfo.address;
                 if (!(address instanceof DisplayAddress.Physical)) {
                     Slog.e(TAG, "Display does not have a physical address: " + displayId);
diff --git a/services/core/java/com/android/server/wm/StartingSurfaceController.java b/services/core/java/com/android/server/wm/StartingSurfaceController.java
index 28a35b9..3032110 100644
--- a/services/core/java/com/android/server/wm/StartingSurfaceController.java
+++ b/services/core/java/com/android/server/wm/StartingSurfaceController.java
@@ -276,12 +276,14 @@
         /**
          * Removes the starting window surface. Do not hold the window manager lock when calling
          * this method!
+         *
          * @param animate Whether need to play the default exit animation for starting window.
+         * @param hasImeSurface Whether the starting window has IME surface.
          */
-        public void remove(boolean animate) {
+        public void remove(boolean animate, boolean hasImeSurface) {
             synchronized (mService.mGlobalLock) {
                 mService.mAtmService.mTaskOrganizerController.removeStartingWindow(mTask,
-                        mTaskOrganizer, animate);
+                        mTaskOrganizer, animate, hasImeSurface);
             }
         }
     }
diff --git a/services/core/java/com/android/server/wm/TaskOrganizerController.java b/services/core/java/com/android/server/wm/TaskOrganizerController.java
index 17ab00d6..3a711b2 100644
--- a/services/core/java/com/android/server/wm/TaskOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskOrganizerController.java
@@ -673,7 +673,8 @@
         return true;
     }
 
-    void removeStartingWindow(Task task, ITaskOrganizer taskOrganizer, boolean prepareAnimation) {
+    void removeStartingWindow(Task task, ITaskOrganizer taskOrganizer, boolean prepareAnimation,
+            boolean hasImeSurface) {
         final Task rootTask = task.getRootTask();
         if (rootTask == null) {
             return;
@@ -693,13 +694,13 @@
         if (topActivity != null) {
             // Set defer remove mode for IME
             final DisplayContent dc = topActivity.getDisplayContent();
-            final WindowState imeWindow = dc.mInputMethodWindow;
-            if (topActivity.isVisibleRequested() && imeWindow != null
-                    && dc.mayImeShowOnLaunchingActivity(topActivity)
-                    && dc.isFixedRotationLaunchingApp(topActivity)) {
-                removalInfo.deferRemoveForImeMode = DEFER_MODE_ROTATION;
-            } else if (dc.mayImeShowOnLaunchingActivity(topActivity)) {
-                removalInfo.deferRemoveForImeMode = DEFER_MODE_NORMAL;
+            if (hasImeSurface) {
+                if (topActivity.isVisibleRequested() && dc.mInputMethodWindow != null
+                        && dc.isFixedRotationLaunchingApp(topActivity)) {
+                    removalInfo.deferRemoveForImeMode = DEFER_MODE_ROTATION;
+                } else {
+                    removalInfo.deferRemoveForImeMode = DEFER_MODE_NORMAL;
+                }
             } else {
                 removalInfo.deferRemoveForImeMode = DEFER_MODE_NONE;
             }
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index a736874..bacfda5 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -992,11 +992,19 @@
     private void enforceSurfaceVisible(WindowContainer<?> wc) {
         if (wc.mSurfaceControl == null) return;
         wc.getSyncTransaction().show(wc.mSurfaceControl);
+        final ActivityRecord ar = wc.asActivityRecord();
+        if (ar != null) {
+            ar.mLastSurfaceShowing = true;
+        }
         // Force showing the parents because they may be hidden by previous transition.
         for (WindowContainer<?> p = wc.getParent(); p != null && p != wc.mDisplayContent;
                 p = p.getParent()) {
             if (p.mSurfaceControl != null) {
                 p.getSyncTransaction().show(p.mSurfaceControl);
+                final Task task = p.asTask();
+                if (task != null) {
+                    task.mLastSurfaceShowing = true;
+                }
             }
         }
         wc.scheduleAnimation();
diff --git a/services/core/java/com/android/server/wm/WindowAnimator.java b/services/core/java/com/android/server/wm/WindowAnimator.java
index fd22f15..750fd50 100644
--- a/services/core/java/com/android/server/wm/WindowAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowAnimator.java
@@ -196,7 +196,9 @@
             updateRunningExpensiveAnimationsLegacy();
         }
 
+        Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "applyTransaction");
         mTransaction.apply();
+        Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
         mService.mWindowTracing.logState("WindowAnimator");
         ProtoLog.i(WM_SHOW_TRANSACTIONS, "<<< CLOSE TRANSACTION animate");
 
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index a69a07f..575ae69b 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -6238,9 +6238,11 @@
             return;
         }
 
-        Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "WMS.doStartFreezingDisplay");
-        doStartFreezingDisplay(exitAnim, enterAnim, displayContent, overrideOriginalRotation);
-        Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
+        displayContent.requestDisplayUpdate(() -> {
+            Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "WMS.doStartFreezingDisplay");
+            doStartFreezingDisplay(exitAnim, enterAnim, displayContent, overrideOriginalRotation);
+            Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
+        });
     }
 
     private void doStartFreezingDisplay(int exitAnim, int enterAnim, DisplayContent displayContent,
@@ -6276,7 +6278,6 @@
         mExitAnimId = exitAnim;
         mEnterAnimId = enterAnim;
 
-        displayContent.updateDisplayInfo();
         final int originalRotation = overrideOriginalRotation != ROTATION_UNDEFINED
                 ? overrideOriginalRotation
                 : displayContent.getDisplayInfo().rotation;
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 3e43908..6d6bcc8 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -3276,7 +3276,9 @@
             // just kill it. And if it is a window of foreground activity, the activity can be
             // restarted automatically if needed.
             Slog.w(TAG, "Exception thrown during dispatchAppVisibility " + this, e);
-            android.os.Process.killProcess(mSession.mPid);
+            if (android.os.Process.getUidForPid(mSession.mPid) == mSession.mUid) {
+                android.os.Process.killProcess(mSession.mPid);
+            }
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/utils/DisplayInfoOverrides.java b/services/core/java/com/android/server/wm/utils/DisplayInfoOverrides.java
new file mode 100644
index 0000000..8c8f6a6
--- /dev/null
+++ b/services/core/java/com/android/server/wm/utils/DisplayInfoOverrides.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2023 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.utils;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.view.DisplayInfo;
+
+/**
+ * Helper class to copy only subset of fields of DisplayInfo object or to perform
+ * comparison operation between DisplayInfo objects only with a subset of fields.
+ */
+public class DisplayInfoOverrides {
+
+    /**
+     * Set of DisplayInfo fields that are overridden in DisplayManager using values from
+     * WindowManager
+     */
+    public static final DisplayInfoFields WM_OVERRIDE_FIELDS = (out, source) -> {
+        out.appWidth = source.appWidth;
+        out.appHeight = source.appHeight;
+        out.smallestNominalAppWidth = source.smallestNominalAppWidth;
+        out.smallestNominalAppHeight = source.smallestNominalAppHeight;
+        out.largestNominalAppWidth = source.largestNominalAppWidth;
+        out.largestNominalAppHeight = source.largestNominalAppHeight;
+        out.logicalWidth = source.logicalWidth;
+        out.logicalHeight = source.logicalHeight;
+        out.physicalXDpi = source.physicalXDpi;
+        out.physicalYDpi = source.physicalYDpi;
+        out.rotation = source.rotation;
+        out.displayCutout = source.displayCutout;
+        out.logicalDensityDpi = source.logicalDensityDpi;
+        out.roundedCorners = source.roundedCorners;
+        out.displayShape = source.displayShape;
+    };
+
+    /**
+     * Gets {@param base} DisplayInfo, overrides WindowManager-specific overrides using
+     * {@param override} and writes the result to {@param out}
+     */
+    public static void copyDisplayInfoFields(@NonNull DisplayInfo out,
+            @NonNull DisplayInfo base,
+            @Nullable DisplayInfo override,
+            @NonNull DisplayInfoFields fields) {
+        out.copyFrom(base);
+
+        if (override != null) {
+            fields.setFields(out, override);
+        }
+    }
+
+    /**
+     * Callback interface that allows to specify a subset of fields of DisplayInfo object
+     */
+    public interface DisplayInfoFields {
+        /**
+         * Copies a subset of fields from {@param source} to {@param out}
+         *
+         * @param out    resulting DisplayInfo object
+         * @param source source DisplayInfo to copy fields from
+         */
+        void setFields(@NonNull DisplayInfo out, @NonNull DisplayInfo source);
+    }
+}
diff --git a/services/foldables/devicestateprovider/Android.bp b/services/foldables/devicestateprovider/Android.bp
index 34737ef..56daea7 100644
--- a/services/foldables/devicestateprovider/Android.bp
+++ b/services/foldables/devicestateprovider/Android.bp
@@ -5,9 +5,12 @@
 java_library {
     name: "foldable-device-state-provider",
     srcs: [
-        "src/**/*.java"
+        "src/**/*.java",
     ],
     libs: [
         "services",
     ],
+    static_libs: [
+        "device_state_flags_lib",
+    ],
 }
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java b/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
index aea46d1..4c487a7 100644
--- a/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/FoldableDeviceStateProvider.java
@@ -21,6 +21,7 @@
 import static android.hardware.devicestate.DeviceStateManager.MAXIMUM_DEVICE_STATE;
 import static android.hardware.devicestate.DeviceStateManager.MINIMUM_DEVICE_STATE;
 import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Display.TYPE_EXTERNAL;
 
 import android.annotation.IntRange;
 import android.annotation.NonNull;
@@ -33,11 +34,14 @@
 import android.hardware.SensorEvent;
 import android.hardware.SensorEventListener;
 import android.hardware.SensorManager;
-import android.os.PowerManager;
 import android.hardware.display.DisplayManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.PowerManager;
 import android.os.Trace;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.util.SparseBooleanArray;
 import android.view.Display;
 
 import com.android.internal.annotations.GuardedBy;
@@ -45,24 +49,26 @@
 import com.android.internal.util.Preconditions;
 import com.android.server.devicestate.DeviceState;
 import com.android.server.devicestate.DeviceStateProvider;
+import com.android.server.policy.feature.flags.FeatureFlags;
+import com.android.server.policy.feature.flags.FeatureFlagsImpl;
 
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Comparator;
 import java.util.List;
 import java.util.function.BooleanSupplier;
-import java.util.function.Function;
+import java.util.function.Predicate;
 
 /**
  * Device state provider for foldable devices.
- *
+ * <p>
  * It is an implementation of {@link DeviceStateProvider} tailored specifically for
  * foldable devices and allows simple callback-based configuration with hall sensor
  * and hinge angle sensor values.
  */
 public final class FoldableDeviceStateProvider implements DeviceStateProvider,
         SensorEventListener, PowerManager.OnThermalStatusChangedListener,
-       DisplayManager.DisplayListener  {
+        DisplayManager.DisplayListener {
 
     private static final String TAG = "FoldableDeviceStateProvider";
     private static final boolean DEBUG = false;
@@ -77,9 +83,17 @@
     // are met for the device to be in the state.
     private final SparseArray<BooleanSupplier> mStateConditions = new SparseArray<>();
 
+    // Map of state identifier to a boolean supplier that returns true when the device state has all
+    // the conditions needed for availability.
+    private final SparseArray<BooleanSupplier> mStateAvailabilityConditions = new SparseArray<>();
+
+    @GuardedBy("mLock")
+    private final SparseBooleanArray mExternalDisplaysConnected = new SparseBooleanArray();
+
     private final Sensor mHingeAngleSensor;
     private final DisplayManager mDisplayManager;
     private final Sensor mHallSensor;
+    private static final Predicate<FoldableDeviceStateProvider> ALLOWED = p -> true;
 
     @Nullable
     @GuardedBy("mLock")
@@ -99,7 +113,23 @@
     @GuardedBy("mLock")
     private boolean mPowerSaveModeEnabled;
 
-    public FoldableDeviceStateProvider(@NonNull Context context,
+    private final boolean mIsDualDisplayBlockingEnabled;
+
+    public FoldableDeviceStateProvider(
+            @NonNull Context context,
+            @NonNull SensorManager sensorManager,
+            @NonNull Sensor hingeAngleSensor,
+            @NonNull Sensor hallSensor,
+            @NonNull DisplayManager displayManager,
+            @NonNull DeviceStateConfiguration[] deviceStateConfigurations) {
+        this(new FeatureFlagsImpl(), context, sensorManager, hingeAngleSensor, hallSensor,
+                displayManager, deviceStateConfigurations);
+    }
+
+    @VisibleForTesting
+    public FoldableDeviceStateProvider(
+            @NonNull FeatureFlags featureFlags,
+            @NonNull Context context,
             @NonNull SensorManager sensorManager,
             @NonNull Sensor hingeAngleSensor,
             @NonNull Sensor hallSensor,
@@ -112,6 +142,7 @@
         mHingeAngleSensor = hingeAngleSensor;
         mHallSensor = hallSensor;
         mDisplayManager = displayManager;
+        mIsDualDisplayBlockingEnabled = featureFlags.enableDualDisplayBlocking();
 
         sensorManager.registerListener(this, mHingeAngleSensor, SENSOR_DELAY_FASTEST);
         sensorManager.registerListener(this, mHallSensor, SENSOR_DELAY_FASTEST);
@@ -121,20 +152,15 @@
             final DeviceStateConfiguration configuration = deviceStateConfigurations[i];
             mOrderedStates[i] = configuration.mDeviceState;
 
-            if (mStateConditions.get(configuration.mDeviceState.getIdentifier()) != null) {
-                throw new IllegalArgumentException("Device state configurations must have unique"
-                        + " device state identifiers, found duplicated identifier: " +
-                        configuration.mDeviceState.getIdentifier());
-            }
-
-            mStateConditions.put(configuration.mDeviceState.getIdentifier(), () ->
-                    configuration.mPredicate.apply(this));
+            assertUniqueDeviceStateIdentifier(configuration);
+            initialiseStateConditions(configuration);
+            initialiseStateAvailabilityConditions(configuration);
         }
 
+        Handler handler = new Handler(Looper.getMainLooper());
         mDisplayManager.registerDisplayListener(
                 /* listener = */ this,
-                /* handler= */ null,
-                /* eventsMask= */ DisplayManager.EVENT_FLAG_DISPLAY_CHANGED);
+                /* handler= */ handler);
 
         Arrays.sort(mOrderedStates, Comparator.comparingInt(DeviceState::getIdentifier));
 
@@ -167,6 +193,24 @@
         }
     }
 
+    private void assertUniqueDeviceStateIdentifier(DeviceStateConfiguration configuration) {
+        if (mStateConditions.get(configuration.mDeviceState.getIdentifier()) != null) {
+            throw new IllegalArgumentException("Device state configurations must have unique"
+                    + " device state identifiers, found duplicated identifier: "
+                    + configuration.mDeviceState.getIdentifier());
+        }
+    }
+
+    private void initialiseStateConditions(DeviceStateConfiguration configuration) {
+        mStateConditions.put(configuration.mDeviceState.getIdentifier(), () ->
+                configuration.mActiveStatePredicate.test(this));
+    }
+
+    private void initialiseStateAvailabilityConditions(DeviceStateConfiguration configuration) {
+            mStateAvailabilityConditions.put(configuration.mDeviceState.getIdentifier(), () ->
+                    configuration.mAvailabilityPredicate.test(this));
+    }
+
     @Override
     public void setListener(Listener listener) {
         synchronized (mLock) {
@@ -189,16 +233,9 @@
             }
             listener = mListener;
             for (DeviceState deviceState : mOrderedStates) {
-                if (isThermalStatusCriticalOrAbove(mThermalStatus)
-                        && deviceState.hasFlag(
-                        DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL)) {
-                    continue;
+                if (isStateSupported(deviceState)) {
+                    supportedStates.add(deviceState);
                 }
-                if (mPowerSaveModeEnabled && deviceState.hasFlag(
-                        DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE)) {
-                    continue;
-                }
-                supportedStates.add(deviceState);
             }
         }
 
@@ -206,6 +243,26 @@
                 supportedStates.toArray(new DeviceState[supportedStates.size()]), reason);
     }
 
+    @GuardedBy("mLock")
+    private boolean isStateSupported(DeviceState deviceState) {
+        if (isThermalStatusCriticalOrAbove(mThermalStatus)
+                && deviceState.hasFlag(
+                DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL)) {
+            return false;
+        }
+        if (mPowerSaveModeEnabled && deviceState.hasFlag(
+                DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE)) {
+            return false;
+        }
+        if (mIsDualDisplayBlockingEnabled
+                && mStateAvailabilityConditions.contains(deviceState.getIdentifier())) {
+            return mStateAvailabilityConditions
+                    .get(deviceState.getIdentifier())
+                    .getAsBoolean();
+        }
+        return true;
+    }
+
     /** Computes the current device state and notifies the listener of a change, if needed. */
     void notifyDeviceStateChangedIfNeeded() {
         int stateToReport = INVALID_DEVICE_STATE;
@@ -294,7 +351,7 @@
     private void dumpSensorValues() {
         Slog.i(TAG, "Sensor values:");
         dumpSensorValues("Hall Sensor", mHallSensor, mLastHallSensorEvent);
-        dumpSensorValues("Hinge Angle Sensor",mHingeAngleSensor, mLastHingeAngleSensorEvent);
+        dumpSensorValues("Hinge Angle Sensor", mHingeAngleSensor, mLastHingeAngleSensorEvent);
         Slog.i(TAG, "isScreenOn: " + isScreenOn());
     }
 
@@ -307,12 +364,35 @@
 
     @Override
     public void onDisplayAdded(int displayId) {
+        // TODO(b/312397262): consider virtual displays cases
+        synchronized (mLock) {
+            if (mIsDualDisplayBlockingEnabled
+                    && !mExternalDisplaysConnected.get(displayId, false)
+                    && mDisplayManager.getDisplay(displayId).getType() == TYPE_EXTERNAL) {
+                mExternalDisplaysConnected.put(displayId, true);
 
+                // Only update the supported state when going from 0 external display to 1
+                if (mExternalDisplaysConnected.size() == 1) {
+                    notifySupportedStatesChanged(
+                            SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_ADDED);
+                }
+            }
+        }
     }
 
     @Override
     public void onDisplayRemoved(int displayId) {
+        synchronized (mLock) {
+            if (mIsDualDisplayBlockingEnabled && mExternalDisplaysConnected.get(displayId, false)) {
+                mExternalDisplaysConnected.delete(displayId);
 
+                // Only update the supported states when going from 1 external display to 0
+                if (mExternalDisplaysConnected.size() == 0) {
+                    notifySupportedStatesChanged(
+                            SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_REMOVED);
+                }
+            }
+        }
     }
 
     @Override
@@ -338,48 +418,71 @@
      */
     public static class DeviceStateConfiguration {
         private final DeviceState mDeviceState;
-        private final Function<FoldableDeviceStateProvider, Boolean> mPredicate;
+        private final Predicate<FoldableDeviceStateProvider> mActiveStatePredicate;
+        private final Predicate<FoldableDeviceStateProvider> mAvailabilityPredicate;
 
-        private DeviceStateConfiguration(DeviceState deviceState,
-                Function<FoldableDeviceStateProvider, Boolean> predicate) {
+        private DeviceStateConfiguration(
+                @NonNull DeviceState deviceState,
+                @NonNull Predicate<FoldableDeviceStateProvider> predicate) {
+            this(deviceState, predicate, ALLOWED);
+        }
+
+        private DeviceStateConfiguration(
+                @NonNull DeviceState deviceState,
+                @NonNull Predicate<FoldableDeviceStateProvider> activeStatePredicate,
+                @NonNull Predicate<FoldableDeviceStateProvider> availabilityPredicate) {
+
             mDeviceState = deviceState;
-            mPredicate = predicate;
+            mActiveStatePredicate = activeStatePredicate;
+            mAvailabilityPredicate = availabilityPredicate;
         }
 
         public static DeviceStateConfiguration createConfig(
                 @IntRange(from = MINIMUM_DEVICE_STATE, to = MAXIMUM_DEVICE_STATE) int identifier,
                 @NonNull String name,
                 @DeviceState.DeviceStateFlags int flags,
-                Function<FoldableDeviceStateProvider, Boolean> predicate
+                @NonNull Predicate<FoldableDeviceStateProvider> activeStatePredicate
         ) {
             return new DeviceStateConfiguration(new DeviceState(identifier, name, flags),
-                    predicate);
+                    activeStatePredicate);
         }
 
         public static DeviceStateConfiguration createConfig(
                 @IntRange(from = MINIMUM_DEVICE_STATE, to = MAXIMUM_DEVICE_STATE) int identifier,
                 @NonNull String name,
-                Function<FoldableDeviceStateProvider, Boolean> predicate
+                @NonNull Predicate<FoldableDeviceStateProvider> activeStatePredicate
         ) {
             return new DeviceStateConfiguration(new DeviceState(identifier, name, /* flags= */ 0),
-                    predicate);
+                    activeStatePredicate);
+        }
+
+        /** Create a configuration with availability predicate **/
+        public static DeviceStateConfiguration createConfig(
+                @IntRange(from = MINIMUM_DEVICE_STATE, to = MAXIMUM_DEVICE_STATE) int identifier,
+                @NonNull String name,
+                @DeviceState.DeviceStateFlags int flags,
+                @NonNull Predicate<FoldableDeviceStateProvider> activeStatePredicate,
+                @NonNull Predicate<FoldableDeviceStateProvider> availabilityPredicate
+        ) {
+            return new DeviceStateConfiguration(new DeviceState(identifier, name, flags),
+                    activeStatePredicate, availabilityPredicate);
         }
 
         /**
          * Creates a device state configuration for a closed tent-mode aware state.
-         *
+         * <p>
          * During tent mode:
          * - The inner display is OFF
          * - The outer display is ON
          * - The device is partially unfolded (left and right edges could be on the table)
          * In this mode the device the device so it could be used in a posture where both left
          * and right edges of the unfolded device are on the table.
-         *
+         * <p>
          * The predicate returns false after the hinge angle reaches
          * {@code tentModeSwitchAngleDegrees}. Then it switches back only when the hinge angle
          * becomes less than {@code maxClosedAngleDegrees}. Hinge angle is 0 degrees when the device
          * is fully closed and 180 degrees when it is fully unfolded.
-         *
+         * <p>
          * For example, when tentModeSwitchAngleDegrees = 90 and maxClosedAngleDegrees = 5 degrees:
          *  - when unfolding the device from fully closed posture (last state == closed or it is
          *    undefined yet) this state will become not matching after reaching the angle
@@ -435,6 +538,15 @@
     }
 
     /**
+     * @return Whether there is an external connected display.
+     */
+    public boolean hasNoConnectedExternalDisplay() {
+        synchronized (mLock) {
+            return mExternalDisplaysConnected.size() == 0;
+        }
+    }
+
+    /**
      * @return Whether the screen is on.
      */
     public boolean isScreenOn() {
@@ -442,6 +554,7 @@
             return mIsScreenOn;
         }
     }
+
     /**
      * @return current hinge angle value of a foldable device
      */
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/TentModeDeviceStatePolicy.java b/services/foldables/devicestateprovider/src/com/android/server/policy/TentModeDeviceStatePolicy.java
index 5f2cf3c..5968b63 100644
--- a/services/foldables/devicestateprovider/src/com/android/server/policy/TentModeDeviceStatePolicy.java
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/TentModeDeviceStatePolicy.java
@@ -33,6 +33,10 @@
 import com.android.server.devicestate.DeviceStatePolicy;
 import com.android.server.devicestate.DeviceStateProvider;
 import com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration;
+import com.android.server.policy.feature.flags.FeatureFlags;
+import com.android.server.policy.feature.flags.FeatureFlagsImpl;
+
+import java.util.function.Predicate;
 
 /**
  * Device state policy for a foldable device that supports tent mode: a mode when the device
@@ -55,6 +59,10 @@
 
     private final DeviceStateProvider mProvider;
 
+    private final boolean mIsDualDisplayBlockingEnabled;
+    private static final Predicate<FoldableDeviceStateProvider> ALLOWED = p -> true;
+    private static final Predicate<FoldableDeviceStateProvider> NOT_ALLOWED = p -> false;
+
     /**
      * Creates TentModeDeviceStatePolicy
      *
@@ -67,6 +75,12 @@
      */
     public TentModeDeviceStatePolicy(@NonNull Context context,
             @NonNull Sensor hingeAngleSensor, @NonNull Sensor hallSensor, int closeAngleDegrees) {
+        this(new FeatureFlagsImpl(), context, hingeAngleSensor, hallSensor, closeAngleDegrees);
+    }
+
+    public TentModeDeviceStatePolicy(@NonNull FeatureFlags featureFlags, @NonNull Context context,
+                                     @NonNull Sensor hingeAngleSensor, @NonNull Sensor hallSensor,
+                                     int closeAngleDegrees) {
         super(context);
 
         final SensorManager sensorManager = mContext.getSystemService(SensorManager.class);
@@ -74,8 +88,10 @@
 
         final DeviceStateConfiguration[] configuration = createConfiguration(closeAngleDegrees);
 
-        mProvider = new FoldableDeviceStateProvider(mContext, sensorManager, hingeAngleSensor,
-                hallSensor, displayManager, configuration);
+        mIsDualDisplayBlockingEnabled = featureFlags.enableDualDisplayBlocking();
+
+        mProvider = new FoldableDeviceStateProvider(mContext, sensorManager,
+                hingeAngleSensor, hallSensor, displayManager, configuration);
     }
 
     private DeviceStateConfiguration[] createConfiguration(int closeAngleDegrees) {
@@ -83,24 +99,27 @@
                 createClosedConfiguration(closeAngleDegrees),
                 createConfig(DEVICE_STATE_HALF_OPENED,
                         /* name= */ "HALF_OPENED",
-                        (provider) -> {
+                        /* activeStatePredicate= */ (provider) -> {
                             final float hingeAngle = provider.getHingeAngle();
                             return hingeAngle >= MAX_CLOSED_ANGLE_DEGREES
                                     && hingeAngle <= TABLE_TOP_MODE_SWITCH_ANGLE_DEGREES;
                         }),
                 createConfig(DEVICE_STATE_OPENED,
                         /* name= */ "OPENED",
-                        (provider) -> true),
+                        /* activeStatePredicate= */ ALLOWED),
                 createConfig(DEVICE_STATE_REAR_DISPLAY_STATE,
                         /* name= */ "REAR_DISPLAY_STATE",
                         /* flags= */ FLAG_EMULATED_ONLY,
-                        (provider) -> false),
+                        /* activeStatePredicate= */ NOT_ALLOWED),
                 createConfig(DEVICE_STATE_CONCURRENT_INNER_DEFAULT,
                         /* name= */ "CONCURRENT_INNER_DEFAULT",
                         /* flags= */ FLAG_EMULATED_ONLY | FLAG_CANCEL_WHEN_REQUESTER_NOT_ON_TOP
                                 | FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL
                                 | FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE,
-                        (provider) -> false)
+                        /* activeStatePredicate= */ NOT_ALLOWED,
+                        /* availabilityPredicate= */
+                        provider -> !mIsDualDisplayBlockingEnabled
+                                || provider.hasNoConnectedExternalDisplay())
         };
     }
 
@@ -111,7 +130,7 @@
                     DEVICE_STATE_CLOSED,
                     /* name= */ "CLOSED",
                     /* flags= */ FLAG_CANCEL_OVERRIDE_REQUESTS,
-                    (provider) -> {
+                    /* activeStatePredicate= */ (provider) -> {
                         final float hingeAngle = provider.getHingeAngle();
                         return hingeAngle <= closeAngleDegrees;
                     }
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/feature/Android.bp b/services/foldables/devicestateprovider/src/com/android/server/policy/feature/Android.bp
new file mode 100644
index 0000000..6ad8d79
--- /dev/null
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/feature/Android.bp
@@ -0,0 +1,12 @@
+aconfig_declarations {
+    name: "device_state_flags",
+    package: "com.android.server.policy.feature.flags",
+    srcs: [
+        "device_state_flags.aconfig",
+    ],
+}
+
+java_aconfig_library {
+    name: "device_state_flags_lib",
+    aconfig_declarations: "device_state_flags",
+}
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/feature/device_state_flags.aconfig b/services/foldables/devicestateprovider/src/com/android/server/policy/feature/device_state_flags.aconfig
new file mode 100644
index 0000000..47c2a1b
--- /dev/null
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/feature/device_state_flags.aconfig
@@ -0,0 +1,8 @@
+package: "com.android.server.policy.feature.flags"
+
+flag {
+    name: "enable_dual_display_blocking"
+    namespace: "display_manager"
+    description: "Feature flag for dual display blocking"
+    bug: "278667199"
+}
\ No newline at end of file
diff --git a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java
index 8fa4ce5..ddf4a08 100644
--- a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java
+++ b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/FoldableDeviceStateProviderTest.java
@@ -17,18 +17,21 @@
 package com.android.server.policy;
 
 
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Display.STATE_OFF;
+import static android.view.Display.STATE_ON;
+import static android.view.Display.TYPE_EXTERNAL;
+import static android.view.Display.TYPE_INTERNAL;
+
+import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_ADDED;
+import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_REMOVED;
 import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED;
 import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_DISABLED;
 import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_POWER_SAVE_ENABLED;
 import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_CRITICAL;
 import static com.android.server.devicestate.DeviceStateProvider.SUPPORTED_DEVICE_STATES_CHANGED_THERMAL_NORMAL;
-import com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration;
-
-import static android.view.Display.DEFAULT_DISPLAY;
-import static android.view.Display.STATE_OFF;
-import static android.view.Display.STATE_ON;
-
 import static com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration.createConfig;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertArrayEquals;
@@ -36,12 +39,11 @@
 import static org.junit.Assert.assertThrows;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.anyLong;
-import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.nullable;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -51,20 +53,21 @@
 import android.hardware.SensorManager;
 import android.hardware.display.DisplayManager;
 import android.hardware.input.InputSensorInfo;
-import android.os.PowerManager;
 import android.os.Handler;
+import android.os.PowerManager;
 import android.testing.AndroidTestingRunner;
 import android.view.Display;
 
 import com.android.server.devicestate.DeviceState;
-import com.android.server.devicestate.DeviceStateProvider;
 import com.android.server.devicestate.DeviceStateProvider.Listener;
+import com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration;
+import com.android.server.policy.feature.flags.FakeFeatureFlagsImpl;
+import com.android.server.policy.feature.flags.Flags;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
-import org.mockito.ArgumentMatchers;
 import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
@@ -95,10 +98,16 @@
     @Mock
     private DisplayManager mDisplayManager;
     private FoldableDeviceStateProvider mProvider;
+    @Mock
+    private Display mDefaultDisplay;
+    @Mock
+    private Display mExternalDisplay;
 
+    private final FakeFeatureFlagsImpl mFakeFeatureFlags = new FakeFeatureFlagsImpl();
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
+        mFakeFeatureFlags.setFlag(Flags.FLAG_ENABLE_DUAL_DISPLAY_BLOCKING, true);
 
         mHallSensor = new Sensor(mInputSensorInfo);
         mHingeAngleSensor = new Sensor(mInputSensorInfo);
@@ -473,6 +482,133 @@
         assertThat(mProvider.isScreenOn()).isFalse();
     }
 
+    @Test
+    public void test_dualScreenDisabledWhenExternalScreenIsConnected() throws Exception {
+        when(mDisplayManager.getDisplays()).thenReturn(new Display[]{mDefaultDisplay});
+        when(mDefaultDisplay.getType()).thenReturn(TYPE_INTERNAL);
+
+        createProvider(createConfig(/* identifier= */ 1, /* name= */ "CLOSED",
+                        (c) -> c.getHingeAngle() < 5f),
+                createConfig(/* identifier= */ 2, /* name= */ "HALF_OPENED",
+                        (c) -> c.getHingeAngle() < 90f),
+                createConfig(/* identifier= */ 3, /* name= */ "OPENED",
+                        (c) -> c.getHingeAngle() < 180f),
+                createConfig(/* identifier= */ 4, /* name= */ "DUAL_DISPLAY", /* flags */ 0,
+                        (c) -> false, FoldableDeviceStateProvider::hasNoConnectedExternalDisplay));
+
+        Listener listener = mock(Listener.class);
+        mProvider.setListener(listener);
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
+        assertThat(mDeviceStateArrayCaptor.getValue()).asList().containsExactly(
+                new DeviceState[]{
+                        new DeviceState(1, "CLOSED", 0 /* flags */),
+                        new DeviceState(2, "HALF_OPENED", 0 /* flags */),
+                        new DeviceState(3, "OPENED", 0 /* flags */),
+                        new DeviceState(4, "DUAL_DISPLAY", 0 /* flags */)}).inOrder();
+
+        clearInvocations(listener);
+
+        when(mDisplayManager.getDisplays())
+                .thenReturn(new Display[]{mDefaultDisplay, mExternalDisplay});
+        when(mDisplayManager.getDisplay(1)).thenReturn(mExternalDisplay);
+        when(mExternalDisplay.getType()).thenReturn(TYPE_EXTERNAL);
+
+        // The DUAL_DISPLAY state should be disabled.
+        mProvider.onDisplayAdded(1);
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_ADDED));
+        assertThat(mDeviceStateArrayCaptor.getValue()).asList().containsExactly(
+                new DeviceState[]{
+                        new DeviceState(1, "CLOSED", 0 /* flags */),
+                        new DeviceState(2, "HALF_OPENED", 0 /* flags */),
+                        new DeviceState(3, "OPENED", 0 /* flags */)}).inOrder();
+        clearInvocations(listener);
+
+        // The DUAL_DISPLAY state should be re-enabled.
+        when(mDisplayManager.getDisplays()).thenReturn(new Display[]{mDefaultDisplay});
+        mProvider.onDisplayRemoved(1);
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_REMOVED));
+        assertThat(mDeviceStateArrayCaptor.getValue()).asList().containsExactly(
+                new DeviceState[]{
+                        new DeviceState(1, "CLOSED", 0 /* flags */),
+                        new DeviceState(2, "HALF_OPENED", 0 /* flags */),
+                        new DeviceState(3, "OPENED", 0 /* flags */),
+                        new DeviceState(4, "DUAL_DISPLAY", 0 /* flags */)}).inOrder();
+    }
+
+    @Test
+    public void test_notifySupportedStatesChangedCalledOnlyOnInitialExternalScreenAddition() {
+        when(mDisplayManager.getDisplays()).thenReturn(new Display[]{mDefaultDisplay});
+        when(mDefaultDisplay.getType()).thenReturn(TYPE_INTERNAL);
+
+        createProvider(createConfig(/* identifier= */ 1, /* name= */ "CLOSED",
+                        (c) -> c.getHingeAngle() < 5f),
+                createConfig(/* identifier= */ 2, /* name= */ "HALF_OPENED",
+                        (c) -> c.getHingeAngle() < 90f),
+                createConfig(/* identifier= */ 3, /* name= */ "OPENED",
+                        (c) -> c.getHingeAngle() < 180f),
+                createConfig(/* identifier= */ 4, /* name= */ "DUAL_DISPLAY", /* flags */ 0,
+                        (c) -> false, FoldableDeviceStateProvider::hasNoConnectedExternalDisplay));
+
+        Listener listener = mock(Listener.class);
+        mProvider.setListener(listener);
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_INITIALIZED));
+        assertThat(mDeviceStateArrayCaptor.getValue()).asList().containsExactly(
+                new DeviceState[]{
+                        new DeviceState(1, "CLOSED", 0 /* flags */),
+                        new DeviceState(2, "HALF_OPENED", 0 /* flags */),
+                        new DeviceState(3, "OPENED", 0 /* flags */),
+                        new DeviceState(4, "DUAL_DISPLAY", 0 /* flags */)}).inOrder();
+
+        clearInvocations(listener);
+
+        addExternalDisplay(1);
+        verify(listener).onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_ADDED));
+        addExternalDisplay(2);
+        addExternalDisplay(3);
+        addExternalDisplay(4);
+        verify(listener, times(1))
+                .onSupportedDeviceStatesChanged(mDeviceStateArrayCaptor.capture(),
+                eq(SUPPORTED_DEVICE_STATES_CHANGED_EXTERNAL_DISPLAY_ADDED));
+    }
+
+    @Test
+    public void hasNoConnectedDisplay_afterExternalDisplayAdded_returnsFalse() {
+        createProvider(
+                createConfig(
+                        /* identifier= */ 1, /* name= */ "ONE",
+                        /* flags= */0, (c) -> true,
+                        FoldableDeviceStateProvider::hasNoConnectedExternalDisplay)
+        );
+
+        addExternalDisplay(/* displayId */ 1);
+
+        assertThat(mProvider.hasNoConnectedExternalDisplay()).isFalse();
+    }
+
+    @Test
+    public void hasNoConnectedDisplay_afterExternalDisplayAddedAndRemoved_returnsTrue() {
+        createProvider(
+                createConfig(
+                        /* identifier= */ 1, /* name= */ "ONE",
+                        /* flags= */0, (c) -> true,
+                        FoldableDeviceStateProvider::hasNoConnectedExternalDisplay)
+        );
+
+        addExternalDisplay(/* displayId */ 1);
+        mProvider.onDisplayRemoved(1);
+
+        assertThat(mProvider.hasNoConnectedExternalDisplay()).isTrue();
+    }
+    private void addExternalDisplay(int displayId) {
+        when(mDisplayManager.getDisplay(displayId)).thenReturn(mExternalDisplay);
+        when(mExternalDisplay.getType()).thenReturn(TYPE_EXTERNAL);
+        mProvider.onDisplayAdded(displayId);
+    }
     private void setScreenOn(boolean isOn) {
         Display mockDisplay = mock(Display.class);
         int state = isOn ? STATE_ON : STATE_OFF;
@@ -508,12 +644,11 @@
     }
 
     private void createProvider(DeviceStateConfiguration... configurations) {
-        mProvider = new FoldableDeviceStateProvider(mContext, mSensorManager, mHingeAngleSensor,
-                mHallSensor, mDisplayManager, configurations);
+        mProvider = new FoldableDeviceStateProvider(mFakeFeatureFlags, mContext, mSensorManager,
+                mHingeAngleSensor, mHallSensor, mDisplayManager, configurations);
         verify(mDisplayManager)
                 .registerDisplayListener(
                         mDisplayListenerCaptor.capture(),
-                        nullable(Handler.class),
-                        anyLong());
+                        nullable(Handler.class));
     }
 }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/BaseModeRefreshRateVoteTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/BaseModeRefreshRateVoteTest.kt
new file mode 100644
index 0000000..3f72364
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/BaseModeRefreshRateVoteTest.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2023 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.display.mode
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.server.display.mode.DisplayModeDirector.VoteSummary
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+
+private const val BASE_REFRESH_RATE = 60f
+private const val OTHER_BASE_REFRESH_RATE = 90f
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class BaseModeRefreshRateVoteTest {
+
+    private lateinit var baseModeVote: BaseModeRefreshRateVote
+
+    @Before
+    fun setUp() {
+        baseModeVote = BaseModeRefreshRateVote(BASE_REFRESH_RATE)
+    }
+
+    @Test
+    fun `updates summary with base mode refresh rate if not set`() {
+        val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+
+        baseModeVote.updateSummary(summary)
+
+        assertThat(summary.appRequestBaseModeRefreshRate).isEqualTo(BASE_REFRESH_RATE)
+    }
+
+    @Test
+    fun `keeps summary base mode refresh rate if set`() {
+        val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+        summary.appRequestBaseModeRefreshRate = OTHER_BASE_REFRESH_RATE
+
+        baseModeVote.updateSummary(summary)
+
+        assertThat(summary.appRequestBaseModeRefreshRate).isEqualTo(OTHER_BASE_REFRESH_RATE)
+    }
+
+    @Test
+    fun `keeps summary with base mode refresh rate if vote refresh rate is negative`() {
+        val invalidBaseModeVote = BaseModeRefreshRateVote(-10f)
+        val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+
+        invalidBaseModeVote.updateSummary(summary)
+
+        assertThat(summary.appRequestBaseModeRefreshRate).isZero()
+    }
+}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/CombinedVoteTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/CombinedVoteTest.kt
new file mode 100644
index 0000000..7f8da88
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/CombinedVoteTest.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2023 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.display.mode
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.server.display.mode.DisplayModeDirector.VoteSummary
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.junit.MockitoJUnit
+
+
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class CombinedVoteTest {
+    private lateinit var combinedVote: CombinedVote
+
+    @get:Rule
+    val mockitoRule = MockitoJUnit.rule()
+
+    private val mockVote1 = mock<Vote>()
+    private val mockVote2 = mock<Vote>()
+
+    @Before
+    fun setUp() {
+        combinedVote = CombinedVote(listOf(mockVote1, mockVote2))
+    }
+
+    @Test
+    fun `delegates update to children`() {
+        val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+
+        combinedVote.updateSummary(summary)
+
+        verify(mockVote1).updateSummary(summary)
+        verify(mockVote2).updateSummary(summary)
+    }
+}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/DisableRefreshRateSwitchingVoteTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/DisableRefreshRateSwitchingVoteTest.kt
new file mode 100644
index 0000000..c624325
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/DisableRefreshRateSwitchingVoteTest.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2023 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.display.mode
+
+import androidx.test.filters.SmallTest
+import com.android.server.display.mode.DisplayModeDirector.VoteSummary
+import com.google.common.truth.Truth.assertThat
+import com.google.testing.junit.testparameterinjector.TestParameter
+import com.google.testing.junit.testparameterinjector.TestParameterInjector
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(TestParameterInjector::class)
+class DisableRefreshRateSwitchingVoteTest {
+
+    @Test
+    fun `disabled refresh rate switching is not changed`(
+            @TestParameter voteDisableSwitching: Boolean
+    ) {
+        val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+        summary.disableRefreshRateSwitching = true
+        val vote = DisableRefreshRateSwitchingVote(voteDisableSwitching)
+
+        vote.updateSummary(summary)
+
+        assertThat(summary.disableRefreshRateSwitching).isTrue()
+    }
+
+    @Test
+    fun `disables refresh rate switching if requested`() {
+        val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+        val vote = DisableRefreshRateSwitchingVote(true)
+
+        vote.updateSummary(summary)
+
+        assertThat(summary.disableRefreshRateSwitching).isTrue()
+    }
+
+    @Test
+    fun `does not disable refresh rate switching if not requested`() {
+        val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+        val vote = DisableRefreshRateSwitchingVote(false)
+
+        vote.updateSummary(summary)
+
+        assertThat(summary.disableRefreshRateSwitching).isFalse()
+    }
+}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
index 6798a2d..d085923 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
@@ -28,7 +28,6 @@
 import static android.hardware.display.DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_LOW_ZONE;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
-import static com.android.server.display.mode.Vote.INVALID_SIZE;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -1193,7 +1192,9 @@
         assertVoteForPhysicalRefreshRate(vote, 90 /*fps*/);
         vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH);
         assertThat(vote).isNotNull();
-        assertThat(vote.disableRefreshRateSwitching).isTrue();
+        assertThat(vote).isInstanceOf(DisableRefreshRateSwitchingVote.class);
+        DisableRefreshRateSwitchingVote disableVote = (DisableRefreshRateSwitchingVote) vote;
+        assertThat(disableVote.mDisableRefreshRateSwitching).isTrue();
     }
 
     @Test
@@ -1272,7 +1273,9 @@
         assertVoteForPhysicalRefreshRate(vote, 90 /*fps*/);
         vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH);
         assertThat(vote).isNotNull();
-        assertThat(vote.disableRefreshRateSwitching).isTrue();
+        assertThat(vote).isInstanceOf(DisableRefreshRateSwitchingVote.class);
+        DisableRefreshRateSwitchingVote disableVote = (DisableRefreshRateSwitchingVote) vote;
+        assertThat(disableVote.mDisableRefreshRateSwitching).isTrue();
 
         // We expect DisplayModeDirector to act on BrightnessInfo.adjustedBrightness; set only this
         // parameter to the necessary threshold
@@ -1341,7 +1344,9 @@
         assertVoteForPhysicalRefreshRate(vote, 60 /*fps*/);
         vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH);
         assertThat(vote).isNotNull();
-        assertThat(vote.disableRefreshRateSwitching).isTrue();
+        assertThat(vote).isInstanceOf(DisableRefreshRateSwitchingVote.class);
+        DisableRefreshRateSwitchingVote disableVote = (DisableRefreshRateSwitchingVote) vote;
+        assertThat(disableVote.mDisableRefreshRateSwitching).isTrue();
     }
 
     @Test
@@ -1424,7 +1429,9 @@
         assertVoteForPhysicalRefreshRate(vote, 90 /*fps*/);
         vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH);
         assertThat(vote).isNotNull();
-        assertThat(vote.disableRefreshRateSwitching).isTrue();
+        assertThat(vote).isInstanceOf(DisableRefreshRateSwitchingVote.class);
+        DisableRefreshRateSwitchingVote disableVote = (DisableRefreshRateSwitchingVote) vote;
+        assertThat(disableVote.mDisableRefreshRateSwitching).isTrue();
 
         // Set critical and check new refresh rate
         Temperature temp = getSkinTemp(Temperature.THROTTLING_CRITICAL);
@@ -1436,7 +1443,9 @@
         assertVoteForPhysicalRefreshRate(vote, 60 /*fps*/);
         vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH);
         assertThat(vote).isNotNull();
-        assertThat(vote.disableRefreshRateSwitching).isTrue();
+        assertThat(vote).isInstanceOf(DisableRefreshRateSwitchingVote.class);
+        disableVote = (DisableRefreshRateSwitchingVote) vote;
+        assertThat(disableVote.mDisableRefreshRateSwitching).isTrue();
     }
 
     @Test
@@ -1519,7 +1528,9 @@
         assertVoteForPhysicalRefreshRate(vote, 90 /*fps*/);
         vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH);
         assertThat(vote).isNotNull();
-        assertThat(vote.disableRefreshRateSwitching).isTrue();
+        assertThat(vote).isInstanceOf(DisableRefreshRateSwitchingVote.class);
+        DisableRefreshRateSwitchingVote disableVote = (DisableRefreshRateSwitchingVote) vote;
+        assertThat(disableVote.mDisableRefreshRateSwitching).isTrue();
 
         // Set critical and check new refresh rate
         Temperature temp = getSkinTemp(Temperature.THROTTLING_CRITICAL);
@@ -1531,7 +1542,9 @@
         assertVoteForPhysicalRefreshRate(vote, 60 /*fps*/);
         vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH);
         assertThat(vote).isNotNull();
-        assertThat(vote.disableRefreshRateSwitching).isTrue();
+        assertThat(vote).isInstanceOf(DisableRefreshRateSwitchingVote.class);
+        disableVote = (DisableRefreshRateSwitchingVote) vote;
+        assertThat(disableVote.mDisableRefreshRateSwitching).isTrue();
     }
 
     @Test
@@ -1877,61 +1890,43 @@
         DisplayModeDirector director = createDirectorFromFpsRange(60, 90);
         director.getAppRequestObserver().setAppRequest(DISPLAY_ID, 60, 0, 0);
 
-        Vote appRequestRefreshRate =
-                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
-        assertNotNull(appRequestRefreshRate);
-        assertThat(appRequestRefreshRate.refreshRateRanges.physical.min).isZero();
-        assertThat(appRequestRefreshRate.refreshRateRanges.physical.max).isPositiveInfinity();
-        assertThat(appRequestRefreshRate.refreshRateRanges.render.min).isZero();
-        assertThat(appRequestRefreshRate.refreshRateRanges.render.max).isPositiveInfinity();
-        assertThat(appRequestRefreshRate.disableRefreshRateSwitching).isFalse();
-        assertThat(appRequestRefreshRate.appRequestBaseModeRefreshRate)
-                .isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(appRequestRefreshRate.height).isEqualTo(INVALID_SIZE);
-        assertThat(appRequestRefreshRate.width).isEqualTo(INVALID_SIZE);
+        Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
+        assertNotNull(vote);
+        assertThat(vote).isInstanceOf(BaseModeRefreshRateVote.class);
+        BaseModeRefreshRateVote baseModeVote = (BaseModeRefreshRateVote) vote;
+        assertThat(baseModeVote.mAppRequestBaseModeRefreshRate).isWithin(FLOAT_TOLERANCE).of(60);
 
-        Vote appRequestSize = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE);
-        assertNotNull(appRequestSize);
-        assertThat(appRequestSize.refreshRateRanges.physical.min).isZero();
-        assertThat(appRequestSize.refreshRateRanges.physical.max).isPositiveInfinity();
-        assertThat(appRequestSize.refreshRateRanges.render.min).isZero();
-        assertThat(appRequestSize.refreshRateRanges.render.max).isPositiveInfinity();
-        assertThat(appRequestSize.disableRefreshRateSwitching).isFalse();
-        assertThat(appRequestSize.appRequestBaseModeRefreshRate).isZero();
-        assertThat(appRequestSize.height).isEqualTo(1000);
-        assertThat(appRequestSize.width).isEqualTo(1000);
+        vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE);
+        assertNotNull(vote);
+        assertThat(vote).isInstanceOf(SizeVote.class);
+        SizeVote sizeVote = (SizeVote) vote;
+        assertThat(sizeVote.mHeight).isEqualTo(1000);
+        assertThat(sizeVote.mWidth).isEqualTo(1000);
+        assertThat(sizeVote.mMinHeight).isEqualTo(1000);
+        assertThat(sizeVote.mMinWidth).isEqualTo(1000);
 
-        Vote appRequestRefreshRateRange =
-                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
-        assertNull(appRequestRefreshRateRange);
+        vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
+        assertNull(vote);
 
         director.getAppRequestObserver().setAppRequest(DISPLAY_ID, 90, 0, 0);
 
-        appRequestRefreshRate =
-                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
-        assertNotNull(appRequestRefreshRate);
-        assertThat(appRequestRefreshRate.refreshRateRanges.physical.min).isZero();
-        assertThat(appRequestRefreshRate.refreshRateRanges.physical.max).isPositiveInfinity();
-        assertThat(appRequestRefreshRate.refreshRateRanges.render.min).isZero();
-        assertThat(appRequestRefreshRate.refreshRateRanges.render.max).isPositiveInfinity();
-        assertThat(appRequestRefreshRate.disableRefreshRateSwitching).isFalse();
-        assertThat(appRequestRefreshRate.appRequestBaseModeRefreshRate)
-                .isWithin(FLOAT_TOLERANCE).of(90);
-        assertThat(appRequestRefreshRate.height).isEqualTo(INVALID_SIZE);
-        assertThat(appRequestRefreshRate.width).isEqualTo(INVALID_SIZE);
+        vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
+        assertNotNull(vote);
+        assertThat(vote).isInstanceOf(BaseModeRefreshRateVote.class);
+        baseModeVote = (BaseModeRefreshRateVote) vote;
+        assertThat(baseModeVote.mAppRequestBaseModeRefreshRate).isWithin(FLOAT_TOLERANCE).of(90);
 
-        appRequestSize = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE);
-        assertNotNull(appRequestSize);
-        assertThat(appRequestSize.refreshRateRanges.physical.min).isZero();
-        assertThat(appRequestSize.refreshRateRanges.physical.max).isPositiveInfinity();
-        assertThat(appRequestSize.refreshRateRanges.render.min).isZero();
-        assertThat(appRequestSize.refreshRateRanges.render.max).isPositiveInfinity();
-        assertThat(appRequestSize.height).isEqualTo(1000);
-        assertThat(appRequestSize.width).isEqualTo(1000);
+        vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE);
+        assertNotNull(vote);
+        assertThat(vote).isInstanceOf(SizeVote.class);
+        sizeVote = (SizeVote) vote;
+        assertThat(sizeVote.mHeight).isEqualTo(1000);
+        assertThat(sizeVote.mWidth).isEqualTo(1000);
+        assertThat(sizeVote.mMinHeight).isEqualTo(1000);
+        assertThat(sizeVote.mMinWidth).isEqualTo(1000);
 
-        appRequestRefreshRateRange =
-                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
-        assertNull(appRequestRefreshRateRange);
+        vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
+        assertNull(vote);
     }
 
     @Test
@@ -1945,17 +1940,12 @@
         Vote appRequestSize = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE);
         assertNull(appRequestSize);
 
-        Vote appRequestRefreshRateRange =
-                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
-        assertNotNull(appRequestRefreshRateRange);
-        assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.min).isZero();
-        assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.max)
-                .isPositiveInfinity();
-        assertThat(appRequestRefreshRateRange.refreshRateRanges.render.min)
-                .isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(appRequestRefreshRateRange.refreshRateRanges.render.max).isAtLeast(90);
-        assertThat(appRequestRefreshRateRange.height).isEqualTo(INVALID_SIZE);
-        assertThat(appRequestRefreshRateRange.width).isEqualTo(INVALID_SIZE);
+        Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
+        assertNotNull(vote);
+        assertThat(vote).isInstanceOf(RefreshRateVote.RenderVote.class);
+        RefreshRateVote.RenderVote renderVote = (RefreshRateVote.RenderVote) vote;
+        assertThat(renderVote.mMinRefreshRate).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(renderVote.mMaxRefreshRate).isAtLeast(90);
 
         director.getAppRequestObserver().setAppRequest(DISPLAY_ID, -1, 90, 0);
         appRequestRefreshRate =
@@ -1965,18 +1955,12 @@
         appRequestSize = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE);
         assertNull(appRequestSize);
 
-        appRequestRefreshRateRange =
-                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
-        assertNotNull(appRequestRefreshRateRange);
-        assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.min).isZero();
-        assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.max)
-                .isPositiveInfinity();
-
-        assertThat(appRequestRefreshRateRange.refreshRateRanges.render.min)
-                .isWithin(FLOAT_TOLERANCE).of(90);
-        assertThat(appRequestRefreshRateRange.refreshRateRanges.render.max).isAtLeast(90);
-        assertThat(appRequestRefreshRateRange.height).isEqualTo(INVALID_SIZE);
-        assertThat(appRequestRefreshRateRange.width).isEqualTo(INVALID_SIZE);
+        vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
+        assertNotNull(vote);
+        assertThat(vote).isInstanceOf(RefreshRateVote.RenderVote.class);
+        renderVote = (RefreshRateVote.RenderVote) vote;
+        assertThat(renderVote.mMinRefreshRate).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(renderVote.mMaxRefreshRate).isAtLeast(90);
     }
 
     @Test
@@ -1990,18 +1974,12 @@
         Vote appRequestSize = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE);
         assertNull(appRequestSize);
 
-        Vote appRequestRefreshRateRange =
-                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
-        assertNotNull(appRequestRefreshRateRange);
-        assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.min).isZero();
-        assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.max)
-                .isPositiveInfinity();
-
-        assertThat(appRequestRefreshRateRange.refreshRateRanges.render.min).isZero();
-        assertThat(appRequestRefreshRateRange.refreshRateRanges.render.max)
-                .isWithin(FLOAT_TOLERANCE).of(90);
-        assertThat(appRequestRefreshRateRange.height).isEqualTo(INVALID_SIZE);
-        assertThat(appRequestRefreshRateRange.width).isEqualTo(INVALID_SIZE);
+        Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
+        assertNotNull(vote);
+        assertThat(vote).isInstanceOf(RefreshRateVote.RenderVote.class);
+        RefreshRateVote.RenderVote renderVote = (RefreshRateVote.RenderVote) vote;
+        assertThat(renderVote.mMinRefreshRate).isZero();
+        assertThat(renderVote.mMaxRefreshRate).isWithin(FLOAT_TOLERANCE).of(90);
 
         director.getAppRequestObserver().setAppRequest(DISPLAY_ID, -1, 0, 60);
         appRequestRefreshRate =
@@ -2011,18 +1989,12 @@
         appRequestSize = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE);
         assertNull(appRequestSize);
 
-        appRequestRefreshRateRange =
-                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
-        assertNotNull(appRequestRefreshRateRange);
-        assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.min).isZero();
-        assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.max)
-                .isPositiveInfinity();
-
-        assertThat(appRequestRefreshRateRange.refreshRateRanges.render.min).isZero();
-        assertThat(appRequestRefreshRateRange.refreshRateRanges.render.max)
-                .isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(appRequestRefreshRateRange.height).isEqualTo(INVALID_SIZE);
-        assertThat(appRequestRefreshRateRange.width).isEqualTo(INVALID_SIZE);
+        vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
+        assertNotNull(vote);
+        assertThat(vote).isInstanceOf(RefreshRateVote.RenderVote.class);
+        renderVote = (RefreshRateVote.RenderVote) vote;
+        assertThat(renderVote.mMinRefreshRate).isZero();
+        assertThat(renderVote.mMaxRefreshRate).isWithin(FLOAT_TOLERANCE).of(60);
     }
 
     @Test
@@ -2046,41 +2018,27 @@
         DisplayModeDirector director = createDirectorFromFpsRange(60, 90);
         director.getAppRequestObserver().setAppRequest(DISPLAY_ID, 60, 90, 90);
 
-        Vote appRequestRefreshRate =
-                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
-        assertNotNull(appRequestRefreshRate);
-        assertThat(appRequestRefreshRate.refreshRateRanges.physical.min).isZero();
-        assertThat(appRequestRefreshRate.refreshRateRanges.physical.max).isPositiveInfinity();
-        assertThat(appRequestRefreshRate.refreshRateRanges.render.min).isZero();
-        assertThat(appRequestRefreshRate.refreshRateRanges.render.max).isPositiveInfinity();
-        assertThat(appRequestRefreshRate.disableRefreshRateSwitching).isFalse();
-        assertThat(appRequestRefreshRate.appRequestBaseModeRefreshRate)
-                .isWithin(FLOAT_TOLERANCE).of(60);
-        assertThat(appRequestRefreshRate.height).isEqualTo(INVALID_SIZE);
-        assertThat(appRequestRefreshRate.width).isEqualTo(INVALID_SIZE);
+        Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
+        assertNotNull(vote);
+        assertThat(vote).isInstanceOf(BaseModeRefreshRateVote.class);
+        BaseModeRefreshRateVote baseModeVote = (BaseModeRefreshRateVote) vote;
+        assertThat(baseModeVote.mAppRequestBaseModeRefreshRate).isWithin(FLOAT_TOLERANCE).of(60);
 
-        Vote appRequestSize =
-                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE);
-        assertNotNull(appRequestSize);
-        assertThat(appRequestSize.refreshRateRanges.physical.min).isZero();
-        assertThat(appRequestSize.refreshRateRanges.physical.max).isPositiveInfinity();
-        assertThat(appRequestSize.refreshRateRanges.render.min).isZero();
-        assertThat(appRequestSize.refreshRateRanges.render.max).isPositiveInfinity();
-        assertThat(appRequestSize.height).isEqualTo(1000);
-        assertThat(appRequestSize.width).isEqualTo(1000);
+        vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE);
+        assertNotNull(vote);
+        assertThat(vote).isInstanceOf(SizeVote.class);
+        SizeVote sizeVote = (SizeVote) vote;
+        assertThat(sizeVote.mHeight).isEqualTo(1000);
+        assertThat(sizeVote.mWidth).isEqualTo(1000);
+        assertThat(sizeVote.mMinHeight).isEqualTo(1000);
+        assertThat(sizeVote.mMinWidth).isEqualTo(1000);
 
-        Vote appRequestRefreshRateRange =
-                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
-        assertNotNull(appRequestRefreshRateRange);
-        assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.min).isZero();
-        assertThat(appRequestRefreshRateRange.refreshRateRanges.physical.max)
-                .isPositiveInfinity();
-        assertThat(appRequestRefreshRateRange.refreshRateRanges.render.min)
-                .isWithin(FLOAT_TOLERANCE).of(90);
-        assertThat(appRequestRefreshRateRange.refreshRateRanges.render.max)
-                .isWithin(FLOAT_TOLERANCE).of(90);
-        assertThat(appRequestRefreshRateRange.height).isEqualTo(INVALID_SIZE);
-        assertThat(appRequestRefreshRateRange.width).isEqualTo(INVALID_SIZE);
+        vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_RENDER_FRAME_RATE_RANGE);
+        assertNotNull(vote);
+        assertThat(vote).isInstanceOf(RefreshRateVote.RenderVote.class);
+        RefreshRateVote.RenderVote renderVote = (RefreshRateVote.RenderVote) vote;
+        assertThat(renderVote.mMinRefreshRate).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(renderVote.mMaxRefreshRate).isWithin(FLOAT_TOLERANCE).of(90);
     }
 
     @Test
@@ -3150,8 +3108,7 @@
         captor.getValue().onAuthenticationPossible(DISPLAY_ID, true);
 
         Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_AUTH_OPTIMIZER_RENDER_FRAME_RATE);
-        assertThat(vote.refreshRateRanges.physical.min).isWithin(FLOAT_TOLERANCE).of(90);
-        assertThat(vote.refreshRateRanges.physical.max).isWithin(FLOAT_TOLERANCE).of(90);
+        assertVoteForPhysicalRefreshRate(vote, 90);
     }
 
     @Test
@@ -3184,8 +3141,7 @@
         captor.getValue().onRequestEnabled(DISPLAY_ID);
 
         Vote vote = director.getVote(DISPLAY_ID, Vote.PRIORITY_UDFPS);
-        assertThat(vote.refreshRateRanges.physical.min).isWithin(FLOAT_TOLERANCE).of(90);
-        assertThat(vote.refreshRateRanges.physical.max).isWithin(FLOAT_TOLERANCE).of(90);
+        assertVoteForPhysicalRefreshRate(vote, 90);
     }
 
     @Test
@@ -3257,16 +3213,21 @@
 
     private void assertVoteForPhysicalRefreshRate(Vote vote, float refreshRate) {
         assertThat(vote).isNotNull();
-        final RefreshRateRange expectedRange = new RefreshRateRange(refreshRate, refreshRate);
-        assertThat(vote.refreshRateRanges.physical).isEqualTo(expectedRange);
+        assertThat(vote).isInstanceOf(CombinedVote.class);
+        CombinedVote combinedVote = (CombinedVote) vote;
+        RefreshRateVote.PhysicalVote physicalVote =
+                (RefreshRateVote.PhysicalVote) combinedVote.mVotes.get(0);
+        assertThat(physicalVote.mMinRefreshRate).isWithin(FLOAT_TOLERANCE).of(refreshRate);
+        assertThat(physicalVote.mMaxRefreshRate).isWithin(FLOAT_TOLERANCE).of(refreshRate);
     }
 
     private void assertVoteForRenderFrameRateRange(
             Vote vote, float frameRateLow, float frameRateHigh) {
         assertThat(vote).isNotNull();
-        final RefreshRateRange expectedRange =
-                new RefreshRateRange(frameRateLow, frameRateHigh);
-        assertThat(vote.refreshRateRanges.render).isEqualTo(expectedRange);
+        assertThat(vote).isInstanceOf(RefreshRateVote.RenderVote.class);
+        RefreshRateVote.RenderVote renderVote = (RefreshRateVote.RenderVote) vote;
+        assertThat(renderVote.mMinRefreshRate).isEqualTo(frameRateLow);
+        assertThat(renderVote.mMaxRefreshRate).isEqualTo(frameRateHigh);
     }
 
     public static class FakeDeviceConfig extends FakeDeviceConfigInterface {
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/PhysicalVoteTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/PhysicalVoteTest.kt
new file mode 100644
index 0000000..547008e
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/PhysicalVoteTest.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2023 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.display.mode
+
+import androidx.test.filters.SmallTest
+import com.android.server.display.mode.DisplayModeDirector.VoteSummary
+import com.google.common.truth.Truth.assertThat
+import com.google.testing.junit.testparameterinjector.TestParameterInjector
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val MIN_REFRESH_RATE = 60f
+private const val MAX_REFRESH_RATE = 90f
+
+@SmallTest
+@RunWith(TestParameterInjector::class)
+class PhysicalVoteTest {
+    private lateinit var physicalVote: RefreshRateVote.PhysicalVote
+
+    @Before
+    fun setUp() {
+        physicalVote = RefreshRateVote.PhysicalVote(MIN_REFRESH_RATE, MAX_REFRESH_RATE)
+    }
+
+    @Test
+    fun `updates minPhysicalRefreshRate if summary has less`() {
+        val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+        summary.minPhysicalRefreshRate = 45f
+
+        physicalVote.updateSummary(summary)
+
+        assertThat(summary.minPhysicalRefreshRate).isEqualTo(MIN_REFRESH_RATE)
+    }
+
+    @Test
+    fun `does not update minPhysicalRefreshRate if summary has more`() {
+        val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+        summary.minPhysicalRefreshRate = 75f
+
+        physicalVote.updateSummary(summary)
+
+        assertThat(summary.minPhysicalRefreshRate).isEqualTo(75f)
+    }
+
+    @Test
+    fun `updates maxPhysicalRefreshRate if summary has more`() {
+        val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+        summary.maxPhysicalRefreshRate = 120f
+
+        physicalVote.updateSummary(summary)
+
+        assertThat(summary.maxPhysicalRefreshRate).isEqualTo(MAX_REFRESH_RATE)
+    }
+
+    @Test
+    fun `does not update maxPhysicalRefreshRate if summary has less`() {
+        val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+        summary.maxPhysicalRefreshRate = 75f
+
+        physicalVote.updateSummary(summary)
+
+        assertThat(summary.maxPhysicalRefreshRate).isEqualTo(75f)
+    }
+
+    @Test
+    fun `updates maxRenderFrameRate if summary has more`() {
+        val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+        summary.maxRenderFrameRate = 120f
+
+        physicalVote.updateSummary(summary)
+
+        assertThat(summary.maxRenderFrameRate).isEqualTo(MAX_REFRESH_RATE)
+    }
+
+    @Test
+    fun `does not update maxRenderFrameRate if summary has less`() {
+        val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+        summary.maxRenderFrameRate = 75f
+
+        physicalVote.updateSummary(summary)
+
+        assertThat(summary.maxRenderFrameRate).isEqualTo(75f)
+    }
+}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/RenderVoteTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/RenderVoteTest.kt
new file mode 100644
index 0000000..868a893
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/RenderVoteTest.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2023 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.display.mode
+
+import androidx.test.filters.SmallTest
+import com.android.server.display.mode.DisplayModeDirector.VoteSummary
+import com.google.common.truth.Truth.assertThat
+import com.google.testing.junit.testparameterinjector.TestParameterInjector
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private const val MIN_REFRESH_RATE = 60f
+private const val MAX_REFRESH_RATE = 90f
+
+@SmallTest
+@RunWith(TestParameterInjector::class)
+class RenderVoteTest {
+
+    private lateinit var renderVote: RefreshRateVote.RenderVote
+
+    @Before
+    fun setUp() {
+        renderVote = RefreshRateVote.RenderVote(MIN_REFRESH_RATE, MAX_REFRESH_RATE)
+    }
+
+    @Test
+    fun `updates minRenderFrameRate if summary has less`() {
+        val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+        summary.minRenderFrameRate = 45f
+
+        renderVote.updateSummary(summary)
+
+        assertThat(summary.minRenderFrameRate).isEqualTo(MIN_REFRESH_RATE)
+    }
+
+    @Test
+    fun `does not update minRenderFrameRate if summary has more`() {
+        val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+        summary.minRenderFrameRate = 75f
+
+        renderVote.updateSummary(summary)
+
+        assertThat(summary.minRenderFrameRate).isEqualTo(75f)
+    }
+
+    @Test
+    fun `updates maxRenderFrameRate if summary has more`() {
+        val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+        summary.maxRenderFrameRate = 120f
+
+        renderVote.updateSummary(summary)
+
+        assertThat(summary.maxRenderFrameRate).isEqualTo(MAX_REFRESH_RATE)
+    }
+
+    @Test
+    fun `does not update maxRenderFrameRate if summary has less`() {
+        val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+        summary.maxRenderFrameRate = 75f
+
+        renderVote.updateSummary(summary)
+
+        assertThat(summary.maxRenderFrameRate).isEqualTo(75f)
+    }
+
+    @Test
+    fun `updates minPhysicalRefreshRate if summary has less`() {
+        val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+        summary.minPhysicalRefreshRate = 45f
+
+        renderVote.updateSummary(summary)
+
+        assertThat(summary.minPhysicalRefreshRate).isEqualTo(MIN_REFRESH_RATE)
+    }
+
+    @Test
+    fun `does not update minPhysicalRefreshRate if summary has more`() {
+        val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+        summary.minPhysicalRefreshRate = 75f
+
+        renderVote.updateSummary(summary)
+
+        assertThat(summary.minPhysicalRefreshRate).isEqualTo(75f)
+    }
+}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/SizeVoteTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/SizeVoteTest.kt
new file mode 100644
index 0000000..1c631b0
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/SizeVoteTest.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2023 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.display.mode
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.server.display.mode.DisplayModeDirector.VoteSummary
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+
+private const val WIDTH = 800
+private const val HEIGHT = 1600
+private const val MIN_WIDTH = 400
+private const val MIN_HEIGHT = 1200
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SizeVoteTest {
+    private lateinit var sizeVote: SizeVote
+
+    @Before
+    fun setUp() {
+        sizeVote = SizeVote(WIDTH, HEIGHT, MIN_WIDTH, MIN_HEIGHT)
+    }
+
+    @Test
+    fun `updates size if width and height not set and display resolution voting disabled`() {
+        val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ false)
+        summary.width = Vote.INVALID_SIZE
+        summary.height = Vote.INVALID_SIZE
+        summary.minWidth = 100
+        summary.minHeight = 200
+
+        sizeVote.updateSummary(summary)
+
+        assertThat(summary.width).isEqualTo(WIDTH)
+        assertThat(summary.height).isEqualTo(HEIGHT)
+        assertThat(summary.minWidth).isEqualTo(MIN_WIDTH)
+        assertThat(summary.minHeight).isEqualTo(MIN_HEIGHT)
+    }
+
+    @Test
+    fun `does not update size if width set and display resolution voting disabled`() {
+        val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ false)
+        summary.width = 150
+        summary.height = Vote.INVALID_SIZE
+        summary.minWidth = 100
+        summary.minHeight = 200
+
+        sizeVote.updateSummary(summary)
+
+        assertThat(summary.width).isEqualTo(150)
+        assertThat(summary.height).isEqualTo(Vote.INVALID_SIZE)
+        assertThat(summary.minWidth).isEqualTo(100)
+        assertThat(summary.minHeight).isEqualTo(200)
+    }
+
+    @Test
+    fun `does not update size if height set and display resolution voting disabled`() {
+        val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ false)
+        summary.width = Vote.INVALID_SIZE
+        summary.height = 250
+        summary.minWidth = 100
+        summary.minHeight = 200
+
+        sizeVote.updateSummary(summary)
+
+        assertThat(summary.width).isEqualTo(Vote.INVALID_SIZE)
+        assertThat(summary.height).isEqualTo(250)
+        assertThat(summary.minWidth).isEqualTo(100)
+        assertThat(summary.minHeight).isEqualTo(200)
+    }
+
+    @Test
+    fun `updates width if summary has more and display resolution voting enabled`() {
+        val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+        summary.width = 850
+
+        sizeVote.updateSummary(summary)
+
+        assertThat(summary.width).isEqualTo(WIDTH)
+    }
+
+    @Test
+    fun `does not update width if summary has less and display resolution voting enabled`() {
+        val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+        summary.width = 750
+
+        sizeVote.updateSummary(summary)
+
+        assertThat(summary.width).isEqualTo(750)
+    }
+
+    @Test
+    fun `updates height if summary has more and display resolution voting enabled`() {
+        val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+        summary.height = 1650
+
+        sizeVote.updateSummary(summary)
+
+        assertThat(summary.height).isEqualTo(HEIGHT)
+    }
+
+    @Test
+    fun `does not update height if summary has less and display resolution voting enabled`() {
+        val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+        summary.height = 1550
+
+        sizeVote.updateSummary(summary)
+
+        assertThat(summary.height).isEqualTo(1550)
+    }
+
+    @Test
+    fun `updates minWidth if summary has less and display resolution voting enabled`() {
+        val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+        summary.width = 150
+        summary.minWidth = 350
+
+        sizeVote.updateSummary(summary)
+
+        assertThat(summary.minWidth).isEqualTo(MIN_WIDTH)
+    }
+
+    @Test
+    fun `does not update minWidth if summary has more and display resolution voting enabled`() {
+        val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+        summary.width = 150
+        summary.minWidth = 450
+
+        sizeVote.updateSummary(summary)
+
+        assertThat(summary.minWidth).isEqualTo(450)
+    }
+
+    @Test
+    fun `updates minHeight if summary has less and display resolution voting enabled`() {
+        val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+        summary.width = 150
+        summary.minHeight = 1150
+
+        sizeVote.updateSummary(summary)
+
+        assertThat(summary.minHeight).isEqualTo(MIN_HEIGHT)
+    }
+
+    @Test
+    fun `does not update minHeight if summary has more and display resolution voting enabled`() {
+        val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+        summary.width = 150
+        summary.minHeight = 1250
+
+        sizeVote.updateSummary(summary)
+
+        assertThat(summary.minHeight).isEqualTo(1250)
+    }
+}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/SkinThermalStatusObserverTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/SkinThermalStatusObserverTest.java
index 9ab6ee5..f677401 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/mode/SkinThermalStatusObserverTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/SkinThermalStatusObserverTest.java
@@ -17,6 +17,8 @@
 package com.android.server.display.mode;
 
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 
@@ -102,17 +104,21 @@
         SparseArray<Vote> displayVotes = mStorage.getVotes(DISPLAY_ID);
         assertEquals(1, displayVotes.size());
 
-        Vote vote = displayVotes.get(
-                Vote.PRIORITY_SKIN_TEMPERATURE);
-        assertEquals(0, vote.refreshRateRanges.render.min, FLOAT_TOLERANCE);
-        assertEquals(60, vote.refreshRateRanges.render.max, FLOAT_TOLERANCE);
+        Vote vote = displayVotes.get(Vote.PRIORITY_SKIN_TEMPERATURE);
+
+        assertThat(vote).isInstanceOf(RefreshRateVote.RenderVote.class);
+        RefreshRateVote.RenderVote renderVote = (RefreshRateVote.RenderVote) vote;
+        assertEquals(0, renderVote.mMinRefreshRate, FLOAT_TOLERANCE);
+        assertEquals(60, renderVote.mMaxRefreshRate, FLOAT_TOLERANCE);
 
         SparseArray<Vote> otherDisplayVotes = mStorage.getVotes(DISPLAY_ID_OTHER);
         assertEquals(1, otherDisplayVotes.size());
 
         vote = otherDisplayVotes.get(Vote.PRIORITY_SKIN_TEMPERATURE);
-        assertEquals(0, vote.refreshRateRanges.render.min, FLOAT_TOLERANCE);
-        assertEquals(60, vote.refreshRateRanges.render.max, FLOAT_TOLERANCE);
+        assertThat(vote).isInstanceOf(RefreshRateVote.RenderVote.class);
+        renderVote = (RefreshRateVote.RenderVote) vote;
+        assertEquals(0, renderVote.mMinRefreshRate, FLOAT_TOLERANCE);
+        assertEquals(60, renderVote.mMaxRefreshRate, FLOAT_TOLERANCE);
     }
 
     @Test
@@ -167,8 +173,10 @@
         SparseArray<Vote> displayVotes = mStorage.getVotes(DISPLAY_ID);
         assertEquals(1, displayVotes.size());
         Vote vote = displayVotes.get(Vote.PRIORITY_SKIN_TEMPERATURE);
-        assertEquals(90, vote.refreshRateRanges.render.min, FLOAT_TOLERANCE);
-        assertEquals(120, vote.refreshRateRanges.render.max, FLOAT_TOLERANCE);
+        assertThat(vote).isInstanceOf(RefreshRateVote.RenderVote.class);
+        RefreshRateVote.RenderVote renderVote = (RefreshRateVote.RenderVote) vote;
+        assertEquals(90, renderVote.mMinRefreshRate, FLOAT_TOLERANCE);
+        assertEquals(120, renderVote.mMaxRefreshRate, FLOAT_TOLERANCE);
         assertEquals(0, mStorage.getVotes(DISPLAY_ID_OTHER).size());
     }
 
@@ -188,8 +196,10 @@
         SparseArray<Vote> displayVotes = mStorage.getVotes(DISPLAY_ID_ADDED);
 
         Vote vote = displayVotes.get(Vote.PRIORITY_SKIN_TEMPERATURE);
-        assertEquals(0, vote.refreshRateRanges.render.min, FLOAT_TOLERANCE);
-        assertEquals(60, vote.refreshRateRanges.render.max, FLOAT_TOLERANCE);
+        assertThat(vote).isInstanceOf(RefreshRateVote.RenderVote.class);
+        RefreshRateVote.RenderVote renderVote = (RefreshRateVote.RenderVote) vote;
+        assertEquals(0, renderVote.mMinRefreshRate, FLOAT_TOLERANCE);
+        assertEquals(60, renderVote.mMaxRefreshRate, FLOAT_TOLERANCE);
     }
 
     @Test
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/SupportedModesVoteTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/SupportedModesVoteTest.kt
new file mode 100644
index 0000000..cc88003
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/SupportedModesVoteTest.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2023 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.display.mode
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.server.display.mode.DisplayModeDirector.VoteSummary
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SupportedModesVoteTest {
+    private val supportedModes = listOf(
+            SupportedModesVote.SupportedMode(60f, 90f ),
+            SupportedModesVote.SupportedMode(120f, 240f )
+    )
+
+    private val otherMode = SupportedModesVote.SupportedMode(120f, 120f )
+
+    private lateinit var supportedModesVote: SupportedModesVote
+
+    @Before
+    fun setUp() {
+        supportedModesVote = SupportedModesVote(supportedModes)
+    }
+
+    @Test
+    fun `adds supported modes if supportedModes in summary is null`() {
+        val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+
+        supportedModesVote.updateSummary(summary)
+
+        assertThat(summary.supportedModes).containsExactlyElementsIn(supportedModes)
+    }
+
+    @Test
+    fun `does not add supported modes if summary has empty list of modes`() {
+        val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+        summary.supportedModes = ArrayList()
+
+        supportedModesVote.updateSummary(summary)
+
+        assertThat(summary.supportedModes).isEmpty()
+    }
+
+    @Test
+    fun `filters out modes that does not match vote`() {
+        val summary = VoteSummary(/* isDisplayResolutionRangeVotingEnabled= */ true)
+        summary.supportedModes = ArrayList(listOf(otherMode, supportedModes[0]))
+
+        supportedModesVote.updateSummary(summary)
+
+        assertThat(summary.supportedModes).containsExactly(supportedModes[0])
+    }
+}
\ No newline at end of file
diff --git a/services/tests/servicestests/src/com/android/server/audio/LoudnessCodecHelperTest.java b/services/tests/servicestests/src/com/android/server/audio/LoudnessCodecHelperTest.java
new file mode 100644
index 0000000..749b07d
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/audio/LoudnessCodecHelperTest.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.audio;
+
+import static android.media.AudioManager.GET_DEVICES_OUTPUTS;
+import static android.media.AudioPlaybackConfiguration.PLAYER_UPDATE_DEVICE_ID;
+import static android.media.LoudnessCodecInfo.CodecMetadataType.CODEC_METADATA_TYPE_MPEG_4;
+import static android.media.LoudnessCodecInfo.CodecMetadataType.CODEC_METADATA_TYPE_MPEG_D;
+
+import static org.junit.Assume.assumeTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.media.AudioPlaybackConfiguration;
+import android.media.ILoudnessCodecUpdatesDispatcher;
+import android.media.LoudnessCodecInfo;
+import android.media.PlayerBase;
+import android.os.IBinder;
+import android.platform.test.annotations.Presubmit;
+import android.util.Log;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+@RunWith(AndroidJUnit4.class)
+@Presubmit
+public class LoudnessCodecHelperTest {
+    private static final String TAG = "LoudnessCodecHelperTest";
+
+    @Rule
+    public final MockitoRule mockito = MockitoJUnit.rule();
+
+    private LoudnessCodecHelper mLoudnessHelper;
+
+    @Mock
+    private AudioService mAudioService;
+    @Mock
+    private ILoudnessCodecUpdatesDispatcher.Default mDispatcher;
+
+    private final int mInitialApcPiid = 1;
+
+    @Before
+    public void setUp() throws Exception {
+        mLoudnessHelper = new LoudnessCodecHelper(mAudioService);
+
+        when(mAudioService.getActivePlaybackConfigurations()).thenReturn(
+                getApcListForPiids(mInitialApcPiid));
+
+        when(mDispatcher.asBinder()).thenReturn(Mockito.mock(IBinder.class));
+    }
+
+    @Test
+    public void registerDispatcher_sendsInitialUpdateOnStart() throws Exception {
+        mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher);
+
+        mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid,
+                List.of(getLoudnessInfo(/*mediaCodecHash=*/111, /*isDownmixing=*/true,
+                        CODEC_METADATA_TYPE_MPEG_4)));
+
+        verify(mDispatcher).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid), any());
+    }
+
+    @Test
+    public void unregisterDispatcher_noInitialUpdateOnStart() throws Exception {
+        mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher);
+        mLoudnessHelper.unregisterLoudnessCodecUpdatesDispatcher(mDispatcher);
+
+        mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid,
+                List.of(getLoudnessInfo(/*mediaCodecHash=*/222, /*isDownmixing=*/false,
+                        CODEC_METADATA_TYPE_MPEG_D)));
+
+        verify(mDispatcher, times(0)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid),
+                any());
+    }
+
+    @Test
+    public void addCodecInfo_sendsInitialUpdateAfterStart() throws Exception {
+        mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher);
+
+        mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid,
+                List.of(getLoudnessInfo(/*mediaCodecHash=*/111, /*isDownmixing=*/true,
+                        CODEC_METADATA_TYPE_MPEG_4)));
+        mLoudnessHelper.addLoudnessCodecInfo(mInitialApcPiid,
+                getLoudnessInfo(/*mediaCodecHash=*/222, /*isDownmixing=*/true,
+                        CODEC_METADATA_TYPE_MPEG_D));
+
+        verify(mDispatcher, times(2)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid),
+                any());
+    }
+
+    @Test
+    public void addCodecInfoForUnstartedPiid_noUpdateSent() throws Exception {
+        final int newPiid = 2;
+        mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher);
+
+        mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid,
+                List.of(getLoudnessInfo(/*mediaCodecHash=*/111, /*isDownmixing=*/true,
+                        CODEC_METADATA_TYPE_MPEG_4)));
+        mLoudnessHelper.addLoudnessCodecInfo(newPiid,
+                getLoudnessInfo(/*mediaCodecHash=*/222, /*isDownmixing=*/true,
+                        CODEC_METADATA_TYPE_MPEG_D));
+
+        verify(mDispatcher, times(1)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid),
+                any());
+    }
+
+    @Test
+    public void updateCodecParameters_updatesOnlyStartedPiids() throws Exception {
+        final int newPiid = 2;
+        mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher);
+
+        mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid,
+                List.of(getLoudnessInfo(/*mediaCodecHash=*/111, /*isDownmixing=*/true,
+                        CODEC_METADATA_TYPE_MPEG_4)));
+        //does not trigger dispatch since active apc list does not contain newPiid
+        mLoudnessHelper.startLoudnessCodecUpdates(newPiid,
+                List.of(getLoudnessInfo(/*mediaCodecHash=*/222, /*isDownmixing=*/true,
+                        CODEC_METADATA_TYPE_MPEG_D)));
+        verify(mDispatcher, times(1)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid),
+                any());
+
+        // triggers dispatch for new active apc with newPiid
+        mLoudnessHelper.updateCodecParameters(getApcListForPiids(newPiid));
+        verify(mDispatcher, times(1)).dispatchLoudnessCodecParameterChange(eq(newPiid), any());
+    }
+
+    @Test
+    public void updateCodecParameters_noStartedPiids_noDispatch() throws Exception {
+        mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher);
+        mLoudnessHelper.addLoudnessCodecInfo(mInitialApcPiid,
+                getLoudnessInfo(/*mediaCodecHash=*/222, /*isDownmixing=*/true,
+                        CODEC_METADATA_TYPE_MPEG_D));
+
+        mLoudnessHelper.updateCodecParameters(getApcListForPiids(mInitialApcPiid));
+
+        // no dispatch since mInitialApcPiid was not started
+        verify(mDispatcher, times(0)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid),
+                any());
+    }
+
+    @Test
+    public void updateCodecParameters_removedCodecInfo_noDispatch() throws Exception {
+        final LoudnessCodecInfo info = getLoudnessInfo(/*mediaCodecHash=*/111,
+                /*isDownmixing=*/true, CODEC_METADATA_TYPE_MPEG_4);
+        mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher);
+
+        mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid, List.of(info));
+        mLoudnessHelper.removeLoudnessCodecInfo(mInitialApcPiid, info);
+
+        mLoudnessHelper.updateCodecParameters(getApcListForPiids(mInitialApcPiid));
+
+        // no second dispatch since codec info was removed for updates
+        verify(mDispatcher, times(1)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid),
+                any());
+    }
+
+    @Test
+    public void updateCodecParameters_stoppedPiids_noDispatch() throws Exception {
+        final LoudnessCodecInfo info = getLoudnessInfo(/*mediaCodecHash=*/111,
+                /*isDownmixing=*/true, CODEC_METADATA_TYPE_MPEG_4);
+        mLoudnessHelper.registerLoudnessCodecUpdatesDispatcher(mDispatcher);
+
+        mLoudnessHelper.startLoudnessCodecUpdates(mInitialApcPiid, List.of(info));
+        mLoudnessHelper.stopLoudnessCodecUpdates(mInitialApcPiid);
+
+        mLoudnessHelper.updateCodecParameters(getApcListForPiids(mInitialApcPiid));
+
+        // no second dispatch since piid was removed for updates
+        verify(mDispatcher, times(1)).dispatchLoudnessCodecParameterChange(eq(mInitialApcPiid),
+                any());
+    }
+
+    private List<AudioPlaybackConfiguration> getApcListForPiids(int... piids) {
+        final ArrayList<AudioPlaybackConfiguration> apcList = new ArrayList<>();
+
+        AudioDeviceInfo[] devicesStatic = AudioManager.getDevicesStatic(GET_DEVICES_OUTPUTS);
+        assumeTrue(devicesStatic.length > 0);
+        int index = new Random().nextInt(devicesStatic.length);
+        Log.d(TAG, "Out devices number " + devicesStatic.length + ". Picking index " + index);
+        int deviceId = devicesStatic[index].getId();
+
+        for (int piid : piids) {
+            PlayerBase.PlayerIdCard idCard = Mockito.mock(PlayerBase.PlayerIdCard.class);
+            AudioPlaybackConfiguration apc =
+                    new AudioPlaybackConfiguration(idCard, piid, /*uid=*/1, /*pid=*/1);
+            apc.handleStateEvent(PLAYER_UPDATE_DEVICE_ID, deviceId);
+
+            apcList.add(apc);
+        }
+        return apcList;
+    }
+
+    private static LoudnessCodecInfo getLoudnessInfo(int mediaCodecHash, boolean isDownmixing,
+            int metadataType) {
+        LoudnessCodecInfo info = new LoudnessCodecInfo();
+        info.isDownmixing = isDownmixing;
+        info.mediaCodecHashCode = mediaCodecHash;
+        info.metadataType = metadataType;
+
+        return info;
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
index 57b1225..d70a4fd 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserPropertiesTest.java
@@ -60,7 +60,8 @@
                 .setShowInLauncher(21)
                 .setStartWithParent(false)
                 .setShowInSettings(45)
-                .setHideInSettingsInQuietMode(false)
+                .setShowInSharingSurfaces(78)
+                .setShowInQuietMode(12)
                 .setInheritDevicePolicy(67)
                 .setUseParentsContacts(false)
                 .setCrossProfileIntentFilterAccessControl(10)
@@ -74,7 +75,8 @@
         final UserProperties actualProps = new UserProperties(defaultProps);
         actualProps.setShowInLauncher(14);
         actualProps.setShowInSettings(32);
-        actualProps.setHideInSettingsInQuietMode(true);
+        actualProps.setShowInSharingSurfaces(46);
+        actualProps.setShowInQuietMode(27);
         actualProps.setInheritDevicePolicy(51);
         actualProps.setUseParentsContacts(true);
         actualProps.setCrossProfileIntentFilterAccessControl(20);
@@ -236,8 +238,10 @@
         assertThat(expected.getShowInLauncher()).isEqualTo(actual.getShowInLauncher());
         assertThat(expected.getStartWithParent()).isEqualTo(actual.getStartWithParent());
         assertThat(expected.getShowInSettings()).isEqualTo(actual.getShowInSettings());
-        assertThat(expected.getHideInSettingsInQuietMode())
-                .isEqualTo(actual.getHideInSettingsInQuietMode());
+        assertThat(expected.getShowInSharingSurfaces()).isEqualTo(
+                actual.getShowInSharingSurfaces());
+        assertThat(expected.getShowInQuietMode())
+                .isEqualTo(actual.getShowInQuietMode());
         assertThat(expected.getInheritDevicePolicy()).isEqualTo(actual.getInheritDevicePolicy());
         assertThat(expected.getUseParentsContacts()).isEqualTo(actual.getUseParentsContacts());
         assertThat(expected.getCrossProfileIntentFilterAccessControl())
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
index 48eb5c6..77f6939 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerServiceUserTypeTest.java
@@ -29,6 +29,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNotSame;
 import static org.junit.Assert.assertTrue;
@@ -91,7 +92,8 @@
                 .setCredentialShareableWithParent(false)
                 .setAuthAlwaysRequiredToDisableQuietMode(true)
                 .setShowInSettings(900)
-                .setHideInSettingsInQuietMode(true)
+                .setShowInSharingSurfaces(20)
+                .setShowInQuietMode(30)
                 .setInheritDevicePolicy(340)
                 .setDeleteAppWithParent(true)
                 .setAlwaysVisible(true);
@@ -107,9 +109,9 @@
                 .setIconBadge(28)
                 .setBadgePlain(29)
                 .setBadgeNoBackground(30)
-                .setLabel(31)
                 .setMaxAllowedPerParent(32)
                 .setStatusBarIcon(33)
+                .setLabels(34, 35, 36)
                 .setDefaultRestrictions(restrictions)
                 .setDefaultSystemSettings(systemSettings)
                 .setDefaultSecureSettings(secureSettings)
@@ -124,9 +126,11 @@
         assertEquals(28, type.getIconBadge());
         assertEquals(29, type.getBadgePlain());
         assertEquals(30, type.getBadgeNoBackground());
-        assertEquals(31, type.getLabel());
         assertEquals(32, type.getMaxAllowedPerParent());
         assertEquals(33, type.getStatusBarIcon());
+        assertEquals(34, type.getLabel(0));
+        assertEquals(35, type.getLabel(1));
+        assertEquals(36, type.getLabel(2));
 
         assertTrue(UserRestrictionsUtils.areEqual(restrictions, type.getDefaultRestrictions()));
         assertNotSame(restrictions, type.getDefaultRestrictions());
@@ -164,7 +168,9 @@
         assertTrue(type.getDefaultUserPropertiesReference()
                 .isAuthAlwaysRequiredToDisableQuietMode());
         assertEquals(900, type.getDefaultUserPropertiesReference().getShowInSettings());
-        assertTrue(type.getDefaultUserPropertiesReference().getHideInSettingsInQuietMode());
+        assertEquals(20, type.getDefaultUserPropertiesReference().getShowInSharingSurfaces());
+        assertEquals(30,
+                type.getDefaultUserPropertiesReference().getShowInQuietMode());
         assertEquals(340, type.getDefaultUserPropertiesReference()
                 .getInheritDevicePolicy());
         assertTrue(type.getDefaultUserPropertiesReference().getDeleteAppWithParent());
@@ -203,7 +209,7 @@
         assertEquals(Resources.ID_NULL, type.getStatusBarIcon());
         assertEquals(Resources.ID_NULL, type.getBadgeLabel(0));
         assertEquals(Resources.ID_NULL, type.getBadgeColor(0));
-        assertEquals(Resources.ID_NULL, type.getLabel());
+        assertEquals(Resources.ID_NULL, type.getLabel(0));
         assertTrue(type.getDefaultRestrictions().isEmpty());
         assertTrue(type.getDefaultSystemSettings().isEmpty());
         assertTrue(type.getDefaultSecureSettings().isEmpty());
@@ -222,7 +228,9 @@
         assertFalse(props.isCredentialShareableWithParent());
         assertFalse(props.getDeleteAppWithParent());
         assertFalse(props.getAlwaysVisible());
-        assertFalse(props.getHideInSettingsInQuietMode());
+        assertEquals(UserProperties.SHOW_IN_LAUNCHER_SEPARATE, props.getShowInSharingSurfaces());
+        assertEquals(UserProperties.SHOW_IN_QUIET_MODE_PAUSED,
+                props.getShowInQuietMode());
 
         assertFalse(type.hasBadge());
     }
@@ -311,8 +319,9 @@
                 .setCredentialShareableWithParent(true)
                 .setAuthAlwaysRequiredToDisableQuietMode(false)
                 .setShowInSettings(20)
-                .setHideInSettingsInQuietMode(false)
                 .setInheritDevicePolicy(21)
+                .setShowInSharingSurfaces(22)
+                .setShowInQuietMode(24)
                 .setDeleteAppWithParent(true)
                 .setAlwaysVisible(false);
 
@@ -354,9 +363,11 @@
         assertFalse(aospType.getDefaultUserPropertiesReference()
                 .isAuthAlwaysRequiredToDisableQuietMode());
         assertEquals(20, aospType.getDefaultUserPropertiesReference().getShowInSettings());
-        assertFalse(aospType.getDefaultUserPropertiesReference().getHideInSettingsInQuietMode());
         assertEquals(21, aospType.getDefaultUserPropertiesReference()
                 .getInheritDevicePolicy());
+        assertEquals(22, aospType.getDefaultUserPropertiesReference().getShowInSharingSurfaces());
+        assertEquals(24,
+                aospType.getDefaultUserPropertiesReference().getShowInQuietMode());
         assertTrue(aospType.getDefaultUserPropertiesReference().getDeleteAppWithParent());
         assertFalse(aospType.getDefaultUserPropertiesReference().getAlwaysVisible());
 
@@ -403,7 +414,10 @@
         assertTrue(aospType.getDefaultUserPropertiesReference()
                 .isAuthAlwaysRequiredToDisableQuietMode());
         assertEquals(23, aospType.getDefaultUserPropertiesReference().getShowInSettings());
-        assertTrue(aospType.getDefaultUserPropertiesReference().getHideInSettingsInQuietMode());
+        assertEquals(22,
+                aospType.getDefaultUserPropertiesReference().getShowInSharingSurfaces());
+        assertEquals(24,
+                aospType.getDefaultUserPropertiesReference().getShowInQuietMode());
         assertEquals(450, aospType.getDefaultUserPropertiesReference()
                 .getInheritDevicePolicy());
         assertFalse(aospType.getDefaultUserPropertiesReference().getDeleteAppWithParent());
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
index 2b6d8ed..c7d80ed 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
@@ -343,6 +343,8 @@
         assertThat(mainUserId).isEqualTo(parentProfileInfo.id);
         removeUser(userInfo.id);
         assertThat(mUserManager.getProfileParent(mainUserId)).isNull();
+        assertThat(mUserManager.getProfileLabel()).isEqualTo(
+                Resources.getSystem().getString(userTypeDetails.getLabel(0)));
     }
 
     @MediumTest
diff --git a/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java b/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java
index ebe45a6..d000605 100644
--- a/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.webkit;
 
+import static android.webkit.Flags.updateServiceV2;
+
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -25,6 +27,9 @@
 import android.content.pm.Signature;
 import android.os.Build;
 import android.os.Bundle;
+import android.platform.test.annotations.RequiresFlagsDisabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.test.suitebuilder.annotation.MediumTest;
 import android.util.Base64;
 import android.webkit.UserPackage;
@@ -35,6 +40,7 @@
 import androidx.test.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentMatcher;
@@ -55,11 +61,14 @@
 public class WebViewUpdateServiceTest {
     private final static String TAG = WebViewUpdateServiceTest.class.getSimpleName();
 
-    private WebViewUpdateServiceImpl mWebViewUpdateServiceImpl;
+    private WebViewUpdateServiceInterface mWebViewUpdateServiceImpl;
     private TestSystemImpl mTestSystemImpl;
 
     private static final String WEBVIEW_LIBRARY_FLAG = "com.android.webview.WebViewLibrary";
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     /**
      * Creates a new instance.
      */
@@ -92,8 +101,13 @@
         TestSystemImpl testing = new TestSystemImpl(packages, numRelros, isDebuggable,
                 multiProcessDefault);
         mTestSystemImpl = Mockito.spy(testing);
-        mWebViewUpdateServiceImpl =
-            new WebViewUpdateServiceImpl(null /*Context*/, mTestSystemImpl);
+        if (updateServiceV2()) {
+            mWebViewUpdateServiceImpl =
+                    new WebViewUpdateServiceImpl2(null /*Context*/, mTestSystemImpl);
+        } else {
+            mWebViewUpdateServiceImpl =
+                    new WebViewUpdateServiceImpl(null /*Context*/, mTestSystemImpl);
+        }
     }
 
     private void setEnabledAndValidPackageInfos(WebViewProviderInfo[] providers) {
@@ -1310,11 +1324,13 @@
     }
 
     @Test
+    @RequiresFlagsDisabled("android.webkit.update_service_v2")
     public void testMultiProcessEnabledByDefault() {
         testMultiProcessByDefault(true /* enabledByDefault */);
     }
 
     @Test
+    @RequiresFlagsDisabled("android.webkit.update_service_v2")
     public void testMultiProcessDisabledByDefault() {
         testMultiProcessByDefault(false /* enabledByDefault */);
     }
@@ -1344,6 +1360,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled("android.webkit.update_service_v2")
     public void testMultiProcessEnabledByDefaultWithSettingsValue() {
         testMultiProcessByDefaultWithSettingsValue(
                 true /* enabledByDefault */, Integer.MIN_VALUE, false /* expectEnabled */);
@@ -1356,6 +1373,7 @@
     }
 
     @Test
+    @RequiresFlagsDisabled("android.webkit.update_service_v2")
     public void testMultiProcessDisabledByDefaultWithSettingsValue() {
         testMultiProcessByDefaultWithSettingsValue(
                 false /* enabledByDefault */, Integer.MIN_VALUE, false /* expectEnabled */);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index b45dcd4..09ffe71 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -241,7 +241,6 @@
 
 import com.android.internal.R;
 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
-import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.Flag;
 import com.android.internal.config.sysui.TestableFlagResolver;
 import com.android.internal.logging.InstanceIdSequence;
 import com.android.internal.logging.InstanceIdSequenceFake;
@@ -5296,7 +5295,7 @@
                 new Intent(Intent.ACTION_LOCALE_CHANGED));
 
         verify(mZenModeHelper, times(1)).updateDefaultZenRules(
-                anyInt(), anyBoolean());
+                anyInt());
     }
 
     @Test
@@ -8752,7 +8751,7 @@
 
         // verify that zen mode helper gets passed in a package name of "android"
         verify(mockZenModeHelper).addAutomaticZenRule(eq("android"), eq(rule), anyString(),
-                anyInt(), eq(true)); // system call counts as "is system or system ui"
+                anyInt(), eq(ZenModeHelper.FROM_SYSTEM_OR_SYSTEMUI)); // system call
     }
 
     @Test
@@ -8774,7 +8773,7 @@
 
         // verify that zen mode helper gets passed in a package name of "android"
         verify(mockZenModeHelper).addAutomaticZenRule(eq("android"), eq(rule), anyString(),
-                anyInt(),  eq(true));  // system call counts as "system or system ui"
+                anyInt(), eq(ZenModeHelper.FROM_SYSTEM_OR_SYSTEMUI));  // system call
     }
 
     @Test
@@ -8795,7 +8794,7 @@
         // verify that zen mode helper gets passed in the package name from the arg, not the owner
         verify(mockZenModeHelper).addAutomaticZenRule(
                 eq("another.package"), eq(rule), anyString(), anyInt(),
-                eq(false));  // doesn't count as a system/systemui call
+                eq(ZenModeHelper.FROM_APP));  // doesn't count as a system/systemui call
     }
 
     @Test
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index 0313aaa..97b6b98 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -53,6 +53,9 @@
 import static com.android.os.dnd.DNDProtoEnums.ROOT_CONFIG;
 import static com.android.os.dnd.DNDProtoEnums.STATE_ALLOW;
 import static com.android.os.dnd.DNDProtoEnums.STATE_DISALLOW;
+import static com.android.server.notification.ZenModeHelper.FROM_APP;
+import static com.android.server.notification.ZenModeHelper.FROM_SYSTEM_OR_SYSTEMUI;
+import static com.android.server.notification.ZenModeHelper.FROM_USER;
 import static com.android.server.notification.ZenModeHelper.RULE_LIMIT_PER_PACKAGE;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -108,6 +111,7 @@
 import android.provider.Settings;
 import android.provider.Settings.Global;
 import android.service.notification.Condition;
+import android.service.notification.ZenDeviceEffects;
 import android.service.notification.ZenModeConfig;
 import android.service.notification.ZenModeConfig.ScheduleInfo;
 import android.service.notification.ZenModeConfig.ZenRule;
@@ -1645,8 +1649,7 @@
         ZenModeConfig config = new ZenModeConfig();
         config.automaticRules = new ArrayMap<>();
         mZenModeHelper.mConfig = config;
-        mZenModeHelper.updateDefaultZenRules(
-                Process.SYSTEM_UID, true); // shouldn't throw null pointer
+        mZenModeHelper.updateDefaultZenRules(Process.SYSTEM_UID); // shouldn't throw null pointer
         mZenModeHelper.pullRules(events); // shouldn't throw null pointer
     }
 
@@ -1671,7 +1674,7 @@
         autoRules.put(SCHEDULE_DEFAULT_RULE_ID, updatedDefaultRule);
         mZenModeHelper.mConfig.automaticRules = autoRules;
 
-        mZenModeHelper.updateDefaultZenRules(Process.SYSTEM_UID, true);
+        mZenModeHelper.updateDefaultZenRules(Process.SYSTEM_UID);
         assertEquals(updatedDefaultRule,
                 mZenModeHelper.mConfig.automaticRules.get(SCHEDULE_DEFAULT_RULE_ID));
     }
@@ -1697,7 +1700,7 @@
         autoRules.put(SCHEDULE_DEFAULT_RULE_ID, updatedDefaultRule);
         mZenModeHelper.mConfig.automaticRules = autoRules;
 
-        mZenModeHelper.updateDefaultZenRules(Process.SYSTEM_UID, true);
+        mZenModeHelper.updateDefaultZenRules(Process.SYSTEM_UID);
         assertEquals(updatedDefaultRule,
                 mZenModeHelper.mConfig.automaticRules.get(SCHEDULE_DEFAULT_RULE_ID));
     }
@@ -1724,7 +1727,7 @@
         autoRules.put(SCHEDULE_DEFAULT_RULE_ID, customDefaultRule);
         mZenModeHelper.mConfig.automaticRules = autoRules;
 
-        mZenModeHelper.updateDefaultZenRules(Process.SYSTEM_UID, true);
+        mZenModeHelper.updateDefaultZenRules(Process.SYSTEM_UID);
         ZenModeConfig.ZenRule ruleAfterUpdating =
                 mZenModeHelper.mConfig.automaticRules.get(SCHEDULE_DEFAULT_RULE_ID);
         assertEquals(customDefaultRule.enabled, ruleAfterUpdating.enabled);
@@ -1748,7 +1751,7 @@
             // We need the package name to be something that's not "android" so there aren't any
             // existing rules under that package.
             String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, "test",
-                    CUSTOM_PKG_UID, false);
+                    CUSTOM_PKG_UID, FROM_APP);
             assertNotNull(id);
         }
         try {
@@ -1759,7 +1762,7 @@
                     new ZenPolicy.Builder().build(),
                     NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
             String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, "test",
-                    CUSTOM_PKG_UID, false);
+                    CUSTOM_PKG_UID, FROM_APP);
             fail("allowed too many rules to be created");
         } catch (IllegalArgumentException e) {
             // yay
@@ -1780,7 +1783,7 @@
                     new ZenPolicy.Builder().build(),
                     NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
             String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, "test",
-                    CUSTOM_PKG_UID, false);
+                    CUSTOM_PKG_UID, FROM_APP);
             assertNotNull(id);
         }
         try {
@@ -1791,7 +1794,7 @@
                     new ZenPolicy.Builder().build(),
                     NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
             String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, "test",
-                    CUSTOM_PKG_UID, false);
+                    CUSTOM_PKG_UID, FROM_APP);
             fail("allowed too many rules to be created");
         } catch (IllegalArgumentException e) {
             // yay
@@ -1812,7 +1815,7 @@
                     new ZenPolicy.Builder().build(),
                     NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
             String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, "test",
-                    CUSTOM_PKG_UID, false);
+                    CUSTOM_PKG_UID, FROM_APP);
             assertNotNull(id);
         }
         try {
@@ -1823,7 +1826,7 @@
                     new ZenPolicy.Builder().build(),
                     NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
             String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, "test",
-                    CUSTOM_PKG_UID, false);
+                    CUSTOM_PKG_UID, FROM_APP);
             fail("allowed too many rules to be created");
         } catch (IllegalArgumentException e) {
             // yay
@@ -1839,7 +1842,7 @@
                 new ZenPolicy.Builder().build(),
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
         String id = mZenModeHelper.addAutomaticZenRule("android", zenRule, "test",
-                Process.SYSTEM_UID, true);
+                Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
 
         assertTrue(id != null);
         ZenModeConfig.ZenRule ruleInConfig = mZenModeHelper.mConfig.automaticRules.get(id);
@@ -1860,7 +1863,7 @@
                 ZenModeConfig.toScheduleConditionId(new ScheduleInfo()),
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
         String id = mZenModeHelper.addAutomaticZenRule("android", zenRule, "test",
-                Process.SYSTEM_UID, true);
+                Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
 
         assertTrue(id != null);
         ZenModeConfig.ZenRule ruleInConfig = mZenModeHelper.mConfig.automaticRules.get(id);
@@ -1884,7 +1887,7 @@
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
 
         String id = mZenModeHelper.addAutomaticZenRule(null, zenRule, "test",
-                CUSTOM_PKG_UID, false);
+                CUSTOM_PKG_UID, FROM_APP);
         mZenModeHelper.setAutomaticZenRuleState(zenRule.getConditionId(),
                 new Condition(zenRule.getConditionId(), "", STATE_TRUE),
                 CUSTOM_PKG_UID, false);
@@ -1903,7 +1906,7 @@
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
 
         String id = mZenModeHelper.addAutomaticZenRule(null, zenRule, "test",
-                CUSTOM_PKG_UID, false);
+                CUSTOM_PKG_UID, FROM_APP);
 
         AutomaticZenRule zenRule2 = new AutomaticZenRule("NEW",
                 null,
@@ -1912,7 +1915,7 @@
                 new ZenPolicy.Builder().build(),
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
 
-        mZenModeHelper.updateAutomaticZenRule(id, zenRule2, "", CUSTOM_PKG_UID, false);
+        mZenModeHelper.updateAutomaticZenRule(id, zenRule2, "", CUSTOM_PKG_UID, FROM_APP);
 
         ZenModeConfig.ZenRule ruleInConfig = mZenModeHelper.mConfig.automaticRules.get(id);
         assertEquals("NEW", ruleInConfig.name);
@@ -1928,7 +1931,7 @@
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
 
         String id = mZenModeHelper.addAutomaticZenRule(null, zenRule, "test",
-                CUSTOM_PKG_UID, false);
+                CUSTOM_PKG_UID, FROM_APP);
 
         assertTrue(id != null);
         ZenModeConfig.ZenRule ruleInConfig = mZenModeHelper.mConfig.automaticRules.get(id);
@@ -1948,7 +1951,7 @@
                 new ZenPolicy.Builder().build(),
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
         String id = mZenModeHelper.addAutomaticZenRule(null, zenRule, "test",
-                CUSTOM_PKG_UID, false);
+                CUSTOM_PKG_UID, FROM_APP);
 
         assertTrue(id != null);
         ZenModeConfig.ZenRule ruleInConfig = mZenModeHelper.mConfig.automaticRules.get(id);
@@ -1972,13 +1975,13 @@
                 sharedUri,
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
         String id = mZenModeHelper.addAutomaticZenRule("android", zenRule, "test",
-                Process.SYSTEM_UID, true);
+                Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
         AutomaticZenRule zenRule2 = new AutomaticZenRule("name2",
                 new ComponentName("android", "ScheduleConditionProvider"),
                 sharedUri,
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
         String id2 = mZenModeHelper.addAutomaticZenRule("android", zenRule2, "test",
-                Process.SYSTEM_UID, true);
+                Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
 
         Condition condition = new Condition(sharedUri, "", STATE_TRUE);
         mZenModeHelper.setAutomaticZenRuleState(sharedUri, condition, Process.SYSTEM_UID, true);
@@ -2010,6 +2013,182 @@
     }
 
     @Test
+    public void addAutomaticZenRule_fromApp_ignoresHiddenEffects() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+
+        ZenDeviceEffects zde = new ZenDeviceEffects.Builder()
+                .setShouldDisplayGrayscale(true)
+                .setShouldSuppressAmbientDisplay(true)
+                .setShouldDimWallpaper(true)
+                .setShouldUseNightMode(true)
+                .setShouldDisableAutoBrightness(true)
+                .setShouldDisableTapToWake(true)
+                .setShouldDisableTiltToWake(true)
+                .setShouldDisableTouch(true)
+                .setShouldMinimizeRadioUsage(true)
+                .setShouldMaximizeDoze(true)
+                .build();
+
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                new AutomaticZenRule.Builder("Rule", CONDITION_ID)
+                        .setOwner(OWNER)
+                        .setDeviceEffects(zde)
+                        .build(),
+                "reasons", 0, FROM_APP);
+
+        AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId);
+        assertThat(savedRule.getDeviceEffects()).isEqualTo(
+                new ZenDeviceEffects.Builder()
+                        .setShouldDisplayGrayscale(true)
+                        .setShouldSuppressAmbientDisplay(true)
+                        .setShouldDimWallpaper(true)
+                        .setShouldUseNightMode(true)
+                        .build());
+    }
+
+    @Test
+    public void addAutomaticZenRule_fromSystem_respectsHiddenEffects() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+
+        ZenDeviceEffects zde = new ZenDeviceEffects.Builder()
+                .setShouldDisplayGrayscale(true)
+                .setShouldSuppressAmbientDisplay(true)
+                .setShouldDimWallpaper(true)
+                .setShouldUseNightMode(true)
+                .setShouldDisableAutoBrightness(true)
+                .setShouldDisableTapToWake(true)
+                .setShouldDisableTiltToWake(true)
+                .setShouldDisableTouch(true)
+                .setShouldMinimizeRadioUsage(true)
+                .setShouldMaximizeDoze(true)
+                .build();
+
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                new AutomaticZenRule.Builder("Rule", CONDITION_ID)
+                        .setOwner(OWNER)
+                        .setDeviceEffects(zde)
+                        .build(),
+                "reasons", 0, FROM_SYSTEM_OR_SYSTEMUI);
+
+        AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId);
+        assertThat(savedRule.getDeviceEffects()).isEqualTo(zde);
+    }
+
+    @Test
+    public void addAutomaticZenRule_fromUser_respectsHiddenEffects() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+
+        ZenDeviceEffects zde = new ZenDeviceEffects.Builder()
+                .setShouldDisplayGrayscale(true)
+                .setShouldSuppressAmbientDisplay(true)
+                .setShouldDimWallpaper(true)
+                .setShouldUseNightMode(true)
+                .setShouldDisableAutoBrightness(true)
+                .setShouldDisableTapToWake(true)
+                .setShouldDisableTiltToWake(true)
+                .setShouldDisableTouch(true)
+                .setShouldMinimizeRadioUsage(true)
+                .setShouldMaximizeDoze(true)
+                .build();
+
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                new AutomaticZenRule.Builder("Rule", CONDITION_ID)
+                        .setOwner(OWNER)
+                        .setDeviceEffects(zde)
+                        .build(),
+                "reasons", 0, FROM_USER);
+
+        AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId);
+        assertThat(savedRule.getDeviceEffects()).isEqualTo(zde);
+    }
+
+    @Test
+    public void updateAutomaticZenRule_fromApp_preservesPreviousHiddenEffects() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+        ZenDeviceEffects original = new ZenDeviceEffects.Builder()
+                .setShouldDisableTapToWake(true)
+                .build();
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                new AutomaticZenRule.Builder("Rule", CONDITION_ID)
+                        .setOwner(OWNER)
+                        .setDeviceEffects(original)
+                        .build(),
+                "reasons", 0, FROM_SYSTEM_OR_SYSTEMUI);
+
+        ZenDeviceEffects updateFromApp = new ZenDeviceEffects.Builder()
+                .setShouldUseNightMode(true) // Good
+                .setShouldMaximizeDoze(true) // Bad
+                .build();
+        mZenModeHelper.updateAutomaticZenRule(ruleId,
+                new AutomaticZenRule.Builder("Rule", CONDITION_ID)
+                        .setOwner(OWNER)
+                        .setDeviceEffects(updateFromApp)
+                        .build(),
+                "reasons", 0, FROM_APP);
+
+        AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId);
+        assertThat(savedRule.getDeviceEffects()).isEqualTo(
+                new ZenDeviceEffects.Builder()
+                        .setShouldUseNightMode(true) // From update.
+                        .setShouldDisableTapToWake(true) // From original.
+                        .build());
+    }
+
+    @Test
+    public void updateAutomaticZenRule_fromSystem_updatesHiddenEffects() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+        ZenDeviceEffects original = new ZenDeviceEffects.Builder()
+                .setShouldDisableTapToWake(true)
+                .build();
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                new AutomaticZenRule.Builder("Rule", CONDITION_ID)
+                        .setOwner(OWNER)
+                        .setDeviceEffects(original)
+                        .build(),
+                "reasons", 0, FROM_SYSTEM_OR_SYSTEMUI);
+
+        ZenDeviceEffects updateFromSystem = new ZenDeviceEffects.Builder()
+                .setShouldUseNightMode(true) // Good
+                .setShouldMaximizeDoze(true) // Also good
+                .build();
+        mZenModeHelper.updateAutomaticZenRule(ruleId,
+                new AutomaticZenRule.Builder("Rule", CONDITION_ID)
+                        .setDeviceEffects(updateFromSystem)
+                        .build(),
+                "reasons", 0, FROM_SYSTEM_OR_SYSTEMUI);
+
+        AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId);
+        assertThat(savedRule.getDeviceEffects()).isEqualTo(updateFromSystem);
+    }
+
+    @Test
+    public void updateAutomaticZenRule_fromUser_updatesHiddenEffects() {
+        mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
+        ZenDeviceEffects original = new ZenDeviceEffects.Builder()
+                .setShouldDisableTapToWake(true)
+                .build();
+        String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
+                new AutomaticZenRule.Builder("Rule", CONDITION_ID)
+                        .setOwner(OWNER)
+                        .setDeviceEffects(original)
+                        .build(),
+                "reasons", 0, FROM_SYSTEM_OR_SYSTEMUI);
+
+        ZenDeviceEffects updateFromUser = new ZenDeviceEffects.Builder()
+                .setShouldUseNightMode(true) // Good
+                .setShouldMaximizeDoze(true) // Also good
+                .build();
+        mZenModeHelper.updateAutomaticZenRule(ruleId,
+                new AutomaticZenRule.Builder("Rule", CONDITION_ID)
+                        .setDeviceEffects(updateFromUser)
+                        .build(),
+                "reasons", 0, FROM_USER);
+
+        AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId);
+        assertThat(savedRule.getDeviceEffects()).isEqualTo(updateFromUser);
+    }
+
+    @Test
     public void testSetManualZenMode() {
         mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API);
         setupZenConfig();
@@ -2119,7 +2298,7 @@
                 null,
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
         String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule,
-                "test", Process.SYSTEM_UID, true);
+                "test", Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
 
         // Event 1: Mimic the rule coming on automatically by setting the Condition to STATE_TRUE
         mZenModeHelper.setAutomaticZenRuleState(id,
@@ -2128,7 +2307,8 @@
 
         // Event 2: "User" turns off the automatic rule (sets it to not enabled)
         zenRule.setEnabled(false);
-        mZenModeHelper.updateAutomaticZenRule(id, zenRule, "", Process.SYSTEM_UID, true);
+        mZenModeHelper.updateAutomaticZenRule(id, zenRule, "", Process.SYSTEM_UID,
+                FROM_SYSTEM_OR_SYSTEMUI);
 
         // Add a new system rule
         AutomaticZenRule systemRule = new AutomaticZenRule("systemRule",
@@ -2138,7 +2318,7 @@
                 null,
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
         String systemId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), systemRule,
-                "test", Process.SYSTEM_UID, true);
+                "test", Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
 
         // Event 3: turn on the system rule
         mZenModeHelper.setAutomaticZenRuleState(systemId,
@@ -2270,7 +2450,7 @@
                 null,
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
         String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, "test",
-                Process.SYSTEM_UID, true);
+                Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
 
         // Rule 2, same as rule 1
         AutomaticZenRule zenRule2 = new AutomaticZenRule("name2",
@@ -2280,7 +2460,7 @@
                 null,
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
         String id2 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule2, "test",
-                Process.SYSTEM_UID, true);
+                Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
 
         // Rule 3, has stricter settings than the default settings
         ZenModeConfig ruleConfig = mZenModeHelper.mConfig.copy();
@@ -2294,7 +2474,7 @@
                 ruleConfig.toZenPolicy(),
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
         String id3 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule3, "test",
-                Process.SYSTEM_UID, true);
+                Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
 
         // First: turn on rule 1
         mZenModeHelper.setAutomaticZenRuleState(id,
@@ -2394,7 +2574,7 @@
                 null,
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
         String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, "test",
-                Process.SYSTEM_UID, true);
+                Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
 
         // Rule 2, same as rule 1 but owned by the system
         AutomaticZenRule zenRule2 = new AutomaticZenRule("name2",
@@ -2404,7 +2584,7 @@
                 null,
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
         String id2 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule2, "test",
-                Process.SYSTEM_UID, true);
+                Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
 
         // Turn on rule 1; call looks like it's from the system. Because setting a condition is
         // typically an automatic (non-user-initiated) action, expect the calling UID to be
@@ -2422,7 +2602,8 @@
         // Disable rule 1. Because this looks like a user action, the UID should not be modified
         // from the system-provided one.
         zenRule.setEnabled(false);
-        mZenModeHelper.updateAutomaticZenRule(id, zenRule, "", Process.SYSTEM_UID, true);
+        mZenModeHelper.updateAutomaticZenRule(id, zenRule, "", Process.SYSTEM_UID,
+                FROM_SYSTEM_OR_SYSTEMUI);
 
         // Add a manual rule. Any manual rule changes should not get calling uids reassigned.
         mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, null, "",
@@ -2553,7 +2734,7 @@
                 null,
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
         String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, "test",
-                Process.SYSTEM_UID, true);
+                Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
 
         // enable the rule
         mZenModeHelper.setAutomaticZenRuleState(id,
@@ -2596,7 +2777,7 @@
                 customPolicy,
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
         String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, "test",
-                Process.SYSTEM_UID, true);
+                Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
 
         // enable the rule; this will update the consolidated policy
         mZenModeHelper.setAutomaticZenRuleState(id,
@@ -2632,7 +2813,7 @@
                 null,
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
         String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, "test",
-                Process.SYSTEM_UID, true);
+                Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
 
         // enable rule 1
         mZenModeHelper.setAutomaticZenRuleState(id,
@@ -2656,7 +2837,7 @@
                 customPolicy,
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
         String id2 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule2,
-                "test", Process.SYSTEM_UID, true);
+                "test", Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
 
         // enable rule 2; this will update the consolidated policy
         mZenModeHelper.setAutomaticZenRuleState(id2,
@@ -2694,7 +2875,7 @@
                 customPolicy,
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
         String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, "test",
-                Process.SYSTEM_UID, true);
+                Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
 
         // enable the rule; this will update the consolidated policy
         mZenModeHelper.setAutomaticZenRuleState(id,
@@ -2716,7 +2897,7 @@
                 strictPolicy,
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
         String id2 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule2,
-                "test", Process.SYSTEM_UID, true);
+                "test", Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
 
         // enable rule 2; this will update the consolidated policy
         mZenModeHelper.setAutomaticZenRuleState(id2,
@@ -2784,7 +2965,7 @@
                 null,
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
         final String createdId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
-                zenRule, "test", Process.SYSTEM_UID, true);
+                zenRule, "test", Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
 
         CountDownLatch latch = new CountDownLatch(1);
         final int[] actualStatus = new int[1];
@@ -2800,7 +2981,8 @@
         mZenModeHelper.addCallback(callback);
 
         zenRule.setEnabled(false);
-        mZenModeHelper.updateAutomaticZenRule(createdId, zenRule, "", Process.SYSTEM_UID, true);
+        mZenModeHelper.updateAutomaticZenRule(createdId, zenRule, "", Process.SYSTEM_UID,
+                FROM_SYSTEM_OR_SYSTEMUI);
 
         assertTrue(latch.await(500, TimeUnit.MILLISECONDS));
         assertEquals(AUTOMATIC_RULE_STATUS_DISABLED, actualStatus[0]);
@@ -2818,7 +3000,7 @@
                 null,
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, false);
         final String createdId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
-                zenRule, "test", Process.SYSTEM_UID, true);
+                zenRule, "test", Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
 
         CountDownLatch latch = new CountDownLatch(1);
         final int[] actualStatus = new int[1];
@@ -2834,7 +3016,8 @@
         mZenModeHelper.addCallback(callback);
 
         zenRule.setEnabled(true);
-        mZenModeHelper.updateAutomaticZenRule(createdId, zenRule, "", Process.SYSTEM_UID, true);
+        mZenModeHelper.updateAutomaticZenRule(createdId, zenRule, "", Process.SYSTEM_UID,
+                FROM_SYSTEM_OR_SYSTEMUI);
 
         assertTrue(latch.await(500, TimeUnit.MILLISECONDS));
         assertEquals(AUTOMATIC_RULE_STATUS_ENABLED, actualStatus[0]);
@@ -2853,7 +3036,7 @@
                 null,
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
         final String createdId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
-                zenRule, "test", Process.SYSTEM_UID, true);
+                zenRule, "test", Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
 
         CountDownLatch latch = new CountDownLatch(1);
         final int[] actualStatus = new int[1];
@@ -2889,7 +3072,7 @@
                 null,
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
         final String createdId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
-                zenRule, "test", Process.SYSTEM_UID, true);
+                zenRule, "test", Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
 
         CountDownLatch latch = new CountDownLatch(1);
         final int[] actualStatus = new int[2];
@@ -2929,7 +3112,7 @@
                 null,
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
         final String createdId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
-                zenRule, "test", Process.SYSTEM_UID, true);
+                zenRule, "test", Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
 
         CountDownLatch latch = new CountDownLatch(1);
         final int[] actualStatus = new int[2];
@@ -2969,7 +3152,7 @@
                 null,
                 NotificationManager.INTERRUPTION_FILTER_PRIORITY, true);
         final String createdId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(),
-                zenRule, "test", Process.SYSTEM_UID, true);
+                zenRule, "test", Process.SYSTEM_UID, FROM_SYSTEM_OR_SYSTEMUI);
 
         // Event 1: Mimic the rule coming on automatically by setting the Condition to STATE_TRUE
         mZenModeHelper.setAutomaticZenRuleState(createdId,
@@ -2982,7 +3165,8 @@
 
         // Event 3: "User" turns off the automatic rule (sets it to not enabled)
         zenRule.setEnabled(false);
-        mZenModeHelper.updateAutomaticZenRule(createdId, zenRule, "", Process.SYSTEM_UID, true);
+        mZenModeHelper.updateAutomaticZenRule(createdId, zenRule, "", Process.SYSTEM_UID,
+                FROM_SYSTEM_OR_SYSTEMUI);
 
         assertEquals(false, mZenModeHelper.mConfig.automaticRules.get(createdId).snoozing);
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index 786432a..f2e54bc 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -1175,10 +1175,12 @@
     @Test
     public void testFinishActivityIfPossible_nonVisibleNoAppTransition() {
         registerTestTransitionPlayer();
+        spyOn(mRootWindowContainer.mTransitionController);
+        final ActivityRecord bottomActivity = createActivityWithTask();
+        bottomActivity.setVisibility(false);
+        bottomActivity.setState(STOPPED, "test");
+        bottomActivity.mLastSurfaceShowing = false;
         final ActivityRecord activity = createActivityWithTask();
-        // Put an activity on top of test activity to make it invisible and prevent us from
-        // accidentally resuming the topmost one again.
-        new ActivityBuilder(mAtm).build();
         activity.setVisibleRequested(false);
         activity.setState(STOPPED, "test");
 
@@ -1186,6 +1188,14 @@
 
         verify(activity.mDisplayContent, never()).prepareAppTransition(eq(TRANSIT_CLOSE));
         assertFalse(activity.inTransition());
+
+        // finishIfPossible -> completeFinishing -> addToFinishingAndWaitForIdle
+        // -> resumeFocusedTasksTopActivities
+        assertTrue(bottomActivity.isState(RESUMED));
+        assertTrue(bottomActivity.isVisible());
+        verify(mRootWindowContainer.mTransitionController).onVisibleWithoutCollectingTransition(
+                eq(bottomActivity), any());
+        assertTrue(bottomActivity.mLastSurfaceShowing);
     }
 
     /**
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
index b44d52e..2034751 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java
@@ -201,6 +201,33 @@
         verify(taskChangeNotifier, never()).notifyActivityDismissingDockedRootTask();
     }
 
+    /** Verifies that the activity can be destroying after removing task. */
+    @Test
+    public void testRemoveTask() {
+        final ActivityRecord activity1 = new ActivityBuilder(mAtm).setCreateTask(true).build();
+        activity1.setVisible(false);
+        activity1.finishing = true;
+        activity1.setState(ActivityRecord.State.STOPPING, "test");
+        activity1.addToStopping(false /* scheduleIdle */, false /* idleDelayed */, "test");
+        final ActivityRecord activity2 = new ActivityBuilder(mAtm).setCreateTask(true).build();
+        activity2.setState(ActivityRecord.State.RESUMED, "test");
+        // The state can happen from ActivityRecord#makeInvisible.
+        activity2.addToStopping(false /* scheduleIdle */, false /* idleDelayed */, "test");
+        mSupervisor.removeTask(activity1.getTask(), true /* killProcess */,
+                true /* removeFromRecents */, "testRemoveTask");
+        mSupervisor.removeTask(activity2.getTask(), true /* killProcess */,
+                true /* removeFromRecents */, "testRemoveTask");
+
+        assertEquals(ActivityRecord.State.DESTROYING, activity2.getState());
+        assertEquals(ActivityRecord.State.STOPPING, activity1.getState());
+        assertTrue(mSupervisor.mStoppingActivities.contains(activity1));
+        // Assume that it is called by scheduleIdle from addToStopping. And because
+        // mStoppingActivities remembers the finishing activity, it can continue to destroy.
+        mSupervisor.processStoppingAndFinishingActivities(null /* launchedActivity */,
+                false /* processPausingActivities */, "test");
+        assertEquals(ActivityRecord.State.DESTROYING, activity1.getState());
+    }
+
     /** Ensures that the calling package name passed to client complies with package visibility. */
     @Test
     public void testFilteredReferred() {
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
index c6fa8a1..acce2e2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java
@@ -51,8 +51,6 @@
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
 import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_UNRESTRICTED_GESTURE_EXCLUSION;
-import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN;
-import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
@@ -159,6 +157,10 @@
 import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
 /**
  * Tests for the {@link DisplayContent} class.
@@ -2287,7 +2289,7 @@
                 0 /* userId */);
         dc.mCurrentUniqueDisplayId = mDisplayInfo.uniqueId + "-test";
         // Trigger display changed.
-        dc.onDisplayChanged();
+        updateDisplay(dc);
         // Ensure overridden size and denisty match the most up-to-date values in settings for the
         // display.
         verifySizes(dc, forcedWidth, forcedHeight, forcedDensity);
@@ -2762,26 +2764,6 @@
                 mDisplayContent.getKeepClearAreas());
     }
 
-    @Test
-    public void testMayImeShowOnLaunchingActivity_negativeWhenSoftInputModeHidden() {
-        final ActivityRecord app = createActivityRecord(mDisplayContent);
-        final WindowState appWin = createWindow(null, TYPE_BASE_APPLICATION, app, "appWin");
-        createWindow(null, TYPE_APPLICATION_STARTING, app, "startingWin");
-        app.mStartingData = mock(SnapshotStartingData.class);
-        // Assume the app has shown IME before and warm launching with a snapshot window.
-        doReturn(true).when(app.mStartingData).hasImeSurface();
-
-        // Expect true when this IME focusable activity will show IME during launching.
-        assertTrue(WindowManager.LayoutParams.mayUseInputMethod(appWin.mAttrs.flags));
-        assertTrue(mDisplayContent.mayImeShowOnLaunchingActivity(app));
-
-        // Not expect IME will be shown during launching if the app's softInputMode is hidden.
-        appWin.mAttrs.softInputMode = SOFT_INPUT_STATE_ALWAYS_HIDDEN;
-        assertFalse(mDisplayContent.mayImeShowOnLaunchingActivity(app));
-        appWin.mAttrs.softInputMode = SOFT_INPUT_STATE_HIDDEN;
-        assertFalse(mDisplayContent.mayImeShowOnLaunchingActivity(app));
-    }
-
     private void removeRootTaskTests(Runnable runnable) {
         final TaskDisplayArea taskDisplayArea = mRootWindowContainer.getDefaultTaskDisplayArea();
         final Task rootTask1 = taskDisplayArea.createRootTask(WINDOWING_MODE_FULLSCREEN,
@@ -2852,7 +2834,7 @@
      */
     private DisplayContent createDisplayNoUpdateDisplayInfo() {
         final DisplayContent displayContent = createNewDisplay();
-        doNothing().when(displayContent).updateDisplayInfo();
+        doNothing().when(displayContent).updateDisplayInfo(any());
         return displayContent;
     }
 
@@ -2882,6 +2864,16 @@
         return result;
     }
 
+    private void updateDisplay(DisplayContent displayContent) {
+        CompletableFuture<Object> future = new CompletableFuture<>();
+        displayContent.requestDisplayUpdate(() -> future.complete(new Object()));
+        try {
+            future.get(15, TimeUnit.SECONDS);
+        } catch (InterruptedException | ExecutionException | TimeoutException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
     private void tapOnDisplay(final DisplayContent dc) {
         final DisplayMetrics dm = dc.getDisplayMetrics();
         final float x = dm.widthPixels / 2;
diff --git a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
index 9af5ba5..5738d24 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
@@ -69,6 +69,7 @@
 import android.os.UserHandle;
 import android.provider.DeviceConfig;
 import android.util.Log;
+import android.view.DisplayInfo;
 import android.view.InputChannel;
 import android.view.SurfaceControl;
 
@@ -267,6 +268,12 @@
         // DisplayManagerInternal
         final DisplayManagerInternal dmi = mock(DisplayManagerInternal.class);
         doReturn(dmi).when(() -> LocalServices.getService(eq(DisplayManagerInternal.class)));
+        doAnswer(invocation -> {
+            int displayId = invocation.getArgument(0);
+            DisplayInfo displayInfo = invocation.getArgument(1);
+            mWmService.mRoot.getDisplayContent(displayId).getDisplay().getDisplayInfo(displayInfo);
+            return null;
+        }).when(dmi).getNonOverrideDisplayInfo(anyInt(), any());
 
         // ColorDisplayServiceInternal
         final ColorDisplayService.ColorDisplayServiceInternal cds =
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContextListenerControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContextListenerControllerTests.java
index 2d5b72b..d183cf7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContextListenerControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContextListenerControllerTests.java
@@ -60,6 +60,11 @@
 import org.mockito.Mock;
 import org.mockito.Mockito;
 
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
 /**
  * Build/Install/Run:
  *  atest WmTests:WindowContextListenerControllerTests
@@ -309,7 +314,7 @@
             return null;
         }).when(display).getDisplayInfo(any(DisplayInfo.class));
 
-        mContainer.getDisplayContent().onDisplayChanged();
+        updateDisplay(mContainer.getDisplayContent());
 
         assertThat(mockToken.mConfiguration).isEqualTo(config1);
         assertThat(mockToken.mDisplayId).isEqualTo(mContainer.getDisplayContent().getDisplayId());
@@ -352,4 +357,14 @@
             mRemoved = true;
         }
     }
+
+    private void updateDisplay(DisplayContent displayContent) {
+        CompletableFuture<Object> future = new CompletableFuture<>();
+        displayContent.requestDisplayUpdate(() -> future.complete(new Object()));
+        try {
+            future.get(15, TimeUnit.SECONDS);
+        } catch (InterruptedException | ExecutionException | TimeoutException e) {
+            throw new RuntimeException(e);
+        }
+    }
 }
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index c124079..ede4885 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -8853,6 +8853,24 @@
                 KEY_PREFIX + "epdg_static_address_roaming_string";
 
         /**
+         * Controls if the multiple SA proposals allowed for IKE session to include
+         * all the 3GPP TS 33.210 and RFC 8221 supported cipher suites in multiple
+         * IKE SA proposals as per RFC 7296.
+         */
+        @FlaggedApi(Flags.FLAG_ENABLE_MULTIPLE_SA_PROPOSALS)
+        public static final String KEY_SUPPORTS_IKE_SESSION_MULTIPLE_SA_PROPOSALS_BOOL =
+                KEY_PREFIX + "supports_ike_session_multiple_sa_proposals_bool";
+
+        /**
+         * Controls if the multiple SA proposals allowed for Child session to include
+         * all the 3GPP TS 33.210 and RFC 8221 supported cipher suites in multiple
+         * Child SA proposals as per RFC 7296.
+         */
+        @FlaggedApi(Flags.FLAG_ENABLE_MULTIPLE_SA_PROPOSALS)
+        public static final String KEY_SUPPORTS_CHILD_SESSION_MULTIPLE_SA_PROPOSALS_BOOL =
+                KEY_PREFIX + "supports_child_session_multiple_sa_proposals_bool";
+
+        /**
          * List of supported key sizes for AES Cipher Block Chaining (CBC) encryption mode of child
          * session. Possible values are:
          * {@link android.net.ipsec.ike.SaProposal#KEY_LEN_UNUSED},
@@ -9187,6 +9205,8 @@
             defaults.putInt(KEY_IKE_REKEY_HARD_TIMER_SEC_INT, 14400);
             defaults.putInt(KEY_CHILD_SA_REKEY_SOFT_TIMER_SEC_INT, 3600);
             defaults.putInt(KEY_CHILD_SA_REKEY_HARD_TIMER_SEC_INT, 7200);
+            defaults.putBoolean(KEY_SUPPORTS_IKE_SESSION_MULTIPLE_SA_PROPOSALS_BOOL, false);
+            defaults.putBoolean(KEY_SUPPORTS_CHILD_SESSION_MULTIPLE_SA_PROPOSALS_BOOL, false);
             defaults.putIntArray(
                     KEY_RETRANSMIT_TIMER_MSEC_INT_ARRAY, new int[] {500, 1000, 2000, 4000, 8000});
             defaults.putInt(KEY_DPD_TIMER_SEC_INT, 120);