Merge "Remove unused methods" into main
diff --git a/cmds/gpu_counter_producer/Android.bp b/cmds/gpu_counter_producer/Android.bp
index 2232345..d645d06 100644
--- a/cmds/gpu_counter_producer/Android.bp
+++ b/cmds/gpu_counter_producer/Android.bp
@@ -19,6 +19,4 @@
         "-Wunused",
         "-Wunreachable-code",
     ],
-
-    soc_specific: true,
 }
diff --git a/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java b/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java
new file mode 100644
index 0000000..b89e2cd
--- /dev/null
+++ b/cmds/uinput/src/com/android/commands/uinput/EvemuParser.java
@@ -0,0 +1,380 @@
+/*
+ * Copyright 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.commands.uinput;
+
+import android.annotation.Nullable;
+import android.util.SparseArray;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+
+import src.com.android.commands.uinput.InputAbsInfo;
+
+/**
+ * Parser for the <a href="https://gitlab.freedesktop.org/libevdev/evemu">FreeDesktop evemu</a>
+ * event recording format.
+ */
+public class EvemuParser implements EventParser {
+    private static final String TAG = "UinputEvemuParser";
+
+    /**
+     * The device ID to use for all events. Since evemu files only support single-device
+     * recordings, this will always be the same.
+     */
+    private static final int DEVICE_ID = 1;
+    private static final int REGISTRATION_DELAY_MILLIS = 500;
+
+    private static class CommentAwareReader {
+        private final BufferedReader mReader;
+        private String mNextLine;
+
+        CommentAwareReader(BufferedReader in) throws IOException {
+            mReader = in;
+            mNextLine = findNextLine();
+        }
+
+        private @Nullable String findNextLine() throws IOException {
+            String line = "";
+            while (line != null && line.length() == 0) {
+                String unstrippedLine = mReader.readLine();
+                if (unstrippedLine == null) {
+                    // End of file.
+                    return null;
+                }
+                line = stripComments(unstrippedLine);
+            }
+            return line;
+        }
+
+        private static String stripComments(String line) {
+            int index = line.indexOf('#');
+            // 'N:' lines (which contain the name of the input device) do not support trailing
+            // comments, to support recording device names that contain #s.
+            if (index < 0 || line.startsWith("N: ")) {
+                return line;
+            } else {
+                return line.substring(0, index).strip();
+            }
+        }
+
+        /**
+         * Returns the next line of the file that isn't blank when stripped of comments, or
+         * {@code null} if the end of the file is reached. However, it does not advance to the
+         * next line of the file.
+         */
+        public @Nullable String peekLine() {
+            return mNextLine;
+        }
+
+        /** Moves to the next line of the file. */
+        public void advance() throws IOException {
+            mNextLine = findNextLine();
+        }
+
+        public boolean isAtEndOfFile() {
+            return mNextLine == null;
+        }
+    }
+
+    private final CommentAwareReader mReader;
+    /**
+     * The timestamp of the last event returned, of the head of {@link #mQueuedEvents} if there is
+     * one, or -1 if no events have been returned yet.
+     */
+    private long mLastEventTimeMicros = -1;
+    private final Queue<Event> mQueuedEvents = new ArrayDeque<>(2);
+
+    public EvemuParser(Reader in) throws IOException {
+        mReader = new CommentAwareReader(new BufferedReader(in));
+        mQueuedEvents.add(parseRegistrationEvent());
+
+        // The kernel takes a little time to set up an evdev device after the initial
+        // registration. Any events that we try to inject during this period would be silently
+        // dropped, so we delay for a short period after registration and before injecting any
+        // events.
+        final Event.Builder delayEb = new Event.Builder();
+        delayEb.setId(DEVICE_ID);
+        delayEb.setCommand(Event.Command.DELAY);
+        delayEb.setDurationMillis(REGISTRATION_DELAY_MILLIS);
+        mQueuedEvents.add(delayEb.build());
+    }
+
+    /**
+     * Returns the next event in the evemu recording.
+     */
+    public Event getNextEvent() throws IOException {
+        if (!mQueuedEvents.isEmpty()) {
+            return mQueuedEvents.remove();
+        }
+
+        if (mReader.isAtEndOfFile()) {
+            return null;
+        }
+
+        final String[] parts = expectLineWithParts("E", 4);
+        final String[] timeParts = parts[0].split("\\.");
+        if (timeParts.length != 2) {
+            throw new RuntimeException("Invalid timestamp (does not contain a '.')");
+        }
+        // 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]);
+        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]);
+        eb.setInjections(new int[] {eventType, eventCode, value});
+
+        if (mLastEventTimeMicros == -1) {
+            // This is the first event being injected, so send it straight away.
+            mLastEventTimeMicros = timeMicros;
+            return eb.build();
+        } else {
+            final long delayMicros = timeMicros - mLastEventTimeMicros;
+            // The shortest delay supported by Handler.sendMessageAtTime (used for timings by the
+            // Device class) is 1ms, so ignore time differences smaller than that.
+            if (delayMicros < 1000) {
+                mLastEventTimeMicros = timeMicros;
+                return eb.build();
+            } else {
+                // Send a delay now, and queue the actual event for the next call.
+                mQueuedEvents.add(eb.build());
+                mLastEventTimeMicros = timeMicros;
+                final Event.Builder delayEb = new Event.Builder();
+                delayEb.setId(DEVICE_ID);
+                delayEb.setCommand(Event.Command.DELAY);
+                delayEb.setDurationMillis((int) (delayMicros / 1000));
+                return delayEb.build();
+            }
+        }
+    }
+
+    private Event parseRegistrationEvent() throws IOException {
+        // The registration details at the start of a recording are specified by a set of lines
+        // that have to be in this order: N, I, P, B, A, L, S. Recordings must have exactly one N
+        // (name) and I (IDs) line. The remaining lines are optional, and there may be multiple
+        // of those lines.
+
+        final Event.Builder eb = new Event.Builder();
+        eb.setId(DEVICE_ID);
+        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 SparseArray<int[]> config = new SparseArray<>();
+        config.append(Event.UinputControlCode.UI_SET_PROPBIT.getValue(), parseProperties());
+
+        parseAxisBitmaps(config);
+
+        eb.setConfiguration(config);
+        if (config.contains(Event.UinputControlCode.UI_SET_FFBIT.getValue())) {
+            // If the device specifies any force feedback effects, the kernel will require the
+            // ff_effects_max value to be set.
+            eb.setFfEffectsMax(config.get(Event.UinputControlCode.UI_SET_FFBIT.getValue()).length);
+        }
+
+        eb.setAbsInfo(parseAbsInfos());
+
+        // L: and S: lines allow the initial states of the device's LEDs and switches to be
+        // recorded. However, the FreeDesktop implementation doesn't support actually setting these
+        // states at the start of playback (apparently due to concerns over race conditions), and we
+        // have no need for this feature either, so for now just skip over them.
+        skipUnsupportedLines("L");
+        skipUnsupportedLines("S");
+
+        return eb.build();
+    }
+
+    private int[] parseProperties() throws IOException {
+        final List<String> propBitmapParts = new ArrayList<>();
+        String line = acceptLine("P");
+        while (line != null) {
+            propBitmapParts.addAll(List.of(line.strip().split(" ")));
+            line = acceptLine("P");
+        }
+        return hexStringBitmapToEventCodes(propBitmapParts);
+    }
+
+    private void parseAxisBitmaps(SparseArray<int[]> config) throws IOException {
+        final Map<Integer, List<String>> axisBitmapParts = new HashMap<>();
+        String line = acceptLine("B");
+        while (line != null) {
+            final String[] parts = line.strip().split(" ");
+            if (parts.length < 2) {
+                throw new RuntimeException(
+                        "Expected event type and at least one bitmap byte on 'B:' line; only found "
+                                + parts.length + " elements");
+            }
+            final int eventType = Integer.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<>());
+                }
+                for (int i = 1; i < parts.length; i++) {
+                    axisBitmapParts.get(eventType).add(parts[i]);
+                }
+            }
+            line = acceptLine("B");
+        }
+        final List<Integer> eventTypesToSet = new ArrayList<>();
+        for (var entry : axisBitmapParts.entrySet()) {
+            if (entry.getValue().size() == 0) {
+                continue;
+            }
+            final Event.UinputControlCode controlCode =
+                    Event.UinputControlCode.forEventType(entry.getKey());
+            final int[] eventCodes = hexStringBitmapToEventCodes(entry.getValue());
+            if (controlCode != null && eventCodes.length > 0) {
+                config.append(controlCode.getValue(), eventCodes);
+                eventTypesToSet.add(entry.getKey());
+            }
+        }
+        config.append(
+                Event.UinputControlCode.UI_SET_EVBIT.getValue(), unboxIntList(eventTypesToSet));
+    }
+
+    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> "
+                                + "<flat> [<resolution>]'; expected 5 or 6 numbers but found "
+                                + parts.length);
+            }
+            final int axisCode = Integer.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;
+            absInfos.append(axisCode, info);
+            line = acceptLine("A");
+        }
+        return absInfos;
+    }
+
+    private void skipUnsupportedLines(String type) throws IOException {
+        if (acceptLine(type) != null) {
+            while (acceptLine(type) != null) {
+                // Skip the line.
+            }
+        }
+    }
+
+    /**
+     * Returns the contents of the next line in the file if it has the given type, or raises an
+     * error if it does not.
+     *
+     * @param type the type of the line to expect, represented by the letter before the ':'.
+     * @return the part of the line after the ": ".
+     */
+    private String expectLine(String type) throws IOException {
+        final String line = acceptLine(type);
+        if (line == null) {
+            throw new RuntimeException("Expected line of type '" + type + "'");
+        } else {
+            return line;
+        }
+    }
+
+    /**
+     * Peeks at the next line in the file to see if it has the given type, and if so, returns its
+     * contents and advances the reader.
+     *
+     * @param type the type of the line to accept, represented by the letter before the ':'.
+     * @return the part of the line after the ": ", if the type matches; otherwise {@code null}.
+     */
+    private @Nullable String acceptLine(String type) throws IOException {
+        final String line = mReader.peekLine();
+        if (line == null) {
+            return null;
+        }
+        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 ': '");
+        }
+        if (lineParts[0].equals(type)) {
+            mReader.advance();
+            return lineParts[1];
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * 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(" ");
+        if (parts.length != numParts) {
+            throw new RuntimeException("Expected a '" + type + "' line with " + numParts
+                    + " parts, found one with " + parts.length);
+        }
+        return parts;
+    }
+
+    private static int[] hexStringBitmapToEventCodes(List<String> strs) {
+        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 iBit = 0; iBit < 8; iBit++) {
+                if ((b & 1) != 0) {
+                    codes.add(iByte * 8 + iBit);
+                }
+                b >>= 1;
+            }
+        }
+        return unboxIntList(codes);
+    }
+
+    private static int[] unboxIntList(List<Integer> list) {
+        final int[] array = new int[list.size()];
+        Arrays.setAll(array, list::get);
+        return array;
+    }
+}
diff --git a/cmds/uinput/src/com/android/commands/uinput/Event.java b/cmds/uinput/src/com/android/commands/uinput/Event.java
index 4498bc2..5ec40e5 100644
--- a/cmds/uinput/src/com/android/commands/uinput/Event.java
+++ b/cmds/uinput/src/com/android/commands/uinput/Event.java
@@ -16,6 +16,7 @@
 
 package com.android.commands.uinput;
 
+import android.annotation.Nullable;
 import android.util.SparseArray;
 
 import java.util.Arrays;
@@ -39,6 +40,7 @@
 
     // Constants representing evdev event types, from include/uapi/linux/input-event-codes.h in the
     // kernel.
+    public static final int EV_SYN = 0x00;
     public static final int EV_KEY = 0x01;
     public static final int EV_REL = 0x02;
     public static final int EV_ABS = 0x03;
@@ -69,19 +71,23 @@
         public int getValue() {
             return mValue;
         }
-    }
 
-    // These constants come from "include/uapi/linux/input.h" in the kernel
-    enum Bus {
-        USB(0x03), BLUETOOTH(0x05);
-        private final int mValue;
-
-        Bus(int value) {
-            mValue = value;
-        }
-
-        int getValue() {
-            return mValue;
+        /**
+         * Returns the control code for the given evdev event type, or {@code null} if there is no
+         * control code for that type.
+         */
+        public static @Nullable UinputControlCode forEventType(int eventType) {
+            return switch (eventType) {
+                case EV_KEY -> UI_SET_KEYBIT;
+                case EV_REL -> UI_SET_RELBIT;
+                case EV_ABS -> UI_SET_ABSBIT;
+                case EV_MSC -> UI_SET_MSCBIT;
+                case EV_SW -> UI_SET_SWBIT;
+                case EV_LED -> UI_SET_LEDBIT;
+                case EV_SND -> UI_SET_SNDBIT;
+                case EV_FF -> UI_SET_FFBIT;
+                default -> null;
+            };
         }
     }
 
@@ -90,7 +96,7 @@
     private String mName;
     private int mVid;
     private int mPid;
-    private Bus mBus;
+    private int mBusId;
     private int[] mInjections;
     private SparseArray<int[]> mConfiguration;
     private int mDurationMillis;
@@ -120,7 +126,7 @@
     }
 
     public int getBus() {
-        return mBus.getValue();
+        return mBusId;
     }
 
     public int[] getInjections() {
@@ -168,7 +174,7 @@
             + ", name=" + mName
             + ", vid=" + mVid
             + ", pid=" + mPid
-            + ", bus=" + mBus
+            + ", busId=" + mBusId
             + ", events=" + Arrays.toString(mInjections)
             + ", configuration=" + mConfiguration
             + ", duration=" + mDurationMillis + "ms"
@@ -218,8 +224,8 @@
             mEvent.mPid = pid;
         }
 
-        public void setBus(Bus bus) {
-            mEvent.mBus = bus;
+        public void setBusId(int busId) {
+            mEvent.mBusId = busId;
         }
 
         public void setDurationMillis(int durationMillis) {
diff --git a/cmds/uinput/src/com/android/commands/uinput/EventParser.java b/cmds/uinput/src/com/android/commands/uinput/EventParser.java
new file mode 100644
index 0000000..a4df03d
--- /dev/null
+++ b/cmds/uinput/src/com/android/commands/uinput/EventParser.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 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.commands.uinput;
+
+import java.io.IOException;
+
+/**
+ * Interface for a class that reads a stream of {@link Event}s.
+ */
+public interface EventParser {
+    /**
+     * Returns the next event in the file that the parser is reading from.
+     */
+    Event getNextEvent() throws IOException;
+}
diff --git a/cmds/uinput/src/com/android/commands/uinput/JsonStyleParser.java b/cmds/uinput/src/com/android/commands/uinput/JsonStyleParser.java
index a2195c7..888ec5a 100644
--- a/cmds/uinput/src/com/android/commands/uinput/JsonStyleParser.java
+++ b/cmds/uinput/src/com/android/commands/uinput/JsonStyleParser.java
@@ -22,7 +22,7 @@
 import android.util.SparseArray;
 
 import java.io.IOException;
-import java.io.InputStreamReader;
+import java.io.Reader;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
@@ -34,12 +34,12 @@
 /**
  * A class that parses the JSON-like event format described in the README to build {@link Event}s.
  */
-public class JsonStyleParser {
+public class JsonStyleParser implements EventParser {
     private static final String TAG = "UinputJsonStyleParser";
 
     private JsonReader mReader;
 
-    public JsonStyleParser(InputStreamReader in) {
+    public JsonStyleParser(Reader in) {
         mReader = new JsonReader(in);
         mReader.setLenient(true);
     }
@@ -62,7 +62,7 @@
                         case "name" -> eb.setName(mReader.nextString());
                         case "vid" -> eb.setVid(readInt());
                         case "pid" -> eb.setPid(readInt());
-                        case "bus" -> eb.setBus(readBus());
+                        case "bus" -> eb.setBusId(readBus());
                         case "events" -> {
                             int[] injections = readInjectedEvents().stream()
                                     .mapToInt(Integer::intValue).toArray();
@@ -139,9 +139,35 @@
         });
     }
 
-    private Event.Bus readBus() throws IOException {
+    private int readBus() throws IOException {
         String val = mReader.nextString();
-        return Event.Bus.valueOf(val.toUpperCase());
+        // See include/uapi/linux/input.h in the kernel for the source of these constants.
+        return switch (val.toUpperCase()) {
+            case "PCI" -> 0x01;
+            case "ISAPNP" -> 0x02;
+            case "USB" -> 0x03;
+            case "HIL" -> 0x04;
+            case "BLUETOOTH" -> 0x05;
+            case "VIRTUAL" -> 0x06;
+            case "ISA" -> 0x10;
+            case "I8042" -> 0x11;
+            case "XTKBD" -> 0x12;
+            case "RS232" -> 0x13;
+            case "GAMEPORT" -> 0x14;
+            case "PARPORT" -> 0x15;
+            case "AMIGA" -> 0x16;
+            case "ADB" -> 0x17;
+            case "I2C" -> 0x18;
+            case "HOST" -> 0x19;
+            case "GSC" -> 0x1A;
+            case "ATARI" -> 0x1B;
+            case "SPI" -> 0x1C;
+            case "RMI" -> 0x1D;
+            case "CEC" -> 0x1E;
+            case "INTEL_ISHTP" -> 0x1F;
+            case "AMD_SFH" -> 0x20;
+            default -> throw new IllegalArgumentException("Invalid bus ID " + val);
+        };
     }
 
     private SparseArray<int[]> readConfiguration()
diff --git a/cmds/uinput/src/com/android/commands/uinput/Uinput.java b/cmds/uinput/src/com/android/commands/uinput/Uinput.java
index fe76abb..684a12f 100644
--- a/cmds/uinput/src/com/android/commands/uinput/Uinput.java
+++ b/cmds/uinput/src/com/android/commands/uinput/Uinput.java
@@ -19,12 +19,12 @@
 import android.util.Log;
 import android.util.SparseArray;
 
+import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
-import java.io.UnsupportedEncodingException;
 import java.util.Objects;
 
 /**
@@ -35,7 +35,7 @@
 public class Uinput {
     private static final String TAG = "UINPUT";
 
-    private final JsonStyleParser mParser;
+    private final EventParser mParser;
     private final SparseArray<Device> mDevices;
 
     private static void usage() {
@@ -74,12 +74,32 @@
     private Uinput(InputStream in) {
         mDevices = new SparseArray<Device>();
         try {
-            mParser = new JsonStyleParser(new InputStreamReader(in, "UTF-8"));
-        } catch (UnsupportedEncodingException e) {
+            BufferedReader reader = new BufferedReader(new InputStreamReader(in, "UTF-8"));
+            mParser = isEvemuFile(reader) ? new EvemuParser(reader) : new JsonStyleParser(reader);
+        } catch (IOException e) {
             throw new RuntimeException(e);
         }
     }
 
+    private boolean isEvemuFile(BufferedReader in) throws IOException {
+        // After zero or more empty lines (not even containing horizontal whitespace), evemu
+        // recordings must either start with '#' (indicating the EVEMU version header or a comment)
+        // or 'N' (for the name line). If we encounter anything else, assume it's a JSON-style input
+        // file.
+
+        String lineSep = System.lineSeparator();
+        char[] buf = new char[1];
+
+        in.mark(1 /* readAheadLimit */);
+        int charsRead = in.read(buf);
+        while (charsRead > 0 && lineSep.contains(String.valueOf(buf[0]))) {
+            in.mark(1 /* readAheadLimit */);
+            charsRead = in.read(buf);
+        }
+        in.reset();
+        return buf[0] == '#' || buf[0] == 'N';
+    }
+
     private void run() {
         try {
             Event e = null;
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 275fe77..c282e4b6 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -307,7 +307,7 @@
     field public static final String RECOVER_KEYSTORE = "android.permission.RECOVER_KEYSTORE";
     field public static final String REGISTER_CALL_PROVIDER = "android.permission.REGISTER_CALL_PROVIDER";
     field public static final String REGISTER_CONNECTION_MANAGER = "android.permission.REGISTER_CONNECTION_MANAGER";
-    field public static final String REGISTER_NSD_OFFLOAD_ENGINE = "android.permission.REGISTER_NSD_OFFLOAD_ENGINE";
+    field @FlaggedApi("com.android.net.flags.register_nsd_offload_engine") public static final String REGISTER_NSD_OFFLOAD_ENGINE = "android.permission.REGISTER_NSD_OFFLOAD_ENGINE";
     field public static final String REGISTER_SIM_SUBSCRIPTION = "android.permission.REGISTER_SIM_SUBSCRIPTION";
     field public static final String REGISTER_STATS_PULL_ATOM = "android.permission.REGISTER_STATS_PULL_ATOM";
     field public static final String REMOTE_DISPLAY_PROVIDER = "android.permission.REMOTE_DISPLAY_PROVIDER";
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index b5f7f23..6c10f49 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -7604,15 +7604,17 @@
      * @param taskDescription The TaskDescription properties that describe the task with this activity
      */
     public void setTaskDescription(ActivityManager.TaskDescription taskDescription) {
-        if (mTaskDescription != taskDescription) {
-            mTaskDescription.copyFromPreserveHiddenFields(taskDescription);
-            // Scale the icon down to something reasonable if it is provided
-            if (taskDescription.getIconFilename() == null && taskDescription.getIcon() != null) {
-                final int size = ActivityManager.getLauncherLargeIconSizeInner(this);
-                final Bitmap icon = Bitmap.createScaledBitmap(taskDescription.getIcon(), size, size,
-                        true);
-                mTaskDescription.setIcon(Icon.createWithBitmap(icon));
-            }
+        if (taskDescription == null || mTaskDescription.equals(taskDescription)) {
+            return;
+        }
+
+        mTaskDescription.copyFromPreserveHiddenFields(taskDescription);
+        // Scale the icon down to something reasonable if it is provided
+        if (taskDescription.getIconFilename() == null && taskDescription.getIcon() != null) {
+            final int size = ActivityManager.getLauncherLargeIconSizeInner(this);
+            final Bitmap icon = Bitmap.createScaledBitmap(taskDescription.getIcon(), size, size,
+                    true);
+            mTaskDescription.setIcon(Icon.createWithBitmap(icon));
         }
         ActivityClient.getInstance().setTaskDescription(mToken, mTaskDescription);
     }
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 7b075e6..4d208c6 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2301,6 +2301,7 @@
     <!-- Allows system apps to call methods to register itself as a mDNS offload engine.
         <p>Not for use by third-party or privileged applications.
         @SystemApi
+        @FlaggedApi("com.android.net.flags.register_nsd_offload_engine")
         @hide This should only be used by system apps.
     -->
     <permission android:name="android.permission.REGISTER_NSD_OFFLOAD_ENGINE"
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
index b528089d15..5c02dbc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
@@ -17,6 +17,7 @@
 package com.android.wm.shell.transition;
 
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED;
 
 import static com.android.wm.shell.transition.Transitions.TransitionObserver;
 
@@ -61,9 +62,10 @@
             }
 
             final int mode = change.getMode();
+            final boolean isBackGesture = change.hasFlags(FLAG_BACK_GESTURE_ANIMATED);
             if (taskInfo.getActivityType() == ACTIVITY_TYPE_HOME
-                    && TransitionUtil.isOpenOrCloseMode(mode)) {
-                notifyHomeVisibilityChanged(TransitionUtil.isOpeningType(mode));
+                    && (TransitionUtil.isOpenOrCloseMode(mode) || isBackGesture)) {
+                notifyHomeVisibilityChanged(TransitionUtil.isOpeningType(mode) || isBackGesture);
             }
         }
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
index a5629c8..b355ab0 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
@@ -14,8 +14,6 @@
 import android.window.TransitionInfo
 import android.window.TransitionInfo.FLAG_IS_WALLPAPER
 import androidx.test.filters.SmallTest
-import com.android.server.testutils.any
-import com.android.server.testutils.mock
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
 import com.android.wm.shell.ShellTestCase
 import com.android.wm.shell.TestRunningTaskInfoBuilder
@@ -28,8 +26,10 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
 import org.mockito.ArgumentMatchers.eq
 import org.mockito.Mock
+import org.mockito.kotlin.mock
 import org.mockito.kotlin.never
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.verifyZeroInteractions
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
index ea7c0d9..421c445 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
@@ -18,8 +18,10 @@
 
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
+import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 
@@ -145,6 +147,26 @@
         verify(mListener, times(0)).onHomeVisibilityChanged(anyBoolean());
     }
 
+    @Test
+    public void testHomeActivityWithBackGestureNotifiesHomeIsVisible() throws RemoteException {
+        TransitionInfo info = mock(TransitionInfo.class);
+        TransitionInfo.Change change = mock(TransitionInfo.Change.class);
+        ActivityManager.RunningTaskInfo taskInfo = mock(ActivityManager.RunningTaskInfo.class);
+        when(change.getTaskInfo()).thenReturn(taskInfo);
+        when(info.getChanges()).thenReturn(new ArrayList<>(List.of(change)));
+
+        when(change.hasFlags(FLAG_BACK_GESTURE_ANIMATED)).thenReturn(true);
+        setupTransitionInfo(taskInfo, change, ACTIVITY_TYPE_HOME, TRANSIT_CHANGE);
+
+        mHomeTransitionObserver.onTransitionReady(mock(IBinder.class),
+                info,
+                mock(SurfaceControl.Transaction.class),
+                mock(SurfaceControl.Transaction.class));
+
+        verify(mListener, times(1)).onHomeVisibilityChanged(true);
+    }
+
+
     /**
      * Helper class to initialize variables for the rest.
      */
diff --git a/packages/InputDevices/res/raw/keyboard_layout_english_uk.kcm b/packages/InputDevices/res/raw/keyboard_layout_english_uk.kcm
index 93a5082..071f9f4 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_english_uk.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_english_uk.kcm
@@ -116,6 +116,9 @@
     base:                               'w'
     shift, capslock:                    'W'
     shift+capslock:                     'w'
+    ralt:                               '\u1e83'
+    shift+ralt, capslock+ralt:          '\u1e82'
+    shift+capslock+ralt:                '\u1e83'
 }
 
 key E {
@@ -147,6 +150,9 @@
     base:                               'y'
     shift, capslock:                    'Y'
     shift+capslock:                     'y'
+    ralt:                               '\u00fd'
+    shift+ralt, capslock+ralt:          '\u00dd'
+    shift+capslock+ralt:                '\u00fd'
 }
 
 key U {
@@ -313,6 +319,9 @@
     base:                               'c'
     shift, capslock:                    'C'
     shift+capslock:                     'c'
+    ralt:                               '\u00e7'
+    shift+ralt, capslock+ralt:          '\u00c7'
+    shift+capslock+ralt:                '\u00e7'
 }
 
 key V {
diff --git a/packages/InputDevices/res/raw/keyboard_layout_french.kcm b/packages/InputDevices/res/raw/keyboard_layout_french.kcm
index 4906304..636f98d 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_french.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_french.kcm
@@ -44,7 +44,7 @@
     label:                              '2'
     base:                               '\u00e9'
     shift:                              '2'
-    ralt:                               '~'
+    ralt:                               '\u0303'
 }
 
 key 3 {
@@ -79,7 +79,7 @@
     label:                              '7'
     base:                               '\u00e8'
     shift:                              '7'
-    ralt:                               '`'
+    ralt:                               '\u0300'
 }
 
 key 8 {
diff --git a/packages/InputDevices/res/xml/keyboard_layouts.xml b/packages/InputDevices/res/xml/keyboard_layouts.xml
index 7f23f74..ee49b23 100644
--- a/packages/InputDevices/res/xml/keyboard_layouts.xml
+++ b/packages/InputDevices/res/xml/keyboard_layouts.xml
@@ -94,7 +94,7 @@
         android:name="keyboard_layout_swiss_german"
         android:label="@string/keyboard_layout_swiss_german_label"
         android:keyboardLayout="@raw/keyboard_layout_swiss_german"
-        android:keyboardLocale="de-Latn-CH"
+        android:keyboardLocale="de-Latn-CH|gsw-Latn-CH"
         android:keyboardLayoutType="qwertz" />
 
     <keyboard-layout
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index 83c16ae..6a0e882 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -19,6 +19,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor
 import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor
+import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
 import javax.inject.Inject
 
 /** A class in which engineers can define flag dependencies */
@@ -26,6 +27,7 @@
 class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, handler: Handler) :
     FlagDependenciesBase(featureFlags, handler) {
     override fun defineDependencies() {
+        NotificationsLiveDataStoreRefactor.token dependsOn NotificationIconContainerRefactor.token
         FooterViewRefactor.token dependsOn NotificationIconContainerRefactor.token
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/model/StatusBarMode.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/model/StatusBarMode.kt
index 747efe3..933d0ab 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/data/model/StatusBarMode.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/model/StatusBarMode.kt
@@ -36,7 +36,8 @@
     /**
      * A mode where notification icons in the status bar are hidden and replaced by a dot (this mode
      * can be requested by apps). See
-     * [com.android.systemui.statusbar.phone.LightsOutNotifController].
+     * [com.android.systemui.statusbar.phone.LegacyLightsOutNotifController] and
+     * [com.android.systemui.statusbar.phone.domain.interactor.LightsOutInteractor].
      */
     LIGHTS_OUT,
     /** Similar to [LIGHTS_OUT], but also with a transparent background for the status bar. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/KeyguardStatusBarRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/KeyguardStatusBarRepository.kt
index d1594ef..0415212 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/KeyguardStatusBarRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/KeyguardStatusBarRepository.kt
@@ -33,7 +33,7 @@
 
 /**
  * Repository for data that's specific to the status bar **on keyguard**. For data that applies to
- * all status bars, use [StatusBarModeRepository].
+ * all status bars, use [StatusBarModeRepositoryStore].
  */
 interface KeyguardStatusBarRepository {
     /** True if we can show the user switcher on keyguard and false otherwise. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt
similarity index 92%
rename from packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepository.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt
index 47994d9..6429815 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModePerDisplayRepository.kt
@@ -25,10 +25,8 @@
 import android.view.WindowInsetsController.Appearance
 import com.android.internal.statusbar.LetterboxDetails
 import com.android.internal.view.AppearanceRegion
-import com.android.systemui.CoreStartable
-import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.Dumpable
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.DisplayId
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.core.StatusBarInitializer.OnStatusBarViewInitializedListener
 import com.android.systemui.statusbar.data.model.StatusBarAppearance
@@ -38,13 +36,10 @@
 import com.android.systemui.statusbar.phone.StatusBarBoundsProvider
 import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent
 import com.android.systemui.statusbar.phone.ongoingcall.data.repository.OngoingCallRepository
-import dagger.Binds
-import dagger.Module
-import dagger.multibindings.ClassKey
-import dagger.multibindings.IntoMap
-import dagger.multibindings.IntoSet
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
 import java.io.PrintWriter
-import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
@@ -61,7 +56,7 @@
  * Note: These status bar modes are status bar *window* states that are sent to us from
  * WindowManager, not determined internally.
  */
-interface StatusBarModeRepository {
+interface StatusBarModePerDisplayRepository {
     /**
      * True if the status bar window is showing transiently and will disappear soon, and false
      * otherwise. ("Otherwise" in this case means the status bar is persistently hidden OR
@@ -108,16 +103,15 @@
     fun clearTransient()
 }
 
-@SysUISingleton
-class StatusBarModeRepositoryImpl
-@Inject
+class StatusBarModePerDisplayRepositoryImpl
+@AssistedInject
 constructor(
     @Application scope: CoroutineScope,
-    @DisplayId thisDisplayId: Int,
+    @Assisted("displayId") thisDisplayId: Int,
     private val commandQueue: CommandQueue,
     private val letterboxAppearanceCalculator: LetterboxAppearanceCalculator,
     ongoingCallRepository: OngoingCallRepository,
-) : StatusBarModeRepository, CoreStartable, OnStatusBarViewInitializedListener {
+) : StatusBarModePerDisplayRepository, OnStatusBarViewInitializedListener, Dumpable {
 
     private val commandQueueCallback =
         object : CommandQueue.Callbacks {
@@ -166,7 +160,7 @@
             }
         }
 
-    override fun start() {
+    fun start() {
         commandQueue.addCallback(commandQueueCallback)
     }
 
@@ -340,16 +334,7 @@
     )
 }
 
-@Module
-interface StatusBarModeRepositoryModule {
-    @Binds fun bindImpl(impl: StatusBarModeRepositoryImpl): StatusBarModeRepository
-
-    @Binds
-    @IntoMap
-    @ClassKey(StatusBarModeRepositoryImpl::class)
-    fun bindCoreStartable(impl: StatusBarModeRepositoryImpl): CoreStartable
-
-    @Binds
-    @IntoSet
-    fun bindViewInitListener(impl: StatusBarModeRepositoryImpl): OnStatusBarViewInitializedListener
+@AssistedFactory
+interface StatusBarModePerDisplayRepositoryFactory {
+    fun create(@Assisted("displayId") displayId: Int): StatusBarModePerDisplayRepositoryImpl
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryStore.kt
new file mode 100644
index 0000000..962cb095
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryStore.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.systemui.statusbar.data.repository
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.DisplayId
+import com.android.systemui.statusbar.core.StatusBarInitializer
+import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+import dagger.multibindings.IntoSet
+import java.io.PrintWriter
+import javax.inject.Inject
+
+interface StatusBarModeRepositoryStore {
+    val defaultDisplay: StatusBarModePerDisplayRepository
+    fun forDisplay(displayId: Int): StatusBarModePerDisplayRepository
+}
+
+@SysUISingleton
+class StatusBarModeRepositoryImpl
+@Inject
+constructor(
+    @DisplayId private val displayId: Int,
+    factory: StatusBarModePerDisplayRepositoryFactory
+) :
+    StatusBarModeRepositoryStore,
+    CoreStartable,
+    StatusBarInitializer.OnStatusBarViewInitializedListener {
+    override val defaultDisplay = factory.create(displayId)
+
+    override fun forDisplay(displayId: Int) =
+        if (this.displayId == displayId) {
+            defaultDisplay
+        } else {
+            TODO("b/127878649 implement multi-display state management")
+        }
+
+    override fun start() {
+        defaultDisplay.start()
+    }
+
+    override fun onStatusBarViewInitialized(component: StatusBarFragmentComponent) {
+        defaultDisplay.onStatusBarViewInitialized(component)
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        defaultDisplay.dump(pw, args)
+    }
+}
+
+@Module
+interface StatusBarModeRepositoryModule {
+    @Binds fun bindImpl(impl: StatusBarModeRepositoryImpl): StatusBarModeRepositoryStore
+
+    @Binds
+    @IntoMap
+    @ClassKey(StatusBarModeRepositoryStore::class)
+    fun bindCoreStartable(impl: StatusBarModeRepositoryImpl): CoreStartable
+
+    @Binds
+    @IntoSet
+    fun bindViewInitListener(
+        impl: StatusBarModeRepositoryImpl
+    ): StatusBarInitializer.OnStatusBarViewInitializedListener
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsLiveDataRefactor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsLiveDataStoreRefactor.kt
similarity index 99%
rename from packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsLiveDataRefactor.kt
rename to packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsLiveDataStoreRefactor.kt
index 44387c2..8fc7106 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsLiveDataRefactor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsLiveDataStoreRefactor.kt
@@ -50,4 +50,4 @@
      */
     @JvmStatic
     inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
-}
\ No newline at end of file
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index b3fe79e..57d49b2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -199,7 +199,7 @@
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.core.StatusBarInitializer;
 import com.android.systemui.statusbar.data.model.StatusBarMode;
-import com.android.systemui.statusbar.data.repository.StatusBarModeRepository;
+import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
 import com.android.systemui.statusbar.notification.NotificationActivityStarter;
 import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorControllerProvider;
@@ -387,7 +387,7 @@
     private final NotificationShadeWindowController mNotificationShadeWindowController;
     private final StatusBarInitializer mStatusBarInitializer;
     private final StatusBarWindowController mStatusBarWindowController;
-    private final StatusBarModeRepository mStatusBarModeRepository;
+    private final StatusBarModeRepositoryStore mStatusBarModeRepository;
     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
     @VisibleForTesting
     DozeServiceHost mDozeServiceHost;
@@ -605,7 +605,7 @@
             StatusBarInitializer statusBarInitializer,
             StatusBarWindowController statusBarWindowController,
             StatusBarWindowStateController statusBarWindowStateController,
-            StatusBarModeRepository statusBarModeRepository,
+            StatusBarModeRepositoryStore statusBarModeRepository,
             KeyguardUpdateMonitor keyguardUpdateMonitor,
             StatusBarSignalPolicy statusBarSignalPolicy,
             PulseExpansionHandler pulseExpansionHandler,
@@ -899,7 +899,7 @@
         setUpPresenter();
 
         if ((result.mTransientBarTypes & WindowInsets.Type.statusBars()) != 0) {
-            mStatusBarModeRepository.showTransient();
+            mStatusBarModeRepository.getDefaultDisplay().showTransient();
         }
         mCommandQueueCallbacks.onSystemBarAttributesChanged(mDisplayId, result.mAppearance,
                 result.mAppearanceRegions, result.mNavbarColorManagedByIme, result.mBehavior,
@@ -1146,9 +1146,10 @@
 
         mDemoModeController.addCallback(mDemoModeCallback);
         mJavaAdapter.alwaysCollectFlow(
-                mStatusBarModeRepository.isTransientShown(), this::onTransientShownChanged);
+                mStatusBarModeRepository.getDefaultDisplay().isTransientShown(),
+                this::onTransientShownChanged);
         mJavaAdapter.alwaysCollectFlow(
-                mStatusBarModeRepository.getStatusBarMode(),
+                mStatusBarModeRepository.getDefaultDisplay().getStatusBarMode(),
                 this::updateBarMode);
 
         mCommandQueueCallbacks = mCommandQueueCallbacksLazy.get();
@@ -1208,7 +1209,7 @@
 
             @Override
             public void hide() {
-                mStatusBarModeRepository.clearTransient();
+                mStatusBarModeRepository.getDefaultDisplay().clearTransient();
             }
         });
 
@@ -1656,7 +1657,7 @@
         if (mDemoModeController.isInDemoMode()) return;
         if (mStatusBarTransitions != null) {
             checkBarMode(
-                    mStatusBarModeRepository.getStatusBarMode().getValue(),
+                    mStatusBarModeRepository.getDefaultDisplay().getStatusBarMode().getValue(),
                     mStatusBarWindowState,
                     mStatusBarTransitions);
         }
@@ -1667,7 +1668,8 @@
     /** Temporarily hides Bubbles if the status bar is hidden. */
     @Override
     public void updateBubblesVisibility() {
-        StatusBarMode mode = mStatusBarModeRepository.getStatusBarMode().getValue();
+        StatusBarMode mode =
+                mStatusBarModeRepository.getDefaultDisplay().getStatusBarMode().getValue();
         mBubblesOptional.ifPresent(bubbles -> bubbles.onStatusBarVisibilityChanged(
                 mode != StatusBarMode.LIGHTS_OUT
                         && mode != StatusBarMode.LIGHTS_OUT_TRANSPARENT
@@ -2953,7 +2955,7 @@
     // End Extra BaseStatusBarMethods.
 
     boolean isTransientShown() {
-        return mStatusBarModeRepository.isTransientShown().getValue();
+        return mStatusBarModeRepository.getDefaultDisplay().isTransientShown().getValue();
     }
 
     private void updateLightRevealScrimVisibility() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightsOutNotifController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyLightsOutNotifController.java
similarity index 94%
rename from packages/SystemUI/src/com/android/systemui/statusbar/phone/LightsOutNotifController.java
rename to packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyLightsOutNotifController.java
index eba7fe0..7c871e1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightsOutNotifController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyLightsOutNotifController.java
@@ -36,6 +36,7 @@
 import com.android.internal.view.AppearanceRegion;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.notification.collection.NotifLiveDataStore;
+import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor;
 import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentScope;
 import com.android.systemui.util.ViewController;
 
@@ -51,7 +52,7 @@
  * whether there are notifications when the device is in {@link View#SYSTEM_UI_FLAG_LOW_PROFILE}.
  */
 @StatusBarFragmentScope
-public class LightsOutNotifController extends ViewController<View> {
+public class LegacyLightsOutNotifController extends ViewController<View> {
     private final CommandQueue mCommandQueue;
     private final NotifLiveDataStore mNotifDataStore;
     private final WindowManager mWindowManager;
@@ -63,7 +64,7 @@
     private int mDisplayId;
 
     @Inject
-    LightsOutNotifController(
+    LegacyLightsOutNotifController(
             @Named(LIGHTS_OUT_NOTIF_VIEW) View lightsOutNotifView,
             WindowManager windowManager,
             NotifLiveDataStore notifDataStore,
@@ -72,7 +73,12 @@
         mWindowManager = windowManager;
         mNotifDataStore = notifDataStore;
         mCommandQueue = commandQueue;
+    }
 
+    @Override
+    protected void onInit() {
+        super.onInit();
+        NotificationsLiveDataStoreRefactor.assertInLegacyMode();
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
index 4d3e2ad..eec617b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LightBarController.java
@@ -42,7 +42,7 @@
 import com.android.systemui.plugins.DarkIconDispatcher;
 import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.statusbar.data.model.StatusBarAppearance;
-import com.android.systemui.statusbar.data.repository.StatusBarModeRepository;
+import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.util.Compile;
 import com.android.systemui.util.kotlin.JavaAdapter;
@@ -68,7 +68,7 @@
     private final JavaAdapter mJavaAdapter;
     private final SysuiDarkIconDispatcher mStatusBarIconController;
     private final BatteryController mBatteryController;
-    private final StatusBarModeRepository mStatusBarModeRepository;
+    private final StatusBarModeRepositoryStore mStatusBarModeRepository;
     private BiometricUnlockController mBiometricUnlockController;
 
     private LightBarTransitionsController mNavigationBarController;
@@ -126,7 +126,7 @@
             DarkIconDispatcher darkIconDispatcher,
             BatteryController batteryController,
             NavigationModeController navModeController,
-            StatusBarModeRepository statusBarModeRepository,
+            StatusBarModeRepositoryStore statusBarModeRepository,
             DumpManager dumpManager,
             DisplayTracker displayTracker) {
         mJavaAdapter = javaAdapter;
@@ -146,7 +146,7 @@
     @Override
     public void start() {
         mJavaAdapter.alwaysCollectFlow(
-                mStatusBarModeRepository.getStatusBarAppearance(),
+                mStatusBarModeRepository.getDefaultDisplay().getStatusBarAppearance(),
                 this::onStatusBarAppearanceChanged);
     }
 
@@ -476,7 +476,7 @@
         private final DarkIconDispatcher mDarkIconDispatcher;
         private final BatteryController mBatteryController;
         private final NavigationModeController mNavModeController;
-        private final StatusBarModeRepository mStatusBarModeRepository;
+        private final StatusBarModeRepositoryStore mStatusBarModeRepository;
         private final DumpManager mDumpManager;
         private final DisplayTracker mDisplayTracker;
 
@@ -486,7 +486,7 @@
                 DarkIconDispatcher darkIconDispatcher,
                 BatteryController batteryController,
                 NavigationModeController navModeController,
-                StatusBarModeRepository statusBarModeRepository,
+                StatusBarModeRepositoryStore statusBarModeRepository,
                 DumpManager dumpManager,
                 DisplayTracker displayTracker) {
             mJavaAdapter = javaAdapter;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/domain/interactor/LightsOutInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/domain/interactor/LightsOutInteractor.kt
new file mode 100644
index 0000000..ed8b3e8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/domain/interactor/LightsOutInteractor.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.systemui.statusbar.phone.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.data.model.StatusBarMode
+import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+/**
+ * Apps can request a low profile mode [android.view.View.SYSTEM_UI_FLAG_LOW_PROFILE] where status
+ * bar and navigation icons dim. In this mode, a notification dot appears where the notification
+ * icons would appear if they would be shown outside of this mode.
+ *
+ * This interactor knows whether the device is in [android.view.View.SYSTEM_UI_FLAG_LOW_PROFILE].
+ */
+@SysUISingleton
+class LightsOutInteractor
+@Inject
+constructor(private val repository: StatusBarModeRepositoryStore) {
+
+    fun isLowProfile(displayId: Int): Flow<Boolean> =
+        repository.forDisplay(displayId).statusBarMode.map {
+            when (it) {
+                StatusBarMode.LIGHTS_OUT,
+                StatusBarMode.LIGHTS_OUT_TRANSPARENT -> true
+                else -> false
+            }
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
index 7adc08c..49880d4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
@@ -43,7 +43,6 @@
 import com.android.systemui.demomode.DemoMode;
 import com.android.systemui.demomode.DemoModeController;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlagsClassic;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.res.R;
 import com.android.systemui.shade.ShadeExpansionStateManager;
@@ -139,7 +138,6 @@
     private final OngoingCallController mOngoingCallController;
     private final SystemStatusAnimationScheduler mAnimationScheduler;
     private final StatusBarLocationPublisher mLocationPublisher;
-    private final FeatureFlagsClassic mFeatureFlags;
     private final NotificationIconAreaController mNotificationIconAreaController;
     private final ShadeExpansionStateManager mShadeExpansionStateManager;
     private final StatusBarIconController mStatusBarIconController;
@@ -228,7 +226,6 @@
             StatusBarLocationPublisher locationPublisher,
             NotificationIconAreaController notificationIconAreaController,
             ShadeExpansionStateManager shadeExpansionStateManager,
-            FeatureFlagsClassic featureFlags,
             StatusBarIconController statusBarIconController,
             DarkIconManager.Factory darkIconManagerFactory,
             CollapsedStatusBarViewModel collapsedStatusBarViewModel,
@@ -258,7 +255,6 @@
         mLocationPublisher = locationPublisher;
         mNotificationIconAreaController = notificationIconAreaController;
         mShadeExpansionStateManager = shadeExpansionStateManager;
-        mFeatureFlags = featureFlags;
         mStatusBarIconController = statusBarIconController;
         mCollapsedStatusBarViewModel = collapsedStatusBarViewModel;
         mCollapsedStatusBarViewBinder = collapsedStatusBarViewBinder;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentComponent.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentComponent.java
index 0618abb..96faa35 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentComponent.java
@@ -18,8 +18,9 @@
 
 import com.android.systemui.battery.BatteryMeterViewController;
 import com.android.systemui.dagger.qualifiers.RootView;
+import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor;
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
-import com.android.systemui.statusbar.phone.LightsOutNotifController;
+import com.android.systemui.statusbar.phone.LegacyLightsOutNotifController;
 import com.android.systemui.statusbar.phone.PhoneStatusBarTransitions;
 import com.android.systemui.statusbar.phone.PhoneStatusBarView;
 import com.android.systemui.statusbar.phone.PhoneStatusBarViewController;
@@ -78,7 +79,9 @@
         getBatteryMeterViewController().init();
         getHeadsUpAppearanceController().init();
         getPhoneStatusBarViewController().init();
-        getLightsOutNotifController().init();
+        if (!NotificationsLiveDataStoreRefactor.isEnabled()) {
+            getLegacyLightsOutNotifController().init();
+        }
         getStatusBarDemoMode().init();
     }
 
@@ -101,7 +104,7 @@
 
     /** */
     @StatusBarFragmentScope
-    LightsOutNotifController getLightsOutNotifController();
+    LegacyLightsOutNotifController getLegacyLightsOutNotifController();
 
     /** */
     @StatusBarFragmentScope
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
index b0532ce..0bdd1a5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
@@ -36,7 +36,7 @@
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.plugins.ActivityStarter
-import com.android.systemui.statusbar.data.repository.StatusBarModeRepository
+import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore
 import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
@@ -68,7 +68,7 @@
     private val dumpManager: DumpManager,
     private val statusBarWindowController: StatusBarWindowController,
     private val swipeStatusBarAwayGestureHandler: SwipeStatusBarAwayGestureHandler,
-    private val statusBarModeRepository: StatusBarModeRepository,
+    private val statusBarModeRepository: StatusBarModeRepositoryStore,
 ) : CallbackController<OngoingCallListener>, Dumpable, CoreStartable {
     private var isFullscreen: Boolean = false
     /** Non-null if there's an active call notification. */
@@ -129,7 +129,7 @@
         dumpManager.registerDumpable(this)
         notifCollection.addCollectionListener(notifListener)
         scope.launch {
-            statusBarModeRepository.isInFullscreenMode.collect {
+            statusBarModeRepository.defaultDisplay.isInFullscreenMode.collect {
                 isFullscreen = it
                 updateChipClickListener()
                 updateGestureListening()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/data/repository/OngoingCallRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/data/repository/OngoingCallRepository.kt
index da9c45a..9c78ab4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/data/repository/OngoingCallRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/data/repository/OngoingCallRepository.kt
@@ -27,7 +27,7 @@
  *
  * This class is used to break a dependency cycle between
  * [com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController] and
- * [com.android.systemui.statusbar.data.repository.StatusBarModeRepository]. Instead, those two
+ * [com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore]. Instead, those two
  * classes both refer to this repository.
  */
 @SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
index b9b88f4..7d7f49b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
@@ -16,11 +16,15 @@
 
 package com.android.systemui.statusbar.pipeline.shared.ui.binder
 
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
 import android.view.View
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
 import com.android.systemui.statusbar.pipeline.shared.ui.viewmodel.CollapsedStatusBarViewModel
 import javax.inject.Inject
 import kotlinx.coroutines.launch
@@ -61,9 +65,49 @@
                         listener.onTransitionFromLockscreenToDreamStarted()
                     }
                 }
+
+                if (NotificationsLiveDataStoreRefactor.isEnabled) {
+                    val displayId = view.display.displayId
+                    val lightsOutView: View = view.requireViewById(R.id.notification_lights_out)
+                    launch {
+                        viewModel.areNotificationsLightsOut(displayId).collect { show ->
+                            animateLightsOutView(lightsOutView, show)
+                        }
+                    }
+                }
             }
         }
     }
+
+    private fun animateLightsOutView(view: View, visible: Boolean) {
+        view.animate().cancel()
+
+        val alpha = if (visible) 1f else 0f
+        val duration = if (visible) 750L else 250L
+        val visibility = if (visible) View.VISIBLE else View.GONE
+
+        if (visible) {
+            view.alpha = 0f
+            view.visibility = View.VISIBLE
+        }
+
+        view
+            .animate()
+            .alpha(alpha)
+            .setDuration(duration)
+            .setListener(
+                object : AnimatorListenerAdapter() {
+                    override fun onAnimationEnd(animation: Animator) {
+                        view.alpha = alpha
+                        view.visibility = visibility
+                        // Unset the listener, otherwise this may persist for
+                        // another view property animation
+                        view.animate().setListener(null)
+                    }
+                }
+            )
+            .start()
+    }
 }
 
 /** Listener for various events that may affect the status bar's visibility. */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModel.kt
index 15ab143..52a6d8c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModel.kt
@@ -20,11 +20,17 @@
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor
+import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor
+import com.android.systemui.statusbar.phone.domain.interactor.LightsOutInteractor
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
@@ -48,12 +54,25 @@
 
     /** Emits whenever a transition from lockscreen to dream has started. */
     val transitionFromLockscreenToDreamStartedEvent: Flow<Unit>
+
+    /**
+     * Apps can request a low profile mode [android.view.View.SYSTEM_UI_FLAG_LOW_PROFILE] where
+     * status bar and navigation icons dim. In this mode, a notification dot appears where the
+     * notification icons would appear if they would be shown outside of this mode.
+     *
+     * This flow tells when to show or hide the notification dot in the status bar to indicate
+     * whether there are notifications when the device is in
+     * [android.view.View.SYSTEM_UI_FLAG_LOW_PROFILE].
+     */
+    fun areNotificationsLightsOut(displayId: Int): Flow<Boolean>
 }
 
 @SysUISingleton
 class CollapsedStatusBarViewModelImpl
 @Inject
 constructor(
+    private val lightsOutInteractor: LightsOutInteractor,
+    private val notificationsInteractor: ActiveNotificationsInteractor,
     keyguardTransitionInteractor: KeyguardTransitionInteractor,
     @Application coroutineScope: CoroutineScope,
 ) : CollapsedStatusBarViewModel {
@@ -69,4 +88,17 @@
         keyguardTransitionInteractor.lockscreenToDreamingTransition
             .filter { it.transitionState == TransitionState.STARTED }
             .map {}
+
+    override fun areNotificationsLightsOut(displayId: Int): Flow<Boolean> =
+        if (NotificationsLiveDataStoreRefactor.isUnexpectedlyInLegacyMode()) {
+            emptyFlow()
+        } else {
+            combine(
+                    notificationsInteractor.areAnyNotificationsPresent,
+                    lightsOutInteractor.isLowProfile(displayId),
+                ) { hasNotifications, isLowProfile ->
+                    hasNotifications && isLowProfile
+                }
+                .distinctUntilChanged()
+        }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
index d65a69c..9ee3d22 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
@@ -64,13 +64,13 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.settingslib.volume.MediaSessions;
 import com.android.systemui.Dumpable;
-import com.android.systemui.res.R;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.plugins.VolumeDialogController;
 import com.android.systemui.qs.tiles.DndTile;
+import com.android.systemui.res.R;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.util.RingerModeLiveData;
@@ -99,6 +99,7 @@
 @SysUISingleton
 public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpable {
     private static final String TAG = Util.logTag(VolumeDialogControllerImpl.class);
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
     private static final int TOUCH_FEEDBACK_TIMEOUT_MS = 1000;
     private static final int DYNAMIC_STREAM_START_INDEX = 100;
@@ -1339,14 +1340,24 @@
 
         private boolean showForSession(Token token) {
             if (mVolumeAdjustmentForRemoteGroupSessions) {
+                if (DEBUG) {
+                    Log.d(TAG, "Volume adjustment for remote group sessions allowed,"
+                            + " showForSession: true");
+                }
                 return true;
             }
             MediaController ctr = new MediaController(mContext, token);
             String packageName = ctr.getPackageName();
             List<RoutingSessionInfo> sessions =
                     mRouter2Manager.getRoutingSessions(packageName);
-
+            if (DEBUG) {
+                Log.d(TAG, "Found " + sessions.size() + " routing sessions for package name "
+                        + packageName);
+            }
             for (RoutingSessionInfo session : sessions) {
+                if (DEBUG) {
+                    Log.d(TAG, "Found routingSessionInfo: " + session);
+                }
                 if (!session.isSystemSession()
                         && session.getVolumeHandling() != MediaRoute2Info.PLAYBACK_VOLUME_FIXED) {
                     return true;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryImplTest.kt
index d1a46fc..057dcb2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/data/repository/StatusBarModeRepositoryImplTest.kt
@@ -62,7 +62,7 @@
     private val ongoingCallRepository = OngoingCallRepository()
 
     private val underTest =
-        StatusBarModeRepositoryImpl(
+        StatusBarModePerDisplayRepositoryImpl(
                 testScope.backgroundScope,
                 DISPLAY_ID,
                 commandQueue,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightsOutNotifControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyLightsOutNotifControllerTest.java
similarity index 97%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightsOutNotifControllerTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyLightsOutNotifControllerTest.java
index 287ebba..bde2243 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightsOutNotifControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LegacyLightsOutNotifControllerTest.java
@@ -54,7 +54,7 @@
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @RunWithLooper
-public class LightsOutNotifControllerTest extends SysuiTestCase {
+public class LegacyLightsOutNotifControllerTest extends SysuiTestCase {
     private static final int LIGHTS_ON = 0;
     private static final int LIGHTS_OUT = APPEARANCE_LOW_PROFILE_BARS;
 
@@ -68,7 +68,7 @@
     @Captor private ArgumentCaptor<CommandQueue.Callbacks> mCallbacksCaptor;
 
     private View mLightsOutView;
-    private LightsOutNotifController mLightsOutNotifController;
+    private LegacyLightsOutNotifController mLightsOutNotifController;
     private int mDisplayId;
     private Observer<Boolean> mHaActiveNotifsObserver;
     private CommandQueue.Callbacks mCallbacks;
@@ -83,7 +83,7 @@
         when(mNotifLiveDataStore.getHasActiveNotifs()).thenReturn(mHasActiveNotifs);
         when(mHasActiveNotifs.getValue()).thenReturn(false);
 
-        mLightsOutNotifController = new LightsOutNotifController(
+        mLightsOutNotifController = new LegacyLightsOutNotifController(
                 mLightsOutView,
                 mWindowManager,
                 mNotifLiveDataStore,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java
index c45ecf3..f6419a9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarControllerTest.java
@@ -121,7 +121,7 @@
                 new AppearanceRegion(APPEARANCE_LIGHT_STATUS_BARS, secondBounds)
         );
 
-        mStatusBarModeRepository.getStatusBarAppearance().setValue(
+        mStatusBarModeRepository.getDefaultDisplay().getStatusBarAppearance().setValue(
                 new StatusBarAppearance(
                         StatusBarMode.TRANSPARENT,
                         STATUS_BAR_BOUNDS,
@@ -142,7 +142,7 @@
                 new AppearanceRegion(0 /* appearance */, secondBounds)
         );
 
-        mStatusBarModeRepository.getStatusBarAppearance().setValue(
+        mStatusBarModeRepository.getDefaultDisplay().getStatusBarAppearance().setValue(
                 new StatusBarAppearance(
                         StatusBarMode.TRANSPARENT,
                         STATUS_BAR_BOUNDS,
@@ -165,7 +165,7 @@
                 new AppearanceRegion(APPEARANCE_LIGHT_STATUS_BARS, secondBounds)
         );
 
-        mStatusBarModeRepository.getStatusBarAppearance().setValue(
+        mStatusBarModeRepository.getDefaultDisplay().getStatusBarAppearance().setValue(
                 new StatusBarAppearance(
                         StatusBarMode.TRANSPARENT,
                         STATUS_BAR_BOUNDS,
@@ -190,7 +190,7 @@
                 new AppearanceRegion(APPEARANCE_LIGHT_STATUS_BARS, thirdBounds)
         );
 
-        mStatusBarModeRepository.getStatusBarAppearance().setValue(
+        mStatusBarModeRepository.getDefaultDisplay().getStatusBarAppearance().setValue(
                 new StatusBarAppearance(
                         StatusBarMode.TRANSPARENT,
                         STATUS_BAR_BOUNDS,
@@ -214,7 +214,7 @@
                 new AppearanceRegion(0 /* appearance */, secondBounds)
         );
 
-        mStatusBarModeRepository.getStatusBarAppearance().setValue(
+        mStatusBarModeRepository.getDefaultDisplay().getStatusBarAppearance().setValue(
                 new StatusBarAppearance(
                         StatusBarMode.TRANSPARENT,
                         STATUS_BAR_BOUNDS,
@@ -231,7 +231,7 @@
                 new AppearanceRegion(APPEARANCE_LIGHT_STATUS_BARS, new Rect(0, 0, 1, 1))
         );
 
-        mStatusBarModeRepository.getStatusBarAppearance().setValue(
+        mStatusBarModeRepository.getDefaultDisplay().getStatusBarAppearance().setValue(
                 new StatusBarAppearance(
                         StatusBarMode.TRANSPARENT,
                         STATUS_BAR_BOUNDS,
@@ -249,7 +249,7 @@
                 new AppearanceRegion(0, new Rect(0, 0, 1, 1))
         );
 
-        mStatusBarModeRepository.getStatusBarAppearance().setValue(
+        mStatusBarModeRepository.getDefaultDisplay().getStatusBarAppearance().setValue(
                 new StatusBarAppearance(
                         StatusBarMode.TRANSPARENT,
                         STATUS_BAR_BOUNDS,
@@ -266,7 +266,7 @@
                 new AppearanceRegion(APPEARANCE_LIGHT_STATUS_BARS, new Rect(0, 0, 1, 1))
         );
 
-        mStatusBarModeRepository.getStatusBarAppearance().setValue(
+        mStatusBarModeRepository.getDefaultDisplay().getStatusBarAppearance().setValue(
                 new StatusBarAppearance(
                         StatusBarMode.TRANSPARENT,
                         STATUS_BAR_BOUNDS,
@@ -276,7 +276,7 @@
         reset(mStatusBarIconController);
 
         // WHEN the same appearance regions but different status bar mode is sent
-        mStatusBarModeRepository.getStatusBarAppearance().setValue(
+        mStatusBarModeRepository.getDefaultDisplay().getStatusBarAppearance().setValue(
                 new StatusBarAppearance(
                         StatusBarMode.LIGHTS_OUT_TRANSPARENT,
                         STATUS_BAR_BOUNDS,
@@ -298,7 +298,7 @@
                 /* start= */ new Rect(0, 0, 10, 10),
                 /* end= */ new Rect(0, 0, 20, 20));
 
-        mStatusBarModeRepository.getStatusBarAppearance().setValue(
+        mStatusBarModeRepository.getDefaultDisplay().getStatusBarAppearance().setValue(
                 new StatusBarAppearance(
                         StatusBarMode.TRANSPARENT,
                         startingBounds,
@@ -311,7 +311,7 @@
         BoundsPair newBounds = new BoundsPair(
                 /* start= */ new Rect(0, 0, 30, 30),
                 /* end= */ new Rect(0, 0, 40, 40));
-        mStatusBarModeRepository.getStatusBarAppearance().setValue(
+        mStatusBarModeRepository.getDefaultDisplay().getStatusBarAppearance().setValue(
                 new StatusBarAppearance(
                         StatusBarMode.TRANSPARENT,
                         newBounds,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/domain/interactor/LightsOutInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/domain/interactor/LightsOutInteractorTest.kt
new file mode 100644
index 0000000..5a0e13d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/domain/interactor/LightsOutInteractorTest.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.systemui.statusbar.phone.domain.interactor
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.statusbar.data.model.StatusBarMode
+import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository
+import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository.Companion.DISPLAY_ID
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+@SmallTest
+class LightsOutInteractorTest : SysuiTestCase() {
+
+    private val statusBarModeRepository = FakeStatusBarModeRepository()
+    private val interactor: LightsOutInteractor = LightsOutInteractor(statusBarModeRepository)
+
+    @Test
+    fun isLowProfile_lightsOutStatusBarMode_false() = runTest {
+        statusBarModeRepository.defaultDisplay.statusBarMode.value = StatusBarMode.LIGHTS_OUT
+
+        val actual by collectLastValue(interactor.isLowProfile(DISPLAY_ID))
+
+        assertThat(actual).isTrue()
+    }
+
+    @Test
+    fun isLowProfile_lightsOutTransparentStatusBarMode_true() = runTest {
+        statusBarModeRepository.defaultDisplay.statusBarMode.value =
+            StatusBarMode.LIGHTS_OUT_TRANSPARENT
+
+        val actual by collectLastValue(interactor.isLowProfile(DISPLAY_ID))
+
+        assertThat(actual).isTrue()
+    }
+
+    @Test
+    fun isLowProfile_transparentStatusBarMode_false() = runTest {
+        statusBarModeRepository.defaultDisplay.statusBarMode.value = StatusBarMode.TRANSPARENT
+
+        val actual by collectLastValue(interactor.isLowProfile(DISPLAY_ID))
+
+        assertThat(actual).isFalse()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
index 0b87fe8..17c2938 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
@@ -46,7 +46,6 @@
 import com.android.systemui.common.ui.ConfigurationState;
 import com.android.systemui.demomode.DemoModeController;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlagsClassic;
 import com.android.systemui.log.LogBuffer;
 import com.android.systemui.log.LogcatEchoTracker;
 import com.android.systemui.plugins.DarkIconDispatcher;
@@ -695,7 +694,6 @@
                 mLocationPublisher,
                 mMockNotificationAreaController,
                 mShadeExpansionStateManager,
-                mock(FeatureFlagsClassic.class),
                 mStatusBarIconController,
                 mIconManagerFactory,
                 mCollapsedStatusBarViewModel,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
index 49de512..7b73528c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
@@ -549,7 +549,7 @@
     @Test
     fun fullscreenIsTrue_chipStillClickable() {
         notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
-        statusBarModeRepository.isInFullscreenMode.value = true
+        statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
         testScope.runCurrent()
 
         assertThat(chipView.hasOnClickListeners()).isTrue()
@@ -559,7 +559,7 @@
 
     @Test
     fun callStartedInImmersiveMode_swipeGestureCallbackAdded() {
-        statusBarModeRepository.isInFullscreenMode.value = true
+        statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
         testScope.runCurrent()
 
         notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
@@ -570,7 +570,7 @@
 
     @Test
     fun callStartedNotInImmersiveMode_swipeGestureCallbackNotAdded() {
-        statusBarModeRepository.isInFullscreenMode.value = false
+        statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = false
         testScope.runCurrent()
 
         notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
@@ -583,7 +583,7 @@
     fun transitionToImmersiveMode_swipeGestureCallbackAdded() {
         notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
 
-        statusBarModeRepository.isInFullscreenMode.value = true
+        statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
         testScope.runCurrent()
 
         verify(mockSwipeStatusBarAwayGestureHandler)
@@ -592,11 +592,11 @@
 
     @Test
     fun transitionOutOfImmersiveMode_swipeGestureCallbackRemoved() {
-        statusBarModeRepository.isInFullscreenMode.value = true
+        statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
         notifCollectionListener.onEntryUpdated(createOngoingCallNotifEntry())
         reset(mockSwipeStatusBarAwayGestureHandler)
 
-        statusBarModeRepository.isInFullscreenMode.value = false
+        statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = false
         testScope.runCurrent()
 
         verify(mockSwipeStatusBarAwayGestureHandler)
@@ -605,7 +605,7 @@
 
     @Test
     fun callEndedWhileInImmersiveMode_swipeGestureCallbackRemoved() {
-        statusBarModeRepository.isInFullscreenMode.value = true
+        statusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true
         testScope.runCurrent()
         val ongoingCallNotifEntry = createOngoingCallNotifEntry()
         notifCollectionListener.onEntryAdded(ongoingCallNotifEntry)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
index 688f739..09dc1e5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
@@ -17,49 +17,77 @@
 package com.android.systemui.statusbar.pipeline.shared.ui.viewmodel
 
 import androidx.test.filters.SmallTest
+import com.android.systemui.CoroutineTestScopeModule
+import com.android.systemui.Flags
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectValues
+import com.android.systemui.collectLastValue
+import com.android.systemui.collectValues
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
-import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.runTest
+import com.android.systemui.statusbar.data.model.StatusBarMode
+import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository
+import com.android.systemui.statusbar.data.repository.FakeStatusBarModeRepository.Companion.DISPLAY_ID
+import com.android.systemui.statusbar.notification.data.model.activeNotificationModel
+import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
+import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
+import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel
 import com.google.common.truth.Truth.assertThat
+import dagger.BindsInstance
+import dagger.Component
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
 
-@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 class CollapsedStatusBarViewModelImplTest : SysuiTestCase() {
 
-    private lateinit var underTest: CollapsedStatusBarViewModel
+    @SysUISingleton
+    @Component(
+        modules =
+            [
+                SysUITestModule::class,
+            ]
+    )
+    interface TestComponent : SysUITestComponent<CollapsedStatusBarViewModelImpl> {
+        val statusBarModeRepository: FakeStatusBarModeRepository
+        val activeNotificationListRepository: ActiveNotificationListRepository
+        val keyguardTransitionRepository: FakeKeyguardTransitionRepository
 
-    private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository
-    private lateinit var testScope: TestScope
+        @Component.Factory
+        interface Factory {
+            fun create(
+                @BindsInstance test: SysuiTestCase,
+                testScope: CoroutineTestScopeModule,
+            ): TestComponent
+        }
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    private val testComponent: TestComponent =
+        DaggerCollapsedStatusBarViewModelImplTest_TestComponent.factory()
+            .create(
+                test = this,
+                testScope = CoroutineTestScopeModule(TestScope(UnconfinedTestDispatcher())),
+            )
 
     @Before
     fun setUp() {
-        testScope = TestScope(UnconfinedTestDispatcher())
-
-        keyguardTransitionRepository = FakeKeyguardTransitionRepository()
-        val interactor =
-            KeyguardTransitionInteractorFactory.create(
-                    scope = TestScope().backgroundScope,
-                    repository = keyguardTransitionRepository,
-                )
-                .keyguardTransitionInteractor
-        underTest = CollapsedStatusBarViewModelImpl(interactor, testScope.backgroundScope)
+        mSetFlagsRule.enableFlags(Flags.FLAG_NOTIFICATIONS_LIVE_DATA_STORE_REFACTOR)
     }
 
     @Test
     fun isTransitioningFromLockscreenToOccluded_started_isTrue() =
-        testScope.runTest {
-            val job = underTest.isTransitioningFromLockscreenToOccluded.launchIn(this)
+        testComponent.runTest {
+            val job = underTest.isTransitioningFromLockscreenToOccluded.launchIn(testScope)
 
             keyguardTransitionRepository.sendTransitionStep(
                 TransitionStep(
@@ -77,8 +105,8 @@
 
     @Test
     fun isTransitioningFromLockscreenToOccluded_running_isTrue() =
-        testScope.runTest {
-            val job = underTest.isTransitioningFromLockscreenToOccluded.launchIn(this)
+        testComponent.runTest {
+            val job = underTest.isTransitioningFromLockscreenToOccluded.launchIn(testScope)
 
             keyguardTransitionRepository.sendTransitionStep(
                 TransitionStep(
@@ -96,13 +124,13 @@
 
     @Test
     fun isTransitioningFromLockscreenToOccluded_finished_isFalse() =
-        testScope.runTest {
-            val job = underTest.isTransitioningFromLockscreenToOccluded.launchIn(this)
+        testComponent.runTest {
+            val job = underTest.isTransitioningFromLockscreenToOccluded.launchIn(testScope)
 
             keyguardTransitionRepository.sendTransitionSteps(
                 from = KeyguardState.LOCKSCREEN,
                 to = KeyguardState.OCCLUDED,
-                this.testScheduler,
+                testScope.testScheduler,
             )
 
             assertThat(underTest.isTransitioningFromLockscreenToOccluded.value).isFalse()
@@ -112,8 +140,8 @@
 
     @Test
     fun isTransitioningFromLockscreenToOccluded_canceled_isFalse() =
-        testScope.runTest {
-            val job = underTest.isTransitioningFromLockscreenToOccluded.launchIn(this)
+        testComponent.runTest {
+            val job = underTest.isTransitioningFromLockscreenToOccluded.launchIn(testScope)
 
             keyguardTransitionRepository.sendTransitionStep(
                 TransitionStep(
@@ -131,8 +159,8 @@
 
     @Test
     fun isTransitioningFromLockscreenToOccluded_irrelevantTransition_isFalse() =
-        testScope.runTest {
-            val job = underTest.isTransitioningFromLockscreenToOccluded.launchIn(this)
+        testComponent.runTest {
+            val job = underTest.isTransitioningFromLockscreenToOccluded.launchIn(testScope)
 
             keyguardTransitionRepository.sendTransitionStep(
                 TransitionStep(
@@ -150,8 +178,8 @@
 
     @Test
     fun isTransitioningFromLockscreenToOccluded_followsRepoUpdates() =
-        testScope.runTest {
-            val job = underTest.isTransitioningFromLockscreenToOccluded.launchIn(this)
+        testComponent.runTest {
+            val job = underTest.isTransitioningFromLockscreenToOccluded.launchIn(testScope)
 
             keyguardTransitionRepository.sendTransitionStep(
                 TransitionStep(
@@ -182,7 +210,7 @@
 
     @Test
     fun transitionFromLockscreenToDreamStartedEvent_started_emitted() =
-        testScope.runTest {
+        testComponent.runTest {
             val emissions by collectValues(underTest.transitionFromLockscreenToDreamStartedEvent)
 
             keyguardTransitionRepository.sendTransitionStep(
@@ -199,7 +227,7 @@
 
     @Test
     fun transitionFromLockscreenToDreamStartedEvent_startedMultiple_emittedMultiple() =
-        testScope.runTest {
+        testComponent.runTest {
             val emissions by collectValues(underTest.transitionFromLockscreenToDreamStartedEvent)
 
             keyguardTransitionRepository.sendTransitionStep(
@@ -234,7 +262,7 @@
 
     @Test
     fun transitionFromLockscreenToDreamStartedEvent_startedThenRunning_emittedOnlyOne() =
-        testScope.runTest {
+        testComponent.runTest {
             val emissions by collectValues(underTest.transitionFromLockscreenToDreamStartedEvent)
 
             keyguardTransitionRepository.sendTransitionStep(
@@ -283,7 +311,7 @@
 
     @Test
     fun transitionFromLockscreenToDreamStartedEvent_irrelevantTransition_notEmitted() =
-        testScope.runTest {
+        testComponent.runTest {
             val emissions by collectValues(underTest.transitionFromLockscreenToDreamStartedEvent)
 
             keyguardTransitionRepository.sendTransitionStep(
@@ -300,7 +328,7 @@
 
     @Test
     fun transitionFromLockscreenToDreamStartedEvent_irrelevantTransitionState_notEmitted() =
-        testScope.runTest {
+        testComponent.runTest {
             val emissions by collectValues(underTest.transitionFromLockscreenToDreamStartedEvent)
 
             keyguardTransitionRepository.sendTransitionStep(
@@ -317,4 +345,65 @@
 
             assertThat(emissions).isEmpty()
         }
+
+    @Test
+    fun areNotificationsLightsOut_lowProfileWithNotifications_true() =
+        testComponent.runTest {
+            statusBarModeRepository.defaultDisplay.statusBarMode.value =
+                StatusBarMode.LIGHTS_OUT_TRANSPARENT
+            activeNotificationListRepository.activeNotifications.value =
+                activeNotificationsStore(testNotifications)
+
+            val actual by collectLastValue(underTest.areNotificationsLightsOut(DISPLAY_ID))
+
+            assertThat(actual).isTrue()
+        }
+
+    @Test
+    fun areNotificationsLightsOut_lowProfileWithoutNotifications_false() =
+        testComponent.runTest {
+            statusBarModeRepository.defaultDisplay.statusBarMode.value =
+                StatusBarMode.LIGHTS_OUT_TRANSPARENT
+            activeNotificationListRepository.activeNotifications.value =
+                activeNotificationsStore(emptyList())
+
+            val actual by collectLastValue(underTest.areNotificationsLightsOut(DISPLAY_ID))
+
+            assertThat(actual).isFalse()
+        }
+
+    @Test
+    fun areNotificationsLightsOut_defaultStatusBarModeWithoutNotifications_false() =
+        testComponent.runTest {
+            statusBarModeRepository.defaultDisplay.statusBarMode.value = StatusBarMode.TRANSPARENT
+            activeNotificationListRepository.activeNotifications.value =
+                activeNotificationsStore(emptyList())
+
+            val actual by collectLastValue(underTest.areNotificationsLightsOut(DISPLAY_ID))
+
+            assertThat(actual).isFalse()
+        }
+
+    @Test
+    fun areNotificationsLightsOut_defaultStatusBarModeWithNotifications_false() =
+        testComponent.runTest {
+            statusBarModeRepository.defaultDisplay.statusBarMode.value = StatusBarMode.TRANSPARENT
+            activeNotificationListRepository.activeNotifications.value =
+                activeNotificationsStore(testNotifications)
+
+            val actual by collectLastValue(underTest.areNotificationsLightsOut(DISPLAY_ID))
+
+            assertThat(actual).isFalse()
+        }
+
+    private fun activeNotificationsStore(notifications: List<ActiveNotificationModel>) =
+        ActiveNotificationsStore.Builder()
+            .apply { notifications.forEach(::addIndividualNotif) }
+            .build()
+
+    private val testNotifications =
+        listOf(
+            activeNotificationModel(key = "notif1"),
+            activeNotificationModel(key = "notif2"),
+        )
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeCollapsedStatusBarViewModel.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeCollapsedStatusBarViewModel.kt
index 88587b2..bc50f79 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeCollapsedStatusBarViewModel.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/FakeCollapsedStatusBarViewModel.kt
@@ -16,11 +16,20 @@
 
 package com.android.systemui.statusbar.pipeline.shared.ui.viewmodel
 
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
 
 class FakeCollapsedStatusBarViewModel : CollapsedStatusBarViewModel {
+    private val areNotificationLightsOut = MutableStateFlow(false)
+
     override val isTransitioningFromLockscreenToOccluded = MutableStateFlow(false)
 
     override val transitionFromLockscreenToDreamStartedEvent = MutableSharedFlow<Unit>()
+
+    override fun areNotificationsLightsOut(displayId: Int): Flow<Boolean> = areNotificationLightsOut
+
+    fun setNotificationLightsOut(lightsOut: Boolean) {
+        areNotificationLightsOut.value = lightsOut
+    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeStatusBarModeRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeStatusBarModeRepository.kt
index f25d282..6069083 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeStatusBarModeRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeStatusBarModeRepository.kt
@@ -16,14 +16,33 @@
 
 package com.android.systemui.statusbar.data.repository
 
+import android.view.Display
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.statusbar.data.model.StatusBarAppearance
 import com.android.systemui.statusbar.data.model.StatusBarMode
+import com.google.common.truth.Truth.assertThat
 import dagger.Binds
 import dagger.Module
 import javax.inject.Inject
 import kotlinx.coroutines.flow.MutableStateFlow
 
-class FakeStatusBarModeRepository @Inject constructor() : StatusBarModeRepository {
+@SysUISingleton
+class FakeStatusBarModeRepository @Inject constructor() : StatusBarModeRepositoryStore {
+
+    companion object {
+        const val DISPLAY_ID = Display.DEFAULT_DISPLAY
+    }
+
+    override val defaultDisplay: FakeStatusBarModePerDisplayRepository =
+        FakeStatusBarModePerDisplayRepository()
+
+    override fun forDisplay(displayId: Int): FakeStatusBarModePerDisplayRepository {
+        assertThat(displayId).isEqualTo(DISPLAY_ID)
+        return defaultDisplay
+    }
+}
+
+class FakeStatusBarModePerDisplayRepository : StatusBarModePerDisplayRepository {
     override val isTransientShown = MutableStateFlow(false)
     override val isInFullscreenMode = MutableStateFlow(false)
     override val statusBarAppearance = MutableStateFlow<StatusBarAppearance?>(null)
@@ -39,5 +58,5 @@
 
 @Module
 interface FakeStatusBarModeRepositoryModule {
-    @Binds fun bindFake(fake: FakeStatusBarModeRepository): StatusBarModeRepository
+    @Binds fun bindFake(fake: FakeStatusBarModeRepository): StatusBarModeRepositoryStore
 }
diff --git a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java
index 5635dd5..42ab05f 100644
--- a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java
+++ b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java
@@ -21,6 +21,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.ActivityOptions;
 import android.app.Dialog;
 import android.app.PendingIntent;
 import android.content.ComponentName;
@@ -205,7 +206,10 @@
                         intent,
                         PendingIntent.FLAG_MUTABLE
                                 | PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT,
-                        /* options= */ null, UserHandle.CURRENT);
+                        ActivityOptions.makeBasic()
+                                .setPendingIntentCreatorBackgroundActivityStartMode(
+                                        ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED)
+                                .toBundle(), UserHandle.CURRENT);
                 if (sDebug) {
                     Slog.d(TAG, "startActivity add save UI restored with intent=" + intent);
                 }
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index 215970e..e51ef29 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -864,6 +864,23 @@
         }
 
         @Override
+        public @NonNull Set<String> getAllPersistentDeviceIds() {
+            Set<String> persistentIds = new ArraySet<>();
+            synchronized (mVirtualDeviceManagerLock) {
+                for (int i = 0; i < mActiveAssociations.size(); ++i) {
+                    AssociationInfo associationInfo = mActiveAssociations.get(i);
+                    if (VIRTUAL_DEVICE_COMPANION_DEVICE_PROFILES.contains(
+                            associationInfo.getDeviceProfile())) {
+                        persistentIds.add(
+                                VirtualDeviceImpl.createPersistentDeviceId(
+                                        associationInfo.getId()));
+                    }
+                }
+            }
+            return persistentIds;
+        }
+
+        @Override
         public void registerAppsOnVirtualDeviceListener(
                 @NonNull AppsOnVirtualDeviceListener listener) {
             synchronized (mVirtualDeviceManagerLock) {
diff --git a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java
index a570d09..6940ffe 100644
--- a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java
+++ b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java
@@ -69,6 +69,11 @@
             }
 
             @Override
+            public void onProcessCaptureRequest(int streamId, int frameId) throws RemoteException {
+                camera.onProcessCaptureRequest(streamId, frameId, /*metadata=*/ null);
+            }
+
+            @Override
             public void onStreamClosed(int streamId) throws RemoteException {
                 camera.onStreamClosed(streamId);
             }
diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java
index ea92154..61e4f36 100644
--- a/services/core/java/com/android/server/audio/SpatializerHelper.java
+++ b/services/core/java/com/android/server/audio/SpatializerHelper.java
@@ -19,6 +19,9 @@
 import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_HEADPHONES;
 import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_UNKNOWN;
 import static android.media.AudioSystem.isBluetoothDevice;
+import static android.media.AudioSystem.isBluetoothLeDevice;
+
+import static com.android.media.audio.Flags.dsaOverBtLeAudio;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -1625,10 +1628,10 @@
     }
 
     private int getHeadSensorHandleUpdateTracker() {
-        int headHandle = -1;
+        Sensor htSensor = null;
         if (sRoutingDevices.isEmpty()) {
             logloge("getHeadSensorHandleUpdateTracker: no device, no head tracker");
-            return headHandle;
+            return  -1;
         }
         final AudioDeviceAttributes currentDevice = sRoutingDevices.get(0);
         List<String> deviceAddresses = mAudioService.getDeviceAddresses(currentDevice);
@@ -1642,27 +1645,86 @@
         for (String address : deviceAddresses) {
             UUID routingDeviceUuid = UuidUtils.uuidFromAudioDeviceAttributes(
                     new AudioDeviceAttributes(currentDevice.getInternalType(), address));
-            for (Sensor sensor : sensors) {
-                final UUID uuid = sensor.getUuid();
-                if (uuid.equals(routingDeviceUuid)) {
-                    headHandle = sensor.getHandle();
-                    if (!setHasHeadTracker(currentDevice)) {
-                        headHandle = -1;
+            if (dsaOverBtLeAudio()) {
+                for (Sensor sensor : sensors) {
+                    final UUID uuid = sensor.getUuid();
+                    if (uuid.equals(routingDeviceUuid)) {
+                        htSensor = sensor;
+                        HeadtrackerInfo info = new HeadtrackerInfo(sensor);
+                        if (isBluetoothLeDevice(currentDevice.getInternalType())) {
+                            if (info.getMajorVersion() == 2) {
+                                // Version 2 is used only by LE Audio profile
+                                break;
+                            }
+                            // we do not break, as this could be a match on the A2DP sensor
+                            // for a dual mode headset.
+                        } else if (info.getMajorVersion() == 1) {
+                            // Version 1 is used only by A2DP profile
+                            break;
+                        }
                     }
+                    if (htSensor == null && uuid.equals(UuidUtils.STANDALONE_UUID)) {
+                        htSensor = sensor;
+                        // we do not break, perhaps we find a head tracker on device.
+                    }
+                }
+                if (htSensor != null) {
+                    if (htSensor.getUuid().equals(UuidUtils.STANDALONE_UUID)) {
+                        break;
+                    }
+                    if (setHasHeadTracker(currentDevice)) {
+                        break;
+                    } else {
+                        htSensor = null;
+                    }
+                }
+            } else {
+                for (Sensor sensor : sensors) {
+                    final UUID uuid = sensor.getUuid();
+                    if (uuid.equals(routingDeviceUuid)) {
+                        htSensor = sensor;
+                        if (!setHasHeadTracker(currentDevice)) {
+                            htSensor = null;
+                        }
+                        break;
+                    }
+                    if (uuid.equals(UuidUtils.STANDALONE_UUID)) {
+                        htSensor = sensor;
+                        // we do not break, perhaps we find a head tracker on device.
+                    }
+                }
+                if (htSensor != null) {
                     break;
                 }
-                if (uuid.equals(UuidUtils.STANDALONE_UUID)) {
-                    headHandle = sensor.getHandle();
-                    // we do not break, perhaps we find a head tracker on device.
-                }
-            }
-            if (headHandle != -1) {
-                break;
             }
         }
-        return headHandle;
+        return htSensor != null ? htSensor.getHandle() : -1;
     }
 
+    /**
+     * Contains the information parsed from the head tracker sensor version.
+     * See platform/hardware/libhardware/modules/sensors/dynamic_sensor/HidRawSensor.h
+     * for the definition of version and capability fields.
+     */
+    private static class HeadtrackerInfo {
+        private final int mVersion;
+        HeadtrackerInfo(Sensor sensor) {
+            mVersion = sensor.getVersion();
+        }
+        int getMajorVersion() {
+            return (mVersion & 0xFF000000) >> 24;
+        }
+        int getMinorVersion() {
+            return (mVersion & 0xFF0000) >> 16;
+        }
+        boolean hasAclTransport() {
+            return getMajorVersion() == 2 ? ((mVersion & 0x1) != 0) : false;
+        }
+        boolean hasIsoTransport() {
+            return getMajorVersion() == 2 ? ((mVersion & 0x2) != 0) : false;
+        }
+    };
+
     private int getScreenSensorHandle() {
         int screenHandle = -1;
         Sensor screenSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
diff --git a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
index c629b2b..be78ea2 100644
--- a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
+++ b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
@@ -157,4 +157,10 @@
      * @see VirtualDevice#getPersistentDeviceId()
      */
     public abstract @Nullable String getPersistentIdForDevice(int deviceId);
+
+    /**
+     * Returns all current persistent device IDs, including the ones for which no virtual device
+     * exists, as long as one may have existed or can be created.
+     */
+    public abstract @NonNull Set<String> getAllPersistentDeviceIds();
 }
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 0492f43..c2b5964 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -184,6 +184,7 @@
 import android.companion.ICompanionDeviceManager;
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.EnabledAfter;
+import android.compat.annotation.EnabledSince;
 import android.compat.annotation.LoggingOnly;
 import android.content.AttributionSource;
 import android.content.BroadcastReceiver;
@@ -555,7 +556,7 @@
      * creation and activation of an implicit {@link android.app.AutomaticZenRule}.
      */
     @ChangeId
-    @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
     static final long MANAGE_GLOBAL_ZEN_VIA_IMPLICIT_RULES = 308670109L;
 
     private static final Duration POST_WAKE_LOCK_TIMEOUT = Duration.ofSeconds(30);
diff --git a/services/core/java/com/android/server/wm/ClientLifecycleManager.java b/services/core/java/com/android/server/wm/ClientLifecycleManager.java
index 28f656e..8b282dd3 100644
--- a/services/core/java/com/android/server/wm/ClientLifecycleManager.java
+++ b/services/core/java/com/android/server/wm/ClientLifecycleManager.java
@@ -58,6 +58,23 @@
     }
 
     /**
+     * Similar to {@link #scheduleTransactionItem}, but is called without WM lock.
+     *
+     * @see WindowProcessController#setReportedProcState(int)
+     */
+    void scheduleTransactionItemUnlocked(@NonNull IApplicationThread client,
+            @NonNull ClientTransactionItem transactionItem) throws RemoteException {
+        // Immediately dispatching to client, and must not access WMS.
+        final ClientTransaction clientTransaction = ClientTransaction.obtain(client);
+        if (transactionItem.isActivityLifecycleItem()) {
+            clientTransaction.setLifecycleStateRequest((ActivityLifecycleItem) transactionItem);
+        } else {
+            clientTransaction.addCallback(transactionItem);
+        }
+        scheduleTransaction(clientTransaction);
+    }
+
+    /**
      * Schedules a single transaction item, either a callback or a lifecycle request, delivery to
      * client application.
      * @throws RemoteException
@@ -65,6 +82,7 @@
      */
     void scheduleTransactionItem(@NonNull IApplicationThread client,
             @NonNull ClientTransactionItem transactionItem) throws RemoteException {
+        // TODO(b/260873529): queue the transaction items.
         final ClientTransaction clientTransaction = ClientTransaction.obtain(client);
         if (transactionItem.isActivityLifecycleItem()) {
             clientTransaction.setLifecycleStateRequest((ActivityLifecycleItem) transactionItem);
@@ -82,6 +100,7 @@
     void scheduleTransactionAndLifecycleItems(@NonNull IApplicationThread client,
             @NonNull ClientTransactionItem transactionItem,
             @NonNull ActivityLifecycleItem lifecycleItem) throws RemoteException {
+        // TODO(b/260873529): replace with #scheduleTransactionItem after launch for cleanup.
         final ClientTransaction clientTransaction = ClientTransaction.obtain(client);
         clientTransaction.addCallback(transactionItem);
         clientTransaction.setLifecycleStateRequest(lifecycleItem);
diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java
index 558bf9d..2b18f07 100644
--- a/services/core/java/com/android/server/wm/WindowProcessController.java
+++ b/services/core/java/com/android/server/wm/WindowProcessController.java
@@ -388,13 +388,22 @@
         final IApplicationThread thread = mThread;
         if (prevProcState >= CACHED_CONFIG_PROC_STATE && repProcState < CACHED_CONFIG_PROC_STATE
                 && thread != null && mHasCachedConfiguration) {
-            final Configuration config;
+            final ConfigurationChangeItem configurationChangeItem;
             synchronized (mLastReportedConfiguration) {
-                config = new Configuration(mLastReportedConfiguration);
+                onConfigurationChangePreScheduled(mLastReportedConfiguration);
+                configurationChangeItem = ConfigurationChangeItem.obtain(
+                        mLastReportedConfiguration, mLastTopActivityDeviceId);
             }
             // Schedule immediately to make sure the app component (e.g. receiver, service) can get
             // the latest configuration in their lifecycle callbacks (e.g. onReceive, onCreate).
-            scheduleConfigurationChange(thread, config);
+            try {
+                // No WM lock here.
+                mAtm.getLifecycleManager().scheduleTransactionItemUnlocked(
+                        thread, configurationChangeItem);
+            } catch (Exception e) {
+                Slog.e(TAG_CONFIGURATION, "Failed to schedule ConfigurationChangeItem="
+                        + configurationChangeItem + " owner=" + mOwner, e);
+            }
         }
     }
 
@@ -1634,11 +1643,12 @@
             }
         }
 
-        scheduleConfigurationChange(thread, config);
+        onConfigurationChangePreScheduled(config);
+        scheduleClientTransactionItem(thread, ConfigurationChangeItem.obtain(
+                config, mLastTopActivityDeviceId));
     }
 
-    private void scheduleConfigurationChange(@NonNull IApplicationThread thread,
-            @NonNull Configuration config) {
+    private void onConfigurationChangePreScheduled(@NonNull Configuration config) {
         ProtoLog.v(WM_DEBUG_CONFIGURATION, "Sending to proc %s new config %s", mName,
                 config);
         if (Build.IS_DEBUGGABLE && mHasImeService) {
@@ -1646,8 +1656,6 @@
             Slog.v(TAG_CONFIGURATION, "Sending to IME proc " + mName + " new config " + config);
         }
         mHasCachedConfiguration = false;
-        scheduleClientTransactionItem(thread, ConfigurationChangeItem.obtain(
-                config, mLastTopActivityDeviceId));
     }
 
     @VisibleForTesting
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index d87b8d1..b5ba322 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -773,6 +773,30 @@
     }
 
     @Test
+    public void getAllPersistentDeviceIds_respectsCurrentAssociations() {
+        mVdms.onCdmAssociationsChanged(List.of(mAssociationInfo));
+        TestableLooper.get(this).processAllMessages();
+
+        assertThat(mLocalService.getAllPersistentDeviceIds())
+                .containsExactly(mDeviceImpl.getPersistentDeviceId());
+
+        mVdms.onCdmAssociationsChanged(List.of(
+                createAssociationInfo(2, AssociationRequest.DEVICE_PROFILE_APP_STREAMING),
+                createAssociationInfo(3, AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION),
+                createAssociationInfo(4, AssociationRequest.DEVICE_PROFILE_WATCH)));
+        TestableLooper.get(this).processAllMessages();
+
+        assertThat(mLocalService.getAllPersistentDeviceIds()).containsExactly(
+                VirtualDeviceImpl.createPersistentDeviceId(2),
+                VirtualDeviceImpl.createPersistentDeviceId(3));
+
+        mVdms.onCdmAssociationsChanged(Collections.emptyList());
+        TestableLooper.get(this).processAllMessages();
+
+        assertThat(mLocalService.getAllPersistentDeviceIds()).isEmpty();
+    }
+
+    @Test
     public void onAppsOnVirtualDeviceChanged_singleVirtualDevice_listenersNotified() {
         ArraySet<Integer> uids = new ArraySet<>(Arrays.asList(UID_1, UID_2));
         mLocalService.registerAppsOnVirtualDeviceListener(mAppsOnVirtualDeviceListener);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
index e152feb..e31ee11 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowProcessControllerTests.java
@@ -306,7 +306,7 @@
 
     @Test
     public void testCachedStateConfigurationChange() throws RemoteException {
-        doNothing().when(mClientLifecycleManager).scheduleTransactionItem(any(), any());
+        doNothing().when(mClientLifecycleManager).scheduleTransactionItemUnlocked(any(), any());
         final IApplicationThread thread = mWpc.getThread();
         final Configuration newConfig = new Configuration(mWpc.getConfiguration());
         newConfig.densityDpi += 100;
@@ -322,18 +322,17 @@
         newConfig.densityDpi += 100;
         mWpc.onConfigurationChanged(newConfig);
         verify(mClientLifecycleManager, never()).scheduleTransactionItem(eq(thread), any());
+        verify(mClientLifecycleManager, never()).scheduleTransactionItemUnlocked(eq(thread), any());
 
         // Cached -> non-cached will send the previous deferred config immediately.
         mWpc.setReportedProcState(ActivityManager.PROCESS_STATE_RECEIVER);
         final ArgumentCaptor<ConfigurationChangeItem> captor =
                 ArgumentCaptor.forClass(ConfigurationChangeItem.class);
-        verify(mClientLifecycleManager).scheduleTransactionItem(eq(thread), captor.capture());
+        verify(mClientLifecycleManager).scheduleTransactionItemUnlocked(
+                eq(thread), captor.capture());
         final ClientTransactionHandler client = mock(ClientTransactionHandler.class);
         captor.getValue().preExecute(client);
-        final ArgumentCaptor<Configuration> configCaptor =
-                ArgumentCaptor.forClass(Configuration.class);
-        verify(client).updatePendingConfiguration(configCaptor.capture());
-        assertEquals(newConfig, configCaptor.getValue());
+        verify(client).updatePendingConfiguration(newConfig);
     }
 
     @Test
diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt
index b44f1a6..c49f8fe 100644
--- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt
+++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/CloseImeToHomeOnFinishActivityTest.kt
@@ -23,10 +23,13 @@
 import android.tools.device.flicker.legacy.FlickerBuilder
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import android.tools.device.traces.parsers.toFlickerComponent
 import androidx.test.filters.FlakyTest
 import com.android.server.wm.flicker.BaseTest
 import com.android.server.wm.flicker.helpers.ImeAppHelper
 import com.android.server.wm.flicker.helpers.SimpleAppHelper
+import com.android.server.wm.flicker.testapp.ActivityOptions
+import com.android.server.wm.flicker.testapp.ActivityOptions.Ime.Default.ACTION_FINISH_ACTIVITY
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -53,7 +56,12 @@
             testApp.launchViaIntent(wmHelper)
             testApp.openIME(wmHelper)
         }
-        transitions { testApp.finishActivity(wmHelper) }
+        transitions {
+            broadcastActionTrigger.doAction(ACTION_FINISH_ACTIVITY)
+            wmHelper.StateSyncBuilder()
+                    .withActivityRemoved(ActivityOptions.Ime.Default.COMPONENT.toFlickerComponent())
+                    .waitForAndVerify()
+        }
         teardown { simpleApp.exit(wmHelper) }
     }
 
diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/OpenImeWindowToFixedPortraitAppTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/OpenImeWindowToFixedPortraitAppTest.kt
index 976ac82..994edc5 100644
--- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/OpenImeWindowToFixedPortraitAppTest.kt
+++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/OpenImeWindowToFixedPortraitAppTest.kt
@@ -28,6 +28,7 @@
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.BaseTest
 import com.android.server.wm.flicker.helpers.ImeShownOnAppStartHelper
+import com.android.server.wm.flicker.testapp.ActivityOptions.Ime.Default.ACTION_TOGGLE_ORIENTATION
 import org.junit.FixMethodOrder
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -53,7 +54,11 @@
             // Enable letterbox when the app calls setRequestedOrientation
             device.executeShellCommand("cmd window set-ignore-orientation-request true")
         }
-        transitions { testApp.toggleFixPortraitOrientation(wmHelper) }
+        transitions {
+            broadcastActionTrigger.doAction(ACTION_TOGGLE_ORIENTATION)
+            // Ensure app relaunching transition finished and the IME was shown
+            testApp.waitIMEShown(wmHelper)
+        }
         teardown {
             testApp.exit()
             device.executeShellCommand("cmd window set-ignore-orientation-request false")
diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest.kt
index aff8e65..6ee5a9a 100644
--- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest.kt
+++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppFromQuickSwitchTest.kt
@@ -104,7 +104,7 @@
 
     @Presubmit
     @Test
-    open fun imeLayerIsVisibleWhenSwitchingToImeApp() {
+    fun imeLayerIsVisibleWhenSwitchingToImeApp() {
         flicker.assertLayersStart { isVisible(ComponentNameMatcher.IME) }
         flicker.assertLayersTag(TAG_IME_VISIBLE) { isVisible(ComponentNameMatcher.IME) }
         flicker.assertLayersEnd { isVisible(ComponentNameMatcher.IME) }
diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppTest.kt
index 4ffdcea..1ad5c0d 100644
--- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppTest.kt
+++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeOnAppStartWhenLaunchingAppTest.kt
@@ -93,7 +93,7 @@
         }
         transitions {
             testApp.launchViaIntent(wmHelper)
-            wmHelper.StateSyncBuilder().withImeShown().waitForAndVerify()
+            testApp.waitIMEShown(wmHelper)
         }
     }
 
diff --git a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeWhileDismissingThemedPopupDialogTest.kt b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeWhileDismissingThemedPopupDialogTest.kt
index 6ad235c..181a2a2 100644
--- a/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeWhileDismissingThemedPopupDialogTest.kt
+++ b/tests/FlickerTests/IME/src/com/android/server/wm/flicker/ime/ShowImeWhileDismissingThemedPopupDialogTest.kt
@@ -23,11 +23,14 @@
 import android.tools.device.flicker.legacy.FlickerBuilder
 import android.tools.device.flicker.legacy.LegacyFlickerTest
 import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
+import android.tools.device.traces.parsers.toFlickerComponent
 import android.view.WindowInsets.Type.ime
 import android.view.WindowInsets.Type.navigationBars
 import android.view.WindowInsets.Type.statusBars
 import com.android.server.wm.flicker.BaseTest
 import com.android.server.wm.flicker.helpers.ImeShownOnAppStartHelper
+import com.android.server.wm.flicker.testapp.ActivityOptions
+import com.android.server.wm.flicker.testapp.ActivityOptions.Ime.Default.ACTION_START_DIALOG_THEMED_ACTIVITY
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.FixMethodOrder
@@ -50,8 +53,12 @@
     override val transition: FlickerBuilder.() -> Unit = {
         setup {
             testApp.launchViaIntent(wmHelper)
-            wmHelper.StateSyncBuilder().withImeShown().waitForAndVerify()
-            testApp.startDialogThemedActivity(wmHelper)
+            testApp.waitIMEShown(wmHelper)
+            broadcastActionTrigger.doAction(ACTION_START_DIALOG_THEMED_ACTIVITY)
+            wmHelper.StateSyncBuilder()
+                    .withFullScreenApp(
+                        ActivityOptions.DialogThemedActivity.COMPONENT.toFlickerComponent())
+                    .waitForAndVerify()
             // Verify IME insets isn't visible on dialog since it's non-IME focusable window
             assertFalse(testApp.getInsetsVisibleFromDialog(ime()))
             assertTrue(testApp.getInsetsVisibleFromDialog(statusBars()))
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt
index 7c9c05d..ad272a0 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/BaseTest.kt
@@ -17,6 +17,7 @@
 package com.android.server.wm.flicker
 
 import android.app.Instrumentation
+import android.content.Intent
 import android.platform.test.annotations.Presubmit
 import android.tools.common.traces.component.ComponentNameMatcher
 import android.tools.device.flicker.junit.FlickerBuilderProvider
@@ -50,6 +51,19 @@
     /** Specification of the test transition to execute */
     abstract val transition: FlickerBuilder.() -> Unit
 
+    protected val broadcastActionTrigger = BroadcastActionTrigger(instrumentation)
+
+    // Helper class to process test actions by broadcast.
+    protected class BroadcastActionTrigger(private val instrumentation: Instrumentation) {
+        private fun createIntentWithAction(broadcastAction: String): Intent {
+            return Intent(broadcastAction).setFlags(Intent.FLAG_RECEIVER_FOREGROUND)
+        }
+
+        fun doAction(broadcastAction: String) {
+            instrumentation.context.sendBroadcast(createIntentWithAction(broadcastAction))
+        }
+    }
+
     /**
      * Entry point for the test runner. It will use this method to initialize and cache flicker
      * executions
diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/ImeAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/ImeAppHelper.kt
index 252f7d3..cb1aab0 100644
--- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/ImeAppHelper.kt
+++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/ImeAppHelper.kt
@@ -50,7 +50,7 @@
         waitIMEShown(wmHelper)
     }
 
-    protected fun waitIMEShown(wmHelper: WindowManagerStateHelper) {
+    fun waitIMEShown(wmHelper: WindowManagerStateHelper) {
         wmHelper.StateSyncBuilder().withImeShown().waitForAndVerify()
     }
 
@@ -63,17 +63,4 @@
         uiDevice.pressBack()
         wmHelper.StateSyncBuilder().withImeGone().waitForAndVerify()
     }
-
-    open fun finishActivity(wmHelper: WindowManagerStateHelper) {
-        val finishButton =
-            uiDevice.wait(
-                Until.findObject(By.res(packageName, "finish_activity_btn")),
-                FIND_TIMEOUT
-            )
-        requireNotNull(finishButton) {
-            "Finish activity button not found, probably IME activity is not on the screen?"
-        }
-        finishButton.click()
-        wmHelper.StateSyncBuilder().withActivityRemoved(this).waitForAndVerify()
-    }
 }
diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/ImeShownOnAppStartHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/ImeShownOnAppStartHelper.kt
index d3cee64..0ee7aee 100644
--- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/ImeShownOnAppStartHelper.kt
+++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/ImeShownOnAppStartHelper.kt
@@ -74,24 +74,6 @@
         open(expectedPackage)
     }
 
-    fun startDialogThemedActivity(wmHelper: WindowManagerStateHelper) {
-        val button =
-            uiDevice.wait(
-                Until.findObject(By.res(packageName, "start_dialog_themed_activity_btn")),
-                FIND_TIMEOUT
-            )
-
-        requireNotNull(button) {
-            "Button not found, this usually happens when the device " +
-                "was left in an unknown state (e.g. Screen turned off)"
-        }
-        button.click()
-        wmHelper
-            .StateSyncBuilder()
-            .withFullScreenApp(ActivityOptions.DialogThemedActivity.COMPONENT.toFlickerComponent())
-            .waitForAndVerify()
-    }
-
     fun dismissDialog(wmHelper: WindowManagerStateHelper) {
         val dialog = uiDevice.wait(Until.findObject(By.text("Dialog for test")), FIND_TIMEOUT)
 
@@ -126,20 +108,4 @@
         }
         return false
     }
-
-    fun toggleFixPortraitOrientation(wmHelper: WindowManagerStateHelper) {
-        val button =
-            uiDevice.wait(
-                Until.findObject(By.res(packageName, "toggle_fixed_portrait_btn")),
-                FIND_TIMEOUT
-            )
-        require(button != null) {
-            "Button not found, this usually happens when the device " +
-                "was left in an unknown state (e.g. Screen turned off)"
-        }
-        button.click()
-        instrumentation.waitForIdleSync()
-        // Ensure app relaunching transition finish and the IME has shown
-        waitIMEShown(wmHelper)
-    }
 }
diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml
index fa73e2c..507c1b6 100644
--- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml
+++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_ime.xml
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
+<?xml version="1.0" encoding="utf-8"?><!--
  Copyright 2018 The Android Open Source Project
 
  Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,39 +13,17 @@
  See the License for the specific language governing permissions and
  limitations under the License.
 -->
-<LinearLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:orientation="vertical"
+    android:background="@android:color/holo_green_light"
     android:focusableInTouchMode="true"
-    android:background="@android:color/holo_green_light">
-    <EditText android:id="@+id/plain_text_input"
-              android:layout_height="wrap_content"
-              android:layout_width="match_parent"
-	      android:imeOptions="flagNoExtractUi"
-              android:inputType="text"/>
-    <LinearLayout
-        xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical">
+
+    <EditText
+        android:id="@+id/plain_text_input"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:orientation="horizontal">
-        <Button
-            android:id="@+id/finish_activity_btn"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="Finish activity" />
-        <Button
-            android:id="@+id/start_dialog_themed_activity_btn"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:text="Dialog themed activity" />
-        <ToggleButton
-            android:id="@+id/toggle_fixed_portrait_btn"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:textOn="Portrait (On)"
-            android:textOff="Portrait (Off)"
-        />
-    </LinearLayout>
+        android:layout_height="wrap_content"
+        android:imeOptions="flagNoExtractUi"
+        android:inputType="text" />
 </LinearLayout>
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
index 8b334c0..80c1dd0 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityOptions.java
@@ -40,6 +40,18 @@
             public static final String LABEL = "ImeActivity";
             public static final ComponentName COMPONENT = new ComponentName(FLICKER_APP_PACKAGE,
                     FLICKER_APP_PACKAGE + ".ImeActivity");
+
+            /** Intent action used to finish the test activity. */
+            public static final String ACTION_FINISH_ACTIVITY =
+                    FLICKER_APP_PACKAGE + ".ImeActivity.FINISH_ACTIVITY";
+
+            /** Intent action used to start a {@link DialogThemedActivity}. */
+            public static final String ACTION_START_DIALOG_THEMED_ACTIVITY =
+                    FLICKER_APP_PACKAGE + ".ImeActivity.START_DIALOG_THEMED_ACTIVITY";
+
+            /** Intent action used to toggle activity orientation. */
+            public static final String ACTION_TOGGLE_ORIENTATION =
+                    FLICKER_APP_PACKAGE + ".ImeActivity.TOGGLE_ORIENTATION";
         }
 
         public static class AutoFocusActivity {
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivity.java
index d7ee2af..4418b5a 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivity.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivity.java
@@ -16,12 +16,51 @@
 
 package com.android.server.wm.flicker.testapp;
 
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+
+import static com.android.server.wm.flicker.testapp.ActivityOptions.Ime.Default.ACTION_FINISH_ACTIVITY;
+import static com.android.server.wm.flicker.testapp.ActivityOptions.Ime.Default.ACTION_START_DIALOG_THEMED_ACTIVITY;
+import static com.android.server.wm.flicker.testapp.ActivityOptions.Ime.Default.ACTION_TOGGLE_ORIENTATION;
+
 import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.os.Bundle;
+import android.util.Log;
 import android.view.WindowManager;
-import android.widget.Button;
 
 public class ImeActivity extends Activity {
+
+    private static final String TAG = "ImeActivity";
+
+    /** Receiver used to handle actions coming from the test helper methods. */
+    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            switch (intent.getAction()) {
+                case ACTION_FINISH_ACTIVITY -> finish();
+                case ACTION_START_DIALOG_THEMED_ACTIVITY -> startActivity(
+                        new Intent(context, DialogThemedActivity.class));
+                case ACTION_TOGGLE_ORIENTATION -> {
+                    mIsPortrait = !mIsPortrait;
+                    setRequestedOrientation(mIsPortrait
+                            ? SCREEN_ORIENTATION_PORTRAIT
+                            : SCREEN_ORIENTATION_UNSPECIFIED);
+                }
+                default -> Log.w(TAG, "Unhandled action=" + intent.getAction());
+            }
+        }
+    };
+
+    /**
+     * Used to toggle activity orientation between portrait when {@code true} and
+     * unspecified otherwise.
+     */
+    private boolean mIsPortrait = false;
+
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -30,9 +69,17 @@
                 .LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
         getWindow().setAttributes(p);
         setContentView(R.layout.activity_ime);
-        Button button = findViewById(R.id.finish_activity_btn);
-        button.setOnClickListener(view -> {
-            finish();
-        });
+
+        final var filter = new IntentFilter();
+        filter.addAction(ACTION_FINISH_ACTIVITY);
+        filter.addAction(ACTION_START_DIALOG_THEMED_ACTIVITY);
+        filter.addAction(ACTION_TOGGLE_ORIENTATION);
+        registerReceiver(mBroadcastReceiver, filter, Context.RECEIVER_EXPORTED);
+    }
+
+    @Override
+    protected void onDestroy() {
+        unregisterReceiver(mBroadcastReceiver);
+        super.onDestroy();
     }
 }
diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivityAutoFocus.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivityAutoFocus.java
index 7ee8deb..cd711f7 100644
--- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivityAutoFocus.java
+++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ImeActivityAutoFocus.java
@@ -16,29 +16,12 @@
 
 package com.android.server.wm.flicker.testapp;
 
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
-import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
-
-import android.content.Intent;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.ToggleButton;
-
 public class ImeActivityAutoFocus extends ImeActivity {
     @Override
     protected void onStart() {
         super.onStart();
 
-        Button startThemedActivityButton = findViewById(R.id.start_dialog_themed_activity_btn);
-        startThemedActivityButton.setOnClickListener(
-                button -> startActivity(new Intent(this, DialogThemedActivity.class)));
-
-        ToggleButton toggleFixedPortraitButton = findViewById(R.id.toggle_fixed_portrait_btn);
-        toggleFixedPortraitButton.setOnCheckedChangeListener(
-                (button, isChecked) -> setRequestedOrientation(
-                        isChecked ? SCREEN_ORIENTATION_PORTRAIT : SCREEN_ORIENTATION_UNSPECIFIED));
-
-        EditText editTextField = findViewById(R.id.plain_text_input);
+        final var editTextField = findViewById(R.id.plain_text_input);
         editTextField.requestFocus();
     }
 }