Merge "[speech] add binder alive check" into main
diff --git a/cmds/uinput/src/com/android/commands/uinput/Event.java b/cmds/uinput/src/com/android/commands/uinput/Event.java
index 9d8f1f4..01486c0 100644
--- a/cmds/uinput/src/com/android/commands/uinput/Event.java
+++ b/cmds/uinput/src/com/android/commands/uinput/Event.java
@@ -16,19 +16,10 @@
 
 package com.android.commands.uinput;
 
-import android.util.JsonReader;
-import android.util.JsonToken;
-import android.util.Log;
 import android.util.SparseArray;
 
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.List;
 import java.util.Objects;
-import java.util.function.Function;
-import java.util.stream.IntStream;
 
 import src.com.android.commands.uinput.InputAbsInfo;
 
@@ -40,46 +31,44 @@
     private static final String TAG = "UinputEvent";
 
     enum Command {
-        REGISTER("register"),
-        DELAY("delay"),
-        INJECT("inject"),
-        SYNC("sync");
-
-        final String mCommandName;
-
-        Command(String command) {
-            mCommandName = command;
-        }
+        REGISTER,
+        DELAY,
+        INJECT,
+        SYNC,
     }
 
-    private static final int EV_KEY = 0x01;
-    private static final int EV_REL = 0x02;
-    private static final int EV_ABS = 0x03;
-    private static final int EV_MSC = 0x04;
-    private static final int EV_SW = 0x05;
-    private static final int EV_LED = 0x11;
-    private static final int EV_SND = 0x12;
-    private static final int EV_FF = 0x15;
+    // Constants representing evdev event types, from include/uapi/linux/input-event-codes.h in the
+    // kernel.
+    public static final int EV_KEY = 0x01;
+    public static final int EV_REL = 0x02;
+    public static final int EV_ABS = 0x03;
+    public static final int EV_MSC = 0x04;
+    public static final int EV_SW = 0x05;
+    public static final int EV_LED = 0x11;
+    public static final int EV_SND = 0x12;
+    public static final int EV_FF = 0x15;
 
-    private enum UinputControlCode {
-        UI_SET_EVBIT("UI_SET_EVBIT", 100),
-        UI_SET_KEYBIT("UI_SET_KEYBIT", 101),
-        UI_SET_RELBIT("UI_SET_RELBIT", 102),
-        UI_SET_ABSBIT("UI_SET_ABSBIT", 103),
-        UI_SET_MSCBIT("UI_SET_MSCBIT", 104),
-        UI_SET_LEDBIT("UI_SET_LEDBIT", 105),
-        UI_SET_SNDBIT("UI_SET_SNDBIT", 106),
-        UI_SET_FFBIT("UI_SET_FFBIT", 107),
-        UI_SET_SWBIT("UI_SET_SWBIT", 109),
-        UI_SET_PROPBIT("UI_SET_PROPBIT", 110);
+    public enum UinputControlCode {
+        UI_SET_EVBIT(100),
+        UI_SET_KEYBIT(101),
+        UI_SET_RELBIT(102),
+        UI_SET_ABSBIT(103),
+        UI_SET_MSCBIT(104),
+        UI_SET_LEDBIT(105),
+        UI_SET_SNDBIT(106),
+        UI_SET_FFBIT(107),
+        UI_SET_SWBIT(109),
+        UI_SET_PROPBIT(110);
 
-        final String mName;
-        final int mValue;
+        private final int mValue;
 
-        UinputControlCode(String name, int value) {
-            this.mName = name;
+        UinputControlCode(int value) {
             this.mValue = value;
         }
+
+        public int getValue() {
+            return mValue;
+        }
     }
 
     // These constants come from "include/uapi/linux/input.h" in the kernel
@@ -104,9 +93,9 @@
     private Bus mBus;
     private int[] mInjections;
     private SparseArray<int[]> mConfiguration;
-    private int mDuration;
+    private int mDurationMillis;
     private int mFfEffectsMax = 0;
-    private String mInputport;
+    private String mInputPort;
     private SparseArray<InputAbsInfo> mAbsInfo;
     private String mSyncToken;
 
@@ -138,12 +127,20 @@
         return mInjections;
     }
 
+    /**
+     * Returns a {@link SparseArray} describing the event codes that should be registered for the
+     * device. The keys are uinput ioctl codes (such as those returned from {@link
+     * UinputControlCode#getValue()}, while the values are arrays of event codes to be enabled with
+     * those ioctls. For example, key 101 (corresponding to {@link UinputControlCode#UI_SET_KEYBIT})
+     * could have values 0x110 ({@code BTN_LEFT}, 0x111 ({@code BTN_RIGHT}), and 0x112
+     * ({@code BTN_MIDDLE}).
+     */
     public SparseArray<int[]> getConfiguration() {
         return mConfiguration;
     }
 
-    public int getDuration() {
-        return mDuration;
+    public int getDurationMillis() {
+        return mDurationMillis;
     }
 
     public int getFfEffectsMax() {
@@ -155,7 +152,7 @@
     }
 
     public String getPort() {
-        return mInputport;
+        return mInputPort;
     }
 
     public String getSyncToken() {
@@ -174,13 +171,13 @@
             + ", bus=" + mBus
             + ", events=" + Arrays.toString(mInjections)
             + ", configuration=" + mConfiguration
-            + ", duration=" + mDuration
+            + ", duration=" + mDurationMillis + "ms"
             + ", ff_effects_max=" + mFfEffectsMax
-            + ", port=" + mInputport
+            + ", port=" + mInputPort
             + "}";
     }
 
-    private static class Builder {
+    public static class Builder {
         private Event mEvent;
 
         Builder() {
@@ -191,15 +188,8 @@
             mEvent.mId = id;
         }
 
-        private void setCommand(String command) {
-            Objects.requireNonNull(command, "Command must not be null");
-            for (Command cmd : Command.values()) {
-                if (cmd.mCommandName.equals(command)) {
-                    mEvent.mCommand = cmd;
-                    return;
-                }
-            }
-            throw new IllegalStateException("Unrecognized command: " + command);
+        public void setCommand(String command) {
+            mEvent.mCommand = Command.valueOf(command.toUpperCase());
         }
 
         public void setName(String name) {
@@ -210,6 +200,12 @@
             mEvent.mInjections = events;
         }
 
+        /**
+         * Sets the event codes that should be registered with a {@code register} command.
+         *
+         * @param configuration An array of ioctls and event codes, as described at
+         *                      {@link Event#getConfiguration()}.
+         */
         public void setConfiguration(SparseArray<int[]> configuration) {
             mEvent.mConfiguration = configuration;
         }
@@ -226,8 +222,8 @@
             mEvent.mBus = bus;
         }
 
-        public void setDuration(int duration) {
-            mEvent.mDuration = duration;
+        public void setDurationMillis(int durationMillis) {
+            mEvent.mDurationMillis = durationMillis;
         }
 
         public void setFfEffectsMax(int ffEffectsMax) {
@@ -238,8 +234,8 @@
             mEvent.mAbsInfo = absInfo;
         }
 
-        public void setInputport(String port) {
-            mEvent.mInputport = port;
+        public void setInputPort(String port) {
+            mEvent.mInputPort = port;
         }
 
         public void setSyncToken(String syncToken) {
@@ -260,7 +256,7 @@
                     }
                 }
                 case DELAY -> {
-                    if (mEvent.mDuration <= 0) {
+                    if (mEvent.mDurationMillis <= 0) {
                         throw new IllegalStateException("Delay has missing or invalid duration");
                     }
                 }
@@ -278,343 +274,4 @@
             return mEvent;
         }
     }
-
-    /**
-     *  A class that parses the JSON event format from an input stream to build device events.
-     */
-    public static class Reader {
-        private JsonReader mReader;
-
-        public Reader(InputStreamReader in) {
-            mReader = new JsonReader(in);
-            mReader.setLenient(true);
-        }
-
-        /**
-         * Get next event entry from JSON file reader.
-         */
-        public Event getNextEvent() throws IOException {
-            Event e = null;
-            while (e == null && mReader.peek() != JsonToken.END_DOCUMENT) {
-                Event.Builder eb = new Event.Builder();
-                try {
-                    mReader.beginObject();
-                    while (mReader.hasNext()) {
-                        String name = mReader.nextName();
-                        switch (name) {
-                            case "id":
-                                eb.setId(readInt());
-                                break;
-                            case "command":
-                                eb.setCommand(mReader.nextString());
-                                break;
-                            case "name":
-                                eb.setName(mReader.nextString());
-                                break;
-                            case "vid":
-                                eb.setVid(readInt());
-                                break;
-                            case "pid":
-                                eb.setPid(readInt());
-                                break;
-                            case "bus":
-                                eb.setBus(readBus());
-                                break;
-                            case "events":
-                                int[] injections = readInjectedEvents().stream()
-                                        .mapToInt(Integer::intValue).toArray();
-                                eb.setInjections(injections);
-                                break;
-                            case "configuration":
-                                eb.setConfiguration(readConfiguration());
-                                break;
-                            case "ff_effects_max":
-                                eb.setFfEffectsMax(readInt());
-                                break;
-                            case "abs_info":
-                                eb.setAbsInfo(readAbsInfoArray());
-                                break;
-                            case "duration":
-                                eb.setDuration(readInt());
-                                break;
-                            case "port":
-                                eb.setInputport(mReader.nextString());
-                                break;
-                            case "syncToken":
-                                eb.setSyncToken(mReader.nextString());
-                                break;
-                            default:
-                                mReader.skipValue();
-                        }
-                    }
-                    mReader.endObject();
-                } catch (IllegalStateException ex) {
-                    error("Error reading in object, ignoring.", ex);
-                    consumeRemainingElements();
-                    mReader.endObject();
-                    continue;
-                }
-                e = eb.build();
-            }
-
-            return e;
-        }
-
-        private ArrayList<Integer> readInjectedEvents() throws IOException {
-            ArrayList<Integer> data = new ArrayList<>();
-            try {
-                mReader.beginArray();
-                while (mReader.hasNext()) {
-                    // Read events in groups of three, because we expect an event type, event code,
-                    // and event value.
-                    final int type = readEvdevEventType();
-                    data.add(type);
-                    data.add(readEvdevEventCode(type));
-                    data.add(readInt());
-                }
-                mReader.endArray();
-            } catch (IllegalStateException | NumberFormatException e) {
-                consumeRemainingElements();
-                mReader.endArray();
-                throw new IllegalStateException("Encountered malformed data.", e);
-            }
-            return data;
-        }
-
-        private int readValueAsInt(Function<String, Integer> stringToInt) throws IOException {
-            switch (mReader.peek()) {
-                case NUMBER: {
-                    return mReader.nextInt();
-                }
-                case STRING: {
-                    final var str = mReader.nextString();
-                    try {
-                        // Attempt to first parse the value as an int.
-                        return Integer.decode(str);
-                    } catch (NumberFormatException e) {
-                        // Then fall back to the supplied function.
-                        return stringToInt.apply(str);
-                    }
-                }
-                default: {
-                    throw new IllegalStateException(
-                            "Encountered malformed data. Expected int or string.");
-                }
-            }
-        }
-
-        private int readInt() throws IOException {
-            return readValueAsInt((str) -> {
-                throw new IllegalStateException("Encountered malformed data. Expected int.");
-            });
-        }
-
-        private Bus readBus() throws IOException {
-            String val = mReader.nextString();
-            return Bus.valueOf(val.toUpperCase());
-        }
-
-        private SparseArray<int[]> readConfiguration()
-                throws IllegalStateException, IOException {
-            SparseArray<int[]> configuration = new SparseArray<>();
-            try {
-                mReader.beginArray();
-                while (mReader.hasNext()) {
-                    UinputControlCode controlCode = null;
-                    IntStream data = null;
-                    mReader.beginObject();
-                    while (mReader.hasNext()) {
-                        String name = mReader.nextName();
-                        switch (name) {
-                            case "type":
-                                controlCode = readUinputControlCode();
-                                break;
-                            case "data":
-                                Objects.requireNonNull(controlCode,
-                                        "Configuration 'type' must be specified before 'data'.");
-                                data = readDataForControlCode(controlCode)
-                                        .stream().mapToInt(Integer::intValue);
-                                break;
-                            default:
-                                consumeRemainingElements();
-                                mReader.endObject();
-                                throw new IllegalStateException(
-                                        "Invalid key in device configuration: " + name);
-                        }
-                    }
-                    mReader.endObject();
-                    if (controlCode != null && data != null) {
-                        final int[] existing = configuration.get(controlCode.mValue);
-                        configuration.put(controlCode.mValue, existing == null ? data.toArray()
-                                : IntStream.concat(IntStream.of(existing), data).toArray());
-                    }
-                }
-                mReader.endArray();
-            } catch (IllegalStateException | NumberFormatException e) {
-                consumeRemainingElements();
-                mReader.endArray();
-                throw new IllegalStateException("Encountered malformed data.", e);
-            }
-            return configuration;
-        }
-
-        private UinputControlCode readUinputControlCode() throws IOException {
-            var code = readValueAsInt((controlTypeStr) -> {
-                for (UinputControlCode controlCode : UinputControlCode.values()) {
-                    if (controlCode.mName.equals(controlTypeStr)) {
-                        return controlCode.mValue;
-                    }
-                }
-                return -1;
-            });
-            for (UinputControlCode controlCode : UinputControlCode.values()) {
-                if (controlCode.mValue == code) {
-                    return controlCode;
-                }
-            }
-            return null;
-        }
-
-        private List<Integer> readDataForControlCode(
-                UinputControlCode controlCode) throws IOException {
-            return switch (controlCode) {
-                case UI_SET_EVBIT -> readArrayAsInts(this::readEvdevEventType);
-                case UI_SET_KEYBIT -> readArrayAsInts(() -> readEvdevEventCode(EV_KEY));
-                case UI_SET_RELBIT -> readArrayAsInts(() -> readEvdevEventCode(EV_REL));
-                case UI_SET_ABSBIT -> readArrayAsInts(() -> readEvdevEventCode(EV_ABS));
-                case UI_SET_MSCBIT -> readArrayAsInts(() -> readEvdevEventCode(EV_MSC));
-                case UI_SET_LEDBIT -> readArrayAsInts(() -> readEvdevEventCode(EV_LED));
-                case UI_SET_SNDBIT -> readArrayAsInts(() -> readEvdevEventCode(EV_SND));
-                case UI_SET_FFBIT -> readArrayAsInts(() -> readEvdevEventCode(EV_FF));
-                case UI_SET_SWBIT -> readArrayAsInts(() -> readEvdevEventCode(EV_SW));
-                case UI_SET_PROPBIT -> readArrayAsInts(this::readEvdevInputProp);
-            };
-        }
-
-        interface IntValueReader {
-            int readNextValue() throws IOException;
-        }
-
-        private ArrayList<Integer> readArrayAsInts(
-                IntValueReader nextValueReader) throws IOException {
-            ArrayList<Integer> data = new ArrayList<>();
-            try {
-                mReader.beginArray();
-                while (mReader.hasNext()) {
-                    data.add(nextValueReader.readNextValue());
-                }
-                mReader.endArray();
-            } catch (IllegalStateException | NumberFormatException e) {
-                consumeRemainingElements();
-                mReader.endArray();
-                throw new IllegalStateException("Encountered malformed data.", e);
-            }
-            return data;
-        }
-
-        private InputAbsInfo readAbsInfo() throws IllegalStateException, IOException {
-            InputAbsInfo absInfo = new InputAbsInfo();
-            try {
-                mReader.beginObject();
-                while (mReader.hasNext()) {
-                    String name = mReader.nextName();
-                    switch (name) {
-                        case "value":
-                            absInfo.value = readInt();
-                            break;
-                        case "minimum":
-                            absInfo.minimum = readInt();
-                            break;
-                        case "maximum":
-                            absInfo.maximum = readInt();
-                            break;
-                        case "fuzz":
-                            absInfo.fuzz = readInt();
-                            break;
-                        case "flat":
-                            absInfo.flat = readInt();
-                            break;
-                        case "resolution":
-                            absInfo.resolution = readInt();
-                            break;
-                        default:
-                            consumeRemainingElements();
-                            mReader.endObject();
-                            throw new IllegalStateException("Invalid key in abs info: " + name);
-                    }
-                }
-                mReader.endObject();
-            } catch (IllegalStateException | NumberFormatException e) {
-                consumeRemainingElements();
-                mReader.endObject();
-                throw new IllegalStateException("Encountered malformed data.", e);
-            }
-            return absInfo;
-        }
-
-        private SparseArray<InputAbsInfo> readAbsInfoArray()
-                throws IllegalStateException, IOException {
-            SparseArray<InputAbsInfo> infoArray = new SparseArray<>();
-            try {
-                mReader.beginArray();
-                while (mReader.hasNext()) {
-                    int type = 0;
-                    InputAbsInfo absInfo = null;
-                    mReader.beginObject();
-                    while (mReader.hasNext()) {
-                        String name = mReader.nextName();
-                        switch (name) {
-                            case "code":
-                                type = readEvdevEventCode(EV_ABS);
-                                break;
-                            case "info":
-                                absInfo = readAbsInfo();
-                                break;
-                            default:
-                                consumeRemainingElements();
-                                mReader.endObject();
-                                throw new IllegalStateException("Invalid key in abs info array: "
-                                        + name);
-                        }
-                    }
-                    mReader.endObject();
-                    if (absInfo != null) {
-                        infoArray.put(type, absInfo);
-                    }
-                }
-                mReader.endArray();
-            } catch (IllegalStateException | NumberFormatException e) {
-                consumeRemainingElements();
-                mReader.endArray();
-                throw new IllegalStateException("Encountered malformed data.", e);
-            }
-            return infoArray;
-        }
-
-        private int readEvdevEventType() throws IOException {
-            return readValueAsInt(Device::getEvdevEventTypeByLabel);
-        }
-
-        private int readEvdevEventCode(int type) throws IOException {
-            return readValueAsInt((str) -> Device.getEvdevEventCodeByLabel(type, str));
-        }
-
-        private int readEvdevInputProp() throws IOException {
-            return readValueAsInt(Device::getEvdevInputPropByLabel);
-        }
-
-        private void consumeRemainingElements() throws IOException {
-            while (mReader.hasNext()) {
-                mReader.skipValue();
-            }
-        }
-    }
-
-    private static void error(String msg, Exception e) {
-        System.out.println(msg);
-        Log.e(TAG, msg);
-        if (e != null) {
-            Log.e(TAG, Log.getStackTraceString(e));
-        }
-    }
 }
diff --git a/cmds/uinput/src/com/android/commands/uinput/JsonStyleParser.java b/cmds/uinput/src/com/android/commands/uinput/JsonStyleParser.java
new file mode 100644
index 0000000..53d0be8
--- /dev/null
+++ b/cmds/uinput/src/com/android/commands/uinput/JsonStyleParser.java
@@ -0,0 +1,333 @@
+/*
+ * 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.util.JsonReader;
+import android.util.JsonToken;
+import android.util.Log;
+import android.util.SparseArray;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.stream.IntStream;
+
+import src.com.android.commands.uinput.InputAbsInfo;
+
+/**
+ * A class that parses the JSON-like event format described in the README to build {@link Event}s.
+ */
+public class JsonStyleParser {
+    private static final String TAG = "UinputJsonStyleParser";
+
+    private JsonReader mReader;
+
+    public JsonStyleParser(InputStreamReader in) {
+        mReader = new JsonReader(in);
+        mReader.setLenient(true);
+    }
+
+    /**
+     * Gets the next event entry from the JSON file.
+     */
+    public Event getNextEvent() throws IOException {
+        Event e = null;
+        while (e == null && mReader.peek() != JsonToken.END_DOCUMENT) {
+            Event.Builder eb = new Event.Builder();
+            try {
+                mReader.beginObject();
+                while (mReader.hasNext()) {
+                    String name = mReader.nextName();
+                    switch (name) {
+                        case "id" -> eb.setId(readInt());
+                        case "command" -> eb.setCommand(mReader.nextString());
+                        case "name" -> eb.setName(mReader.nextString());
+                        case "vid" -> eb.setVid(readInt());
+                        case "pid" -> eb.setPid(readInt());
+                        case "bus" -> eb.setBus(readBus());
+                        case "events" -> {
+                            int[] injections = readInjectedEvents().stream()
+                                    .mapToInt(Integer::intValue).toArray();
+                            eb.setInjections(injections);
+                        }
+                        case "configuration" -> eb.setConfiguration(readConfiguration());
+                        case "ff_effects_max" -> eb.setFfEffectsMax(readInt());
+                        case "abs_info" -> eb.setAbsInfo(readAbsInfoArray());
+                        case "duration" -> eb.setDurationMillis(readInt());
+                        case "port" -> eb.setInputPort(mReader.nextString());
+                        case "syncToken" -> eb.setSyncToken(mReader.nextString());
+                        default -> mReader.skipValue();
+                    }
+                }
+                mReader.endObject();
+            } catch (IllegalStateException ex) {
+                error("Error reading in object, ignoring.", ex);
+                consumeRemainingElements();
+                mReader.endObject();
+                continue;
+            }
+            e = eb.build();
+        }
+
+        return e;
+    }
+
+    private ArrayList<Integer> readInjectedEvents() throws IOException {
+        ArrayList<Integer> data = new ArrayList<>();
+        try {
+            mReader.beginArray();
+            while (mReader.hasNext()) {
+                // Read events in groups of three, because we expect an event type, event code,
+                // and event value.
+                final int type = readEvdevEventType();
+                data.add(type);
+                data.add(readEvdevEventCode(type));
+                data.add(readInt());
+            }
+            mReader.endArray();
+        } catch (IllegalStateException | NumberFormatException e) {
+            consumeRemainingElements();
+            mReader.endArray();
+            throw new IllegalStateException("Encountered malformed data.", e);
+        }
+        return data;
+    }
+
+    private int readValueAsInt(Function<String, Integer> stringToInt) throws IOException {
+        switch (mReader.peek()) {
+            case NUMBER: {
+                return mReader.nextInt();
+            }
+            case STRING: {
+                final var str = mReader.nextString();
+                try {
+                    // Attempt to first parse the value as an int.
+                    return Integer.decode(str);
+                } catch (NumberFormatException e) {
+                    // Then fall back to the supplied function.
+                    return stringToInt.apply(str);
+                }
+            }
+            default: {
+                throw new IllegalStateException(
+                        "Encountered malformed data. Expected int or string.");
+            }
+        }
+    }
+
+    private int readInt() throws IOException {
+        return readValueAsInt((str) -> {
+            throw new IllegalStateException("Encountered malformed data. Expected int.");
+        });
+    }
+
+    private Event.Bus readBus() throws IOException {
+        String val = mReader.nextString();
+        return Event.Bus.valueOf(val.toUpperCase());
+    }
+
+    private SparseArray<int[]> readConfiguration()
+            throws IllegalStateException, IOException {
+        SparseArray<int[]> configuration = new SparseArray<>();
+        try {
+            mReader.beginArray();
+            while (mReader.hasNext()) {
+                Event.UinputControlCode controlCode = null;
+                IntStream data = null;
+                mReader.beginObject();
+                while (mReader.hasNext()) {
+                    String name = mReader.nextName();
+                    switch (name) {
+                        case "type":
+                            controlCode = readUinputControlCode();
+                            break;
+                        case "data":
+                            Objects.requireNonNull(controlCode,
+                                    "Configuration 'type' must be specified before 'data'.");
+                            data = readDataForControlCode(controlCode)
+                                    .stream().mapToInt(Integer::intValue);
+                            break;
+                        default:
+                            consumeRemainingElements();
+                            mReader.endObject();
+                            throw new IllegalStateException(
+                                    "Invalid key in device configuration: " + name);
+                    }
+                }
+                mReader.endObject();
+                if (controlCode != null && data != null) {
+                    final int[] existing = configuration.get(controlCode.getValue());
+                    configuration.put(controlCode.getValue(), existing == null ? data.toArray()
+                            : IntStream.concat(IntStream.of(existing), data).toArray());
+                }
+            }
+            mReader.endArray();
+        } catch (IllegalStateException | NumberFormatException e) {
+            consumeRemainingElements();
+            mReader.endArray();
+            throw new IllegalStateException("Encountered malformed data.", e);
+        }
+        return configuration;
+    }
+
+    private Event.UinputControlCode readUinputControlCode() throws IOException {
+        var code = readValueAsInt((controlTypeStr) -> {
+            try {
+                return Event.UinputControlCode.valueOf(controlTypeStr).getValue();
+            } catch (IllegalArgumentException ex) {
+                return -1;
+            }
+        });
+        for (Event.UinputControlCode controlCode : Event.UinputControlCode.values()) {
+            if (controlCode.getValue() == code) {
+                return controlCode;
+            }
+        }
+        return null;
+    }
+
+    private List<Integer> readDataForControlCode(
+            Event.UinputControlCode controlCode) throws IOException {
+        return switch (controlCode) {
+            case UI_SET_EVBIT -> readArrayAsInts(this::readEvdevEventType);
+            case UI_SET_KEYBIT -> readArrayAsInts(() -> readEvdevEventCode(Event.EV_KEY));
+            case UI_SET_RELBIT -> readArrayAsInts(() -> readEvdevEventCode(Event.EV_REL));
+            case UI_SET_ABSBIT -> readArrayAsInts(() -> readEvdevEventCode(Event.EV_ABS));
+            case UI_SET_MSCBIT -> readArrayAsInts(() -> readEvdevEventCode(Event.EV_MSC));
+            case UI_SET_LEDBIT -> readArrayAsInts(() -> readEvdevEventCode(Event.EV_LED));
+            case UI_SET_SNDBIT -> readArrayAsInts(() -> readEvdevEventCode(Event.EV_SND));
+            case UI_SET_FFBIT -> readArrayAsInts(() -> readEvdevEventCode(Event.EV_FF));
+            case UI_SET_SWBIT -> readArrayAsInts(() -> readEvdevEventCode(Event.EV_SW));
+            case UI_SET_PROPBIT -> readArrayAsInts(this::readEvdevInputProp);
+        };
+    }
+
+    interface IntValueReader {
+        int readNextValue() throws IOException;
+    }
+
+    private ArrayList<Integer> readArrayAsInts(
+            IntValueReader nextValueReader) throws IOException {
+        ArrayList<Integer> data = new ArrayList<>();
+        try {
+            mReader.beginArray();
+            while (mReader.hasNext()) {
+                data.add(nextValueReader.readNextValue());
+            }
+            mReader.endArray();
+        } catch (IllegalStateException | NumberFormatException e) {
+            consumeRemainingElements();
+            mReader.endArray();
+            throw new IllegalStateException("Encountered malformed data.", e);
+        }
+        return data;
+    }
+
+    private InputAbsInfo readAbsInfo() throws IllegalStateException, IOException {
+        InputAbsInfo absInfo = new InputAbsInfo();
+        try {
+            mReader.beginObject();
+            while (mReader.hasNext()) {
+                String name = mReader.nextName();
+                switch (name) {
+                    case "value" -> absInfo.value = readInt();
+                    case "minimum" -> absInfo.minimum = readInt();
+                    case "maximum" -> absInfo.maximum = readInt();
+                    case "fuzz" -> absInfo.fuzz = readInt();
+                    case "flat" -> absInfo.flat = readInt();
+                    case "resolution" -> absInfo.resolution = readInt();
+                    default -> {
+                        consumeRemainingElements();
+                        mReader.endObject();
+                        throw new IllegalStateException("Invalid key in abs info: " + name);
+                    }
+                }
+            }
+            mReader.endObject();
+        } catch (IllegalStateException | NumberFormatException e) {
+            consumeRemainingElements();
+            mReader.endObject();
+            throw new IllegalStateException("Encountered malformed data.", e);
+        }
+        return absInfo;
+    }
+
+    private SparseArray<InputAbsInfo> readAbsInfoArray()
+            throws IllegalStateException, IOException {
+        SparseArray<InputAbsInfo> infoArray = new SparseArray<>();
+        try {
+            mReader.beginArray();
+            while (mReader.hasNext()) {
+                int type = 0;
+                InputAbsInfo absInfo = null;
+                mReader.beginObject();
+                while (mReader.hasNext()) {
+                    String name = mReader.nextName();
+                    switch (name) {
+                        case "code" -> type = readEvdevEventCode(Event.EV_ABS);
+                        case "info" -> absInfo = readAbsInfo();
+                        default -> {
+                            consumeRemainingElements();
+                            mReader.endObject();
+                            throw new IllegalStateException("Invalid key in abs info array: "
+                                    + name);
+                        }
+                    }
+                }
+                mReader.endObject();
+                if (absInfo != null) {
+                    infoArray.put(type, absInfo);
+                }
+            }
+            mReader.endArray();
+        } catch (IllegalStateException | NumberFormatException e) {
+            consumeRemainingElements();
+            mReader.endArray();
+            throw new IllegalStateException("Encountered malformed data.", e);
+        }
+        return infoArray;
+    }
+
+    private int readEvdevEventType() throws IOException {
+        return readValueAsInt(Device::getEvdevEventTypeByLabel);
+    }
+
+    private int readEvdevEventCode(int type) throws IOException {
+        return readValueAsInt((str) -> Device.getEvdevEventCodeByLabel(type, str));
+    }
+
+    private int readEvdevInputProp() throws IOException {
+        return readValueAsInt(Device::getEvdevInputPropByLabel);
+    }
+
+    private void consumeRemainingElements() throws IOException {
+        while (mReader.hasNext()) {
+            mReader.skipValue();
+        }
+    }
+
+    private static void error(String msg, Exception e) {
+        System.out.println(msg);
+        Log.e(TAG, msg);
+        if (e != null) {
+            Log.e(TAG, Log.getStackTraceString(e));
+        }
+    }
+}
diff --git a/cmds/uinput/src/com/android/commands/uinput/Uinput.java b/cmds/uinput/src/com/android/commands/uinput/Uinput.java
index 47b7a354..fe76abb 100644
--- a/cmds/uinput/src/com/android/commands/uinput/Uinput.java
+++ b/cmds/uinput/src/com/android/commands/uinput/Uinput.java
@@ -35,7 +35,7 @@
 public class Uinput {
     private static final String TAG = "UINPUT";
 
-    private final Event.Reader mReader;
+    private final JsonStyleParser mParser;
     private final SparseArray<Device> mDevices;
 
     private static void usage() {
@@ -74,7 +74,7 @@
     private Uinput(InputStream in) {
         mDevices = new SparseArray<Device>();
         try {
-            mReader = new Event.Reader(new InputStreamReader(in, "UTF-8"));
+            mParser = new JsonStyleParser(new InputStreamReader(in, "UTF-8"));
         } catch (UnsupportedEncodingException e) {
             throw new RuntimeException(e);
         }
@@ -83,7 +83,7 @@
     private void run() {
         try {
             Event e = null;
-            while ((e = mReader.getNextEvent()) != null) {
+            while ((e = mParser.getNextEvent()) != null) {
                 process(e);
             }
         } catch (IOException ex) {
@@ -111,7 +111,7 @@
             case REGISTER ->
                     error("Device id=" + e.getId() + " is already registered. Ignoring event.");
             case INJECT -> d.injectEvent(e.getInjections());
-            case DELAY -> d.addDelay(e.getDuration());
+            case DELAY -> d.addDelay(e.getDurationMillis());
             case SYNC -> d.syncEvent(e.getSyncToken());
         }
     }
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index ee1659b..fbd2142 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3331,6 +3331,53 @@
 
 }
 
+package android.companion.virtual.camera {
+
+  @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCamera implements java.io.Closeable {
+    method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
+    method @NonNull public android.companion.virtual.camera.VirtualCameraConfig getConfig();
+  }
+
+  @FlaggedApi("android.companion.virtual.flags.virtual_camera") public interface VirtualCameraCallback {
+    method public void onProcessCaptureRequest(int, long, @Nullable android.companion.virtual.camera.VirtualCameraMetadata);
+    method public void onStreamClosed(int);
+    method public void onStreamConfigured(int, @NonNull android.view.Surface, @NonNull android.companion.virtual.camera.VirtualCameraStreamConfig);
+  }
+
+  @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCameraConfig implements android.os.Parcelable {
+    method public int describeContents();
+    method @StringRes public int getDisplayNameStringRes();
+    method @NonNull public java.util.Set<android.companion.virtual.camera.VirtualCameraStreamConfig> getStreamConfigs();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.companion.virtual.camera.VirtualCameraConfig> CREATOR;
+  }
+
+  @FlaggedApi("android.companion.virtual.flags.virtual_camera") public static final class VirtualCameraConfig.Builder {
+    ctor public VirtualCameraConfig.Builder();
+    method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder addStreamConfig(int, int, int);
+    method @NonNull public android.companion.virtual.camera.VirtualCameraConfig build();
+    method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setDisplayNameStringRes(@StringRes int);
+    method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setVirtualCameraCallback(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.camera.VirtualCameraCallback);
+  }
+
+  @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCameraMetadata implements android.os.Parcelable {
+    method public int describeContents();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.companion.virtual.camera.VirtualCameraMetadata> CREATOR;
+  }
+
+  @FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCameraStreamConfig implements android.os.Parcelable {
+    ctor public VirtualCameraStreamConfig(@IntRange(from=1) int, @IntRange(from=1) int, int);
+    method public int describeContents();
+    method public int getFormat();
+    method @IntRange(from=1) public int getHeight();
+    method @IntRange(from=1) public int getWidth();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.companion.virtual.camera.VirtualCameraStreamConfig> CREATOR;
+  }
+
+}
+
 package android.companion.virtual.sensor {
 
   public final class VirtualSensor implements android.os.Parcelable {
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index fa52968..79a5879 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -113,6 +113,7 @@
 import android.hardware.lights.LightsManager;
 import android.hardware.lights.SystemLightsManager;
 import android.hardware.location.ContextHubManager;
+import android.hardware.location.IContextHubService;
 import android.hardware.radio.RadioManager;
 import android.hardware.usb.IUsbManager;
 import android.hardware.usb.UsbManager;
@@ -1118,8 +1119,12 @@
                 new CachedServiceFetcher<ContextHubManager>() {
             @Override
             public ContextHubManager createService(ContextImpl ctx) throws ServiceNotFoundException {
-                return new ContextHubManager(ctx.getOuterContext(),
-                  ctx.mMainThread.getHandler().getLooper());
+                IBinder b = ServiceManager.getService(Context.CONTEXTHUB_SERVICE);
+                if (b == null) {
+                    return null;
+                }
+                return new ContextHubManager(IContextHubService.Stub.asInterface(b),
+                        ctx.mMainThread.getHandler().getLooper());
             }});
 
         registerService(Context.INCIDENT_SERVICE, IncidentManager.class,
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 1d036a1..3ee9d692 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -13400,7 +13400,7 @@
      * A device owner, by default, may continue granting these permissions. However, for increased
      * user control, the admin may opt out of controlling grants for these permissions by including
      * {@link #EXTRA_PROVISIONING_SENSORS_PERMISSION_GRANT_OPT_OUT} in the provisioning parameters.
-     * In that case the device owner's control will be limited do denying these permissions.
+     * In that case the device owner's control will be limited to denying these permissions.
      * <p>
      * NOTE: On devices running {@link android.os.Build.VERSION_CODES#S} and above, control over
      * the following permissions are restricted for managed profile owners:
diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl
index 2f97080..102cbf3 100644
--- a/core/java/android/companion/virtual/IVirtualDevice.aidl
+++ b/core/java/android/companion/virtual/IVirtualDevice.aidl
@@ -23,8 +23,7 @@
 import android.companion.virtual.sensor.VirtualSensor;
 import android.companion.virtual.sensor.VirtualSensorConfig;
 import android.companion.virtual.sensor.VirtualSensorEvent;
-import android.companion.virtual.camera.IVirtualCamera;
-import android.companion.virtual.camera.VirtualCameraHalConfig;
+import android.companion.virtual.camera.VirtualCameraConfig;
 import android.content.ComponentName;
 import android.content.IntentFilter;
 import android.graphics.Point;
@@ -236,8 +235,15 @@
     void unregisterIntentInterceptor(in IVirtualDeviceIntentInterceptor intentInterceptor);
 
     /**
-     * Creates a new VirtualCamera and registers it with the VirtualCameraProvider.
+     * Creates a new virtual camera and registers it with the virtual camera service.
      */
     @EnforcePermission("CREATE_VIRTUAL_DEVICE")
-    void registerVirtualCamera(in IVirtualCamera camera);
+    void registerVirtualCamera(in VirtualCameraConfig camera);
+
+    /**
+     * Destroys the virtual camera with given config and unregisters it from the virtual camera
+     * service.
+     */
+    @EnforcePermission("CREATE_VIRTUAL_DEVICE")
+    void unregisterVirtualCamera(in VirtualCameraConfig camera);
 }
diff --git a/core/java/android/companion/virtual/VirtualDevice.java b/core/java/android/companion/virtual/VirtualDevice.java
index 93a3e78..d0c8be6 100644
--- a/core/java/android/companion/virtual/VirtualDevice.java
+++ b/core/java/android/companion/virtual/VirtualDevice.java
@@ -32,9 +32,8 @@
  * Details of a particular virtual device.
  *
  * <p>Read-only device representation exposing the properties of an existing virtual device.
- *
- * @see VirtualDeviceManager#registerVirtualDeviceListener
  */
+// TODO(b/310912420): Link to VirtualDeviceManager#registerVirtualDeviceListener from the docs
 public final class VirtualDevice implements Parcelable {
 
     private final @NonNull IVirtualDevice mVirtualDevice;
@@ -92,8 +91,8 @@
      * per device.
      *
      * @see Context#createDeviceContext
-     * @see #getPersistentDeviceId
      */
+    // TODO(b/310912420): Link to #getPersistentDeviceId from the docs
     public int getDeviceId() {
         return mId;
     }
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index 60965a8..41c90b9 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -212,9 +212,10 @@
      * existing virtual devices.</p>
      *
      * <p>Note that if a virtual device is closed and becomes invalid, the returned objects will
-     * not be updated and may contain stale values. Use a {@link VirtualDeviceListener} for real
-     * time updates of the availability of virtual devices.</p>
+     * not be updated and may contain stale values.</p>
      */
+    // TODO(b/310912420): Add "Use a VirtualDeviceListener for real time updates of the
+    // availability  of virtual devices." in the note paragraph above with a link annotation.
     @NonNull
     public List<android.companion.virtual.VirtualDevice> getVirtualDevices() {
         if (mService == null) {
diff --git a/core/java/android/companion/virtual/camera/IVirtualCamera.aidl b/core/java/android/companion/virtual/camera/IVirtualCamera.aidl
deleted file mode 100644
index 58b850d..0000000
--- a/core/java/android/companion/virtual/camera/IVirtualCamera.aidl
+++ /dev/null
@@ -1,17 +0,0 @@
-package android.companion.virtual.camera;
-
-import android.companion.virtual.camera.IVirtualCameraSession;
-import android.companion.virtual.camera.VirtualCameraHalConfig;
-
-/**
- * Counterpart of ICameraDevice for virtual camera.
- *
- * @hide
- */
-interface IVirtualCamera {
-
-    IVirtualCameraSession open();
-
-    VirtualCameraHalConfig getHalConfig();
-
-}
\ No newline at end of file
diff --git a/core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl b/core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl
new file mode 100644
index 0000000..fac44b5
--- /dev/null
+++ b/core/java/android/companion/virtual/camera/IVirtualCameraCallback.aidl
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.companion.virtual.camera;
+
+import android.companion.virtual.camera.VirtualCameraStreamConfig;
+import android.companion.virtual.camera.VirtualCameraMetadata;
+import android.view.Surface;
+
+/**
+ * Interface for the virtual camera service and system server to talk back to the virtual camera owner.
+ *
+ * @hide
+ */
+interface IVirtualCameraCallback {
+
+    /**
+     * Called when one of the requested stream has been configured by the virtual camera service and
+     * is ready to receive data onto its {@link Surface}
+     *
+     * @param streamId     The id of the configured stream
+     * @param surface      The surface to write data into for this stream
+     * @param streamConfig The image data configuration for this stream
+     */
+    oneway void onStreamConfigured(
+            int streamId,
+            in Surface surface,
+            in VirtualCameraStreamConfig streamConfig);
+
+    /**
+     * The client application is requesting a camera frame for the given streamId with the provided
+     * metadata.
+     *
+     * <p>The virtual camera needs to write the frame data in the {@link Surface} corresponding to
+     * this stream that was provided during the {@link #onStreamConfigured(int, Surface,
+     * VirtualCameraStreamConfig)} call.
+     *
+     * @param streamId The streamId for which the frame is requested. This corresponds to the
+     *     streamId that was given in {@link #onStreamConfigured(int, Surface,
+     *     VirtualCameraStreamConfig)}
+     * @param frameId The frameId that is being requested. Each request will have a different
+     *     frameId, that will be increasing for each call with a particular streamId.
+     * @param metadata The metadata requested for the frame. The virtual camera should do its best
+     *     to honor the requested metadata.
+     */
+    oneway void onProcessCaptureRequest(
+            int streamId, long frameId, in VirtualCameraMetadata metadata);
+
+    /**
+     * The stream previously configured when {@link #onStreamConfigured(int, Surface,
+     * VirtualCameraStreamConfig)} was called is now being closed and associated resources can be
+     * freed. The Surface was disposed on the client side and should not be used anymore by the virtual camera owner
+     *
+     * @param streamId The id of the stream that was closed.
+     */
+    oneway void onStreamClosed(int streamId);
+
+}
\ No newline at end of file
diff --git a/core/java/android/companion/virtual/camera/VirtualCamera.java b/core/java/android/companion/virtual/camera/VirtualCamera.java
index 791bf0a..beee86f 100644
--- a/core/java/android/companion/virtual/camera/VirtualCamera.java
+++ b/core/java/android/companion/virtual/camera/VirtualCamera.java
@@ -16,20 +16,44 @@
 
 package android.companion.virtual.camera;
 
+import android.annotation.FlaggedApi;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
 import android.companion.virtual.IVirtualDevice;
+import android.companion.virtual.VirtualDeviceManager;
+import android.companion.virtual.VirtualDeviceParams;
+import android.companion.virtual.flags.Flags;
+import android.hardware.camera2.CameraDevice;
 import android.os.RemoteException;
 
 import androidx.annotation.NonNull;
 
+import java.io.Closeable;
 import java.util.Objects;
+import java.util.concurrent.Executor;
 
 /**
- * Virtual camera that is used to send image data into system.
+ * A VirtualCamera is the representation of a remote or computer generated camera that will be
+ * exposed to applications using the Android Camera APIs.
  *
+ * <p>A VirtualCamera is created using {@link
+ * VirtualDeviceManager.VirtualDevice#createVirtualCamera(VirtualCameraConfig)}.
+ *
+ * <p>Once a virtual camera is created, it will receive callbacks from the system when an
+ * application attempts to use it via the {@link VirtualCameraCallback} class set using {@link
+ * VirtualCameraConfig.Builder#setVirtualCameraCallback(Executor, VirtualCameraCallback)}
+ *
+ * @see VirtualDeviceManager.VirtualDevice#createVirtualDevice(int, VirtualDeviceParams)
+ * @see VirtualCameraConfig.Builder#setVirtualCameraCallback(Executor, VirtualCameraCallback)
+ * @see android.hardware.camera2.CameraManager#openCamera(String, CameraDevice.StateCallback,
+ *     android.os.Handler)
  * @hide
  */
-public final class VirtualCamera extends IVirtualCamera.Stub {
+@SystemApi
+@FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
+public final class VirtualCamera implements Closeable {
 
+    private final IVirtualDevice mVirtualDevice;
     private final VirtualCameraConfig mConfig;
 
     /**
@@ -37,40 +61,35 @@
      *
      * @param virtualDevice The Binder object representing this camera in the server.
      * @param config Configuration for the new virtual camera
+     * @hide
      */
+    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
     public VirtualCamera(
             @NonNull IVirtualDevice virtualDevice, @NonNull VirtualCameraConfig config) {
+        mVirtualDevice = virtualDevice;
         mConfig = Objects.requireNonNull(config);
         Objects.requireNonNull(virtualDevice);
+        // TODO(b/310857519): Avoid registration inside constructor.
         try {
-            virtualDevice.registerVirtualCamera(this);
+            mVirtualDevice.registerVirtualCamera(config);
         } catch (RemoteException e) {
             e.rethrowFromSystemServer();
         }
     }
 
-    /** Get the camera session associated with this device */
-    @Override
-    public IVirtualCameraSession open() {
-        // TODO: b/302255544 - Make this async.
-        VirtualCameraSession session = mConfig.getCallback().onOpenSession();
-        return new VirtualCameraSessionInternal(session);
-    }
-
     /** Returns the configuration of this virtual camera instance. */
     @NonNull
     public VirtualCameraConfig getConfig() {
         return mConfig;
     }
 
-    /**
-     * Returns the configuration to be used by the virtual camera HAL.
-     *
-     * @hide
-     */
     @Override
-    @NonNull
-    public VirtualCameraHalConfig getHalConfig() {
-        return mConfig.getHalConfig();
+    @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    public void close() {
+        try {
+            mVirtualDevice.unregisterVirtualCamera(mConfig);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
     }
 }
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraCallback.java b/core/java/android/companion/virtual/camera/VirtualCameraCallback.java
index a7c3d4f..a18ae03 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraCallback.java
+++ b/core/java/android/companion/virtual/camera/VirtualCameraCallback.java
@@ -16,7 +16,12 @@
 
 package android.companion.virtual.camera;
 
-import android.hardware.camera2.params.SessionConfiguration;
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.companion.virtual.flags.Flags;
+import android.view.Surface;
 
 import java.util.concurrent.Executor;
 
@@ -24,15 +29,53 @@
  * Interface to be provided when creating a new {@link VirtualCamera} in order to receive callbacks
  * from the framework and the camera system.
  *
- * @see VirtualCameraConfig.Builder#setCallback(Executor, VirtualCameraCallback)
+ * @see VirtualCameraConfig.Builder#setVirtualCameraCallback(Executor, VirtualCameraCallback)
  * @hide
  */
+@SystemApi
+@FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
 public interface VirtualCameraCallback {
 
     /**
-     * Called when a client opens a new camera session for the associated {@link VirtualCamera}
+     * Called when one of the requested stream has been configured by the virtual camera service and
+     * is ready to receive data onto its {@link Surface}
      *
-     * @see android.hardware.camera2.CameraDevice#createCaptureSession(SessionConfiguration)
+     * @param streamId The id of the configured stream
+     * @param surface The surface to write data into for this stream
+     * @param streamConfig The image data configuration for this stream
      */
-    VirtualCameraSession onOpenSession();
+    void onStreamConfigured(
+            int streamId,
+            @NonNull Surface surface,
+            @NonNull VirtualCameraStreamConfig streamConfig);
+
+    /**
+     * The client application is requesting a camera frame for the given streamId with the provided
+     * metadata.
+     *
+     * <p>The virtual camera needs to write the frame data in the {@link Surface} corresponding to
+     * this stream that was provided during the {@link #onStreamConfigured(int, Surface,
+     * VirtualCameraStreamConfig)} call.
+     *
+     * @param streamId The streamId for which the frame is requested. This corresponds to the
+     *     streamId that was given in {@link #onStreamConfigured(int, Surface,
+     *     VirtualCameraStreamConfig)}
+     * @param frameId The frameId that is being requested. Each request will have a different
+     *     frameId, that will be increasing for each call with a particular streamId.
+     * @param metadata The metadata requested for the frame. The virtual camera should do its best
+     *     to honor the requested metadata but the consumer won't be informed about the metadata set
+     *     for a particular frame. If null, the requested frame can be anything the producer sends.
+     */
+    void onProcessCaptureRequest(
+            int streamId, long frameId, @Nullable VirtualCameraMetadata metadata);
+
+    /**
+     * The stream previously configured when {@link #onStreamConfigured(int, Surface,
+     * VirtualCameraStreamConfig)} was called is now being closed and associated resources can be
+     * freed. The Surface corresponding to that streamId was disposed on the client side and should
+     * not be used anymore by the virtual camera owner
+     *
+     * @param streamId The id of the stream that was closed.
+     */
+    void onStreamClosed(int streamId);
 }
diff --git a/core/java/android/companion/virtual/camera/IVirtualCameraSession.aidl b/core/java/android/companion/virtual/camera/VirtualCameraConfig.aidl
similarity index 76%
copy from core/java/android/companion/virtual/camera/IVirtualCameraSession.aidl
copy to core/java/android/companion/virtual/camera/VirtualCameraConfig.aidl
index 2529807..88c27a5 100644
--- a/core/java/android/companion/virtual/camera/IVirtualCameraSession.aidl
+++ b/core/java/android/companion/virtual/camera/VirtualCameraConfig.aidl
@@ -15,14 +15,5 @@
  */
 package android.companion.virtual.camera;
 
-/**
- * Counterpart of ICameraDeviceSession for virtual camera.
- *
- * @hide
- */
-interface IVirtualCameraSession {
-
-    void configureStream(int width, int height, int format);
-
-    void close();
-}
+/** @hide */
+parcelable VirtualCameraConfig;
\ No newline at end of file
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
index fb464d5..f1eb240 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
+++ b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java
@@ -18,11 +18,19 @@
 
 import static java.util.Objects.requireNonNull;
 
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
+import android.annotation.StringRes;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.companion.virtual.flags.Flags;
+import android.content.res.Resources;
 import android.graphics.ImageFormat;
+import android.os.Parcel;
+import android.os.Parcelable;
 import android.util.ArraySet;
+import android.view.Surface;
 
-import java.util.Collections;
 import java.util.Set;
 import java.util.concurrent.Executor;
 
@@ -33,33 +41,115 @@
  *
  * @hide
  */
-public final class VirtualCameraConfig {
+@SystemApi
+@FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
+public final class VirtualCameraConfig implements Parcelable {
 
-    private final String mDisplayName;
+    private final @StringRes int mNameStringRes;
     private final Set<VirtualCameraStreamConfig> mStreamConfigurations;
-    private final VirtualCameraCallback mCallback;
-    private final Executor mCallbackExecutor;
+    private final IVirtualCameraCallback mCallback;
+
+    private VirtualCameraConfig(
+            int displayNameStringRes,
+            @NonNull Set<VirtualCameraStreamConfig> streamConfigurations,
+            @NonNull Executor executor,
+            @NonNull VirtualCameraCallback callback) {
+        mNameStringRes = displayNameStringRes;
+        mStreamConfigurations =
+                Set.copyOf(requireNonNull(streamConfigurations, "Missing stream configurations"));
+        if (mStreamConfigurations.isEmpty()) {
+            throw new IllegalArgumentException(
+                    "At least one stream configuration is needed to create a virtual camera.");
+        }
+        mCallback =
+                new VirtualCameraCallbackInternal(
+                        requireNonNull(callback, "Missing callback"),
+                        requireNonNull(executor, "Missing callback executor"));
+    }
+
+    private VirtualCameraConfig(@NonNull Parcel in) {
+        mNameStringRes = in.readInt();
+        mCallback = IVirtualCameraCallback.Stub.asInterface(in.readStrongBinder());
+        mStreamConfigurations =
+                Set.of(
+                        in.readParcelableArray(
+                                VirtualCameraStreamConfig.class.getClassLoader(),
+                                VirtualCameraStreamConfig.class));
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mNameStringRes);
+        dest.writeStrongInterface(mCallback);
+        dest.writeParcelableArray(
+                mStreamConfigurations.toArray(new VirtualCameraStreamConfig[0]), flags);
+    }
+
+    /**
+     * @return The display name of this VirtualCamera
+     */
+    @StringRes
+    public int getDisplayNameStringRes() {
+        return mNameStringRes;
+    }
+
+    /**
+     * Returns an unmodifiable set of the stream configurations added to this {@link
+     * VirtualCameraConfig}.
+     *
+     * @see VirtualCameraConfig.Builder#addStreamConfig(int, int, int)
+     */
+    @NonNull
+    public Set<VirtualCameraStreamConfig> getStreamConfigs() {
+        return mStreamConfigurations;
+    }
+
+    /**
+     * Returns the callback used to communicate from the server to the client.
+     *
+     * @hide
+     */
+    @NonNull
+    public IVirtualCameraCallback getCallback() {
+        return mCallback;
+    }
 
     /**
      * Builder for {@link VirtualCameraConfig}.
      *
      * <p>To build an instance of {@link VirtualCameraConfig} the following conditions must be met:
-     * <li>At least one stream must be added wit {@link #addStreamConfiguration(int, int, int)}.
-     * <li>A name must be set with {@link #setDisplayName(String)}
-     * <li>A callback must be set wit {@link #setCallback(Executor, VirtualCameraCallback)}
+     * <li>At least one stream must be added with {@link #addStreamConfig(int, int, int)}.
+     * <li>A callback must be set with {@link #setVirtualCameraCallback(Executor,
+     *     VirtualCameraCallback)}
+     * <li>A user readable name can be set with {@link #setDisplayNameStringRes(int)}
      */
+    @FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
     public static final class Builder {
 
-        private String mDisplayName;
-        private final ArraySet<VirtualCameraStreamConfig> mStreamConfiguration = new ArraySet<>();
+        private @StringRes int mDisplayNameStringRes = Resources.ID_NULL;
+        private final ArraySet<VirtualCameraStreamConfig> mStreamConfigurations = new ArraySet<>();
         private Executor mCallbackExecutor;
         private VirtualCameraCallback mCallback;
 
-        /** Set the visible name of this camera for the user. */
-        // TODO: b/290172356 - Take a resource id instead of displayName
+        /**
+         * Set the visible name of this camera for the user.
+         *
+         * <p>Sets the resource to a string representing a user readable name for this virtual
+         * camera.
+         *
+         * @throws IllegalArgumentException if an invalid resource id is passed.
+         */
         @NonNull
-        public Builder setDisplayName(@NonNull String displayName) {
-            mDisplayName = requireNonNull(displayName);
+        public Builder setDisplayNameStringRes(@StringRes int displayNameStringRes) {
+            if (displayNameStringRes <= 0) {
+                throw new IllegalArgumentException("Invalid resource passed for display name");
+            }
+            mDisplayNameStringRes = displayNameStringRes;
             return this;
         }
 
@@ -68,18 +158,21 @@
          *
          * <p>At least one {@link VirtualCameraStreamConfig} must be added.
          *
-         * @param width The width of the stream
-         * @param height The height of the stream
-         * @param format The {@link ImageFormat} of the stream
+         * @param width The width of the stream.
+         * @param height The height of the stream.
+         * @param format The {@link ImageFormat} of the stream.
+         *
+         * @throws IllegalArgumentException if invalid format or dimensions are passed.
          */
         @NonNull
-        public Builder addStreamConfiguration(
-                int width, int height, @ImageFormat.Format int format) {
-            VirtualCameraStreamConfig streamConfig = new VirtualCameraStreamConfig();
-            streamConfig.width = width;
-            streamConfig.height = height;
-            streamConfig.format = format;
-            mStreamConfiguration.add(streamConfig);
+        public Builder addStreamConfig(int width, int height, @ImageFormat.Format int format) {
+            if (width <= 0 || height <= 0) {
+                throw new IllegalArgumentException("Invalid dimensions passed for stream config");
+            }
+            if (!ImageFormat.isPublicFormat(format)) {
+                throw new IllegalArgumentException("Invalid format passed for stream config");
+            }
+            mStreamConfigurations.add(new VirtualCameraStreamConfig(width, height, format));
             return this;
         }
 
@@ -93,7 +186,9 @@
          * @param callback The instance of the callback to be added. Subsequent call to this method
          *     will replace the callback set.
          */
-        public Builder setCallback(
+        @NonNull
+        @SuppressLint("MissingGetterMatchingBuilder") // The configuration is immutable
+        public Builder setVirtualCameraCallback(
                 @NonNull Executor executor, @NonNull VirtualCameraCallback callback) {
             mCallbackExecutor = requireNonNull(executor);
             mCallback = requireNonNull(callback);
@@ -108,67 +203,49 @@
         @NonNull
         public VirtualCameraConfig build() {
             return new VirtualCameraConfig(
-                    mDisplayName, mStreamConfiguration, mCallbackExecutor, mCallback);
+                    mDisplayNameStringRes, mStreamConfigurations, mCallbackExecutor, mCallback);
         }
     }
 
-    private VirtualCameraConfig(
-            @NonNull String displayName,
-            @NonNull Set<VirtualCameraStreamConfig> streamConfigurations,
-            @NonNull Executor executor,
-            @NonNull VirtualCameraCallback callback) {
-        mDisplayName = requireNonNull(displayName, "Missing display name");
-        mStreamConfigurations =
-                Collections.unmodifiableSet(
-                        requireNonNull(streamConfigurations, "Missing stream configuration"));
-        if (mStreamConfigurations.isEmpty()) {
-            throw new IllegalArgumentException(
-                    "At least one StreamConfiguration is needed to create a virtual camera.");
+    private static class VirtualCameraCallbackInternal extends IVirtualCameraCallback.Stub {
+
+        private final VirtualCameraCallback mCallback;
+        private final Executor mExecutor;
+
+        private VirtualCameraCallbackInternal(VirtualCameraCallback callback, Executor executor) {
+            mCallback = callback;
+            mExecutor = executor;
         }
-        mCallback = requireNonNull(callback, "Missing callback");
-        mCallbackExecutor = requireNonNull(executor, "Missing callback executor");
+
+        @Override
+        public void onStreamConfigured(
+                int streamId, Surface surface, VirtualCameraStreamConfig streamConfig) {
+            mExecutor.execute(() -> mCallback.onStreamConfigured(streamId, surface, streamConfig));
+        }
+
+        @Override
+        public void onProcessCaptureRequest(
+                int streamId, long frameId, VirtualCameraMetadata metadata) {
+            mExecutor.execute(() -> mCallback.onProcessCaptureRequest(streamId, frameId, metadata));
+        }
+
+        @Override
+        public void onStreamClosed(int streamId) {
+            mExecutor.execute(() -> mCallback.onStreamClosed(streamId));
+        }
     }
 
-    /**
-     * @return The display name of this VirtualCamera
-     */
     @NonNull
-    public String getDisplayName() {
-        return mDisplayName;
-    }
+    public static final Parcelable.Creator<VirtualCameraConfig> CREATOR =
+            new Parcelable.Creator<>() {
+                @Override
+                public VirtualCameraConfig createFromParcel(Parcel in) {
+                    return new VirtualCameraConfig(in);
+                }
 
-    /**
-     * Returns an unmodifiable set of the stream configurations added to this {@link
-     * VirtualCameraConfig}.
-     *
-     * @see VirtualCameraConfig.Builder#addStreamConfiguration(int, int, int)
-     */
-    @NonNull
-    public Set<VirtualCameraStreamConfig> getStreamConfigs() {
-        return mStreamConfigurations;
-    }
-
-    /** Returns the callback used to communicate from the server to the client. */
-    @NonNull
-    public VirtualCameraCallback getCallback() {
-        return mCallback;
-    }
-
-    /** Returns the executor onto which the callback should be run. */
-    @NonNull
-    public Executor getCallbackExecutor() {
-        return mCallbackExecutor;
-    }
-
-    /**
-     * Returns a new instance of {@link VirtualCameraHalConfig} initialized with data from this
-     * {@link VirtualCameraConfig}
-     */
-    @NonNull
-    public VirtualCameraHalConfig getHalConfig() {
-        VirtualCameraHalConfig halConfig = new VirtualCameraHalConfig();
-        halConfig.displayName = mDisplayName;
-        halConfig.streamConfigs = mStreamConfigurations.toArray(new VirtualCameraStreamConfig[0]);
-        return halConfig;
-    }
+                @Override
+                public VirtualCameraConfig[] newArray(int size) {
+                    return new VirtualCameraConfig[size];
+                }
+            };
 }
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraHalConfig.aidl b/core/java/android/companion/virtual/camera/VirtualCameraHalConfig.aidl
deleted file mode 100644
index 7070a38..0000000
--- a/core/java/android/companion/virtual/camera/VirtualCameraHalConfig.aidl
+++ /dev/null
@@ -1,12 +0,0 @@
-package android.companion.virtual.camera;
-
-import android.companion.virtual.camera.VirtualCameraStreamConfig;
-
-/**
- * Configuration for VirtualCamera to be passed to the server and HAL service.
- * @hide
- */
-parcelable VirtualCameraHalConfig {
-  String displayName;
-  VirtualCameraStreamConfig[] streamConfigs;
-}
diff --git a/core/java/android/companion/virtual/camera/IVirtualCameraSession.aidl b/core/java/android/companion/virtual/camera/VirtualCameraMetadata.aidl
similarity index 79%
rename from core/java/android/companion/virtual/camera/IVirtualCameraSession.aidl
rename to core/java/android/companion/virtual/camera/VirtualCameraMetadata.aidl
index 2529807..6c1f0fc 100644
--- a/core/java/android/companion/virtual/camera/IVirtualCameraSession.aidl
+++ b/core/java/android/companion/virtual/camera/VirtualCameraMetadata.aidl
@@ -13,16 +13,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package android.companion.virtual.camera;
 
 /**
- * Counterpart of ICameraDeviceSession for virtual camera.
- *
+ * Data structure used to store {@link android.hardware.camera2.CameraMetadata} compatible with
+ * VirtualCamera.
  * @hide
  */
-interface IVirtualCameraSession {
-
-    void configureStream(int width, int height, int format);
-
-    void close();
-}
+parcelable VirtualCameraMetadata;
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraMetadata.java b/core/java/android/companion/virtual/camera/VirtualCameraMetadata.java
new file mode 100644
index 0000000..1ba36d0
--- /dev/null
+++ b/core/java/android/companion/virtual/camera/VirtualCameraMetadata.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.companion.virtual.camera;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.companion.virtual.flags.Flags;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Data structure used to store camera metadata compatible with VirtualCamera.
+ *
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
+public final class VirtualCameraMetadata implements Parcelable {
+
+    /** @hide */
+    public VirtualCameraMetadata(@NonNull Parcel in) {}
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {}
+
+    @NonNull
+    public static final Creator<VirtualCameraMetadata> CREATOR =
+            new Creator<>() {
+                @Override
+                @NonNull
+                public VirtualCameraMetadata createFromParcel(Parcel in) {
+                    return new VirtualCameraMetadata(in);
+                }
+
+                @Override
+                @NonNull
+                public VirtualCameraMetadata[] newArray(int size) {
+                    return new VirtualCameraMetadata[size];
+                }
+            };
+}
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraSession.java b/core/java/android/companion/virtual/camera/VirtualCameraSession.java
deleted file mode 100644
index c25d977..0000000
--- a/core/java/android/companion/virtual/camera/VirtualCameraSession.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.companion.virtual.camera;
-
-/***
- * Counterpart of {@link android.hardware.camera2.CameraCaptureSession} for producing
- * images from a {@link VirtualCamera}.
- * @hide
- */
-// TODO: b/289881985 - This is just a POC implementation for now, this will be extended
-// to a full featured Camera Session
-public interface VirtualCameraSession {
-
-    /** Close the session and release its resources. */
-    default void close() {}
-}
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraSessionInternal.java b/core/java/android/companion/virtual/camera/VirtualCameraSessionInternal.java
deleted file mode 100644
index da168de..0000000
--- a/core/java/android/companion/virtual/camera/VirtualCameraSessionInternal.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.companion.virtual.camera;
-
-import android.graphics.ImageFormat;
-
-import androidx.annotation.NonNull;
-
-import java.util.Objects;
-
-/**
- * Wraps the client side {@link VirtualCameraSession} into an {@link IVirtualCameraSession}.
- *
- * @hide
- */
-final class VirtualCameraSessionInternal extends IVirtualCameraSession.Stub {
-
-    @SuppressWarnings("FieldCanBeLocal")
-    // TODO: b/289881985: Will be used once connected with the CameraService
-    private final VirtualCameraSession mVirtualCameraSession;
-
-    VirtualCameraSessionInternal(@NonNull VirtualCameraSession virtualCameraSession) {
-        mVirtualCameraSession = Objects.requireNonNull(virtualCameraSession);
-    }
-
-    @Override
-    public void configureStream(int width, int height, @ImageFormat.Format int format) {}
-
-    public void close() {}
-}
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
index 304d455..ce92b6d 100644
--- a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
+++ b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.aidl
@@ -16,11 +16,7 @@
 package android.companion.virtual.camera;
 
 /**
- * A stream configuration supported by a virtual camera
+ * The configuration of a single virtual camera stream.
  * @hide
  */
-parcelable VirtualCameraStreamConfig {
-    int width;
-    int height;
-    int format;
-}
+parcelable VirtualCameraStreamConfig;
\ No newline at end of file
diff --git a/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java
new file mode 100644
index 0000000..e198821
--- /dev/null
+++ b/core/java/android/companion/virtual/camera/VirtualCameraStreamConfig.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.companion.virtual.camera;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.companion.virtual.flags.Flags;
+import android.graphics.ImageFormat;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * The configuration of a single virtual camera stream.
+ *
+ * @hide
+ */
+@SystemApi
+@FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
+public final class VirtualCameraStreamConfig implements Parcelable {
+
+    private final int mWidth;
+    private final int mHeight;
+    private final int mFormat;
+
+    /**
+     * Construct a new instance of {@link VirtualCameraStreamConfig} initialized with the provided
+     * width, height and {@link ImageFormat}
+     *
+     * @param width The width of the stream.
+     * @param height The height of the stream.
+     * @param format The {@link ImageFormat} of the stream.
+     */
+    public VirtualCameraStreamConfig(
+            @IntRange(from = 1) int width,
+            @IntRange(from = 1) int height,
+            @ImageFormat.Format int format) {
+        this.mWidth = width;
+        this.mHeight = height;
+        this.mFormat = format;
+    }
+
+    private VirtualCameraStreamConfig(@NonNull Parcel in) {
+        mWidth = in.readInt();
+        mHeight = in.readInt();
+        mFormat = in.readInt();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mWidth);
+        dest.writeInt(mHeight);
+        dest.writeInt(mFormat);
+    }
+
+    @NonNull
+    public static final Creator<VirtualCameraStreamConfig> CREATOR =
+            new Creator<>() {
+                @Override
+                public VirtualCameraStreamConfig createFromParcel(Parcel in) {
+                    return new VirtualCameraStreamConfig(in);
+                }
+
+                @Override
+                public VirtualCameraStreamConfig[] newArray(int size) {
+                    return new VirtualCameraStreamConfig[size];
+                }
+            };
+
+    /** Returns the width of this stream. */
+    @IntRange(from = 1)
+    public int getWidth() {
+        return mWidth;
+    }
+
+    /** Returns the height of this stream. */
+    @IntRange(from = 1)
+    public int getHeight() {
+        return mHeight;
+    }
+
+    /** Returns the {@link ImageFormat} of this stream. */
+    @ImageFormat.Format
+    public int getFormat() {
+        return mFormat;
+    }
+}
diff --git a/core/java/android/hardware/location/ContextHubManager.java b/core/java/android/hardware/location/ContextHubManager.java
index 01ce7b9..481ec72 100644
--- a/core/java/android/hardware/location/ContextHubManager.java
+++ b/core/java/android/hardware/location/ContextHubManager.java
@@ -15,6 +15,8 @@
  */
 package android.hardware.location;
 
+import static java.util.Objects.requireNonNull;
+
 import android.annotation.CallbackExecutor;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
@@ -1111,12 +1113,12 @@
         }
     };
 
-    /** @throws ServiceNotFoundException
-     * @hide */
-    public ContextHubManager(Context context, Looper mainLooper) throws ServiceNotFoundException {
+    /** @hide */
+    public ContextHubManager(@NonNull IContextHubService service, @NonNull Looper mainLooper) {
+        requireNonNull(service, "service cannot be null");
+        requireNonNull(mainLooper, "mainLooper cannot be null");
+        mService = service;
         mMainLooper = mainLooper;
-        mService = IContextHubService.Stub.asInterface(
-                ServiceManager.getServiceOrThrow(Context.CONTEXTHUB_SERVICE));
         try {
             mService.registerCallback(mClientCallback);
         } catch (RemoteException e) {
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 4e7734c..2e82ce2 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -19472,6 +19472,7 @@
              *
              * @hide
              */
+            @Readable
             public static final String WEAR_MEDIA_CONTROLS_PACKAGE = "wear_media_controls_package";
 
             /**
@@ -19479,6 +19480,7 @@
              *
              * @hide
              */
+            @Readable
             public static final String WEAR_MEDIA_SESSIONS_PACKAGE = "wear_media_sessions_package";
 
             /*
diff --git a/packages/SettingsLib/Spa/screenshot/Android.bp b/packages/SettingsLib/Spa/screenshot/Android.bp
index bd508cb..dbf4ce0 100644
--- a/packages/SettingsLib/Spa/screenshot/Android.bp
+++ b/packages/SettingsLib/Spa/screenshot/Android.bp
@@ -18,6 +18,14 @@
     default_applicable_licenses: ["frameworks_base_license"],
 }
 
+filegroup {
+    name: "SpaScreenshotTestRNGFiles",
+    srcs: [
+        "src/**/*.java",
+        "src/**/*.kt",
+    ],
+}
+
 android_test {
     name: "SpaScreenshotTests",
     use_resource_processor: true,
@@ -36,6 +44,7 @@
         "androidx.test.ext.junit",
         "androidx.test.runner",
         "mockito-target-minus-junit4",
+        "platform-parametric-runner-lib",
         "platform-screenshot-diff-core",
     ],
     kotlincflags: ["-Xjvm-default=all"],
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/Android.bp b/packages/SettingsLib/Spa/screenshot/robotests/Android.bp
new file mode 100644
index 0000000..6b8197c
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/Android.bp
@@ -0,0 +1,74 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_library {
+    name: "SpaRoboRNGTestsAssetsLib",
+    asset_dirs: ["assets"],
+    sdk_version: "current",
+    platform_apis: true,
+    manifest: "AndroidManifest.xml",
+    optimize: {
+        enabled: false,
+    },
+    use_resource_processor: true,
+    resource_dirs: ["res"],
+}
+
+android_app {
+    name: "SpaRoboApp",
+    srcs: [],
+    static_libs: [
+        "androidx.test.espresso.core",
+        "androidx.appcompat_appcompat",
+        "flag-junit",
+        "guava",
+        "SpaLib",
+        "SpaLibTestUtils",
+        "SpaRoboRNGTestsAssetsLib",
+        "platform-screenshot-diff-core",
+        "PlatformComposeSceneTransitionLayoutTestsUtils",
+    ],
+    manifest: "robo-manifest.xml",
+    aaptflags: [
+        "--extra-packages",
+        "com.android.settingslib.spa.screenshot",
+    ],
+    dont_merge_manifests: true,
+    platform_apis: true,
+    system_ext_specific: true,
+    certificate: "platform",
+    privileged: true,
+    resource_dirs: [],
+    kotlincflags: ["-Xjvm-default=all"],
+
+    plugins: ["dagger2-compiler"],
+    use_resource_processor: true,
+}
+
+android_robolectric_test {
+    name: "SpaRoboRNGTests",
+    srcs: [
+        ":SpaScreenshotTestRNGFiles",
+        ":flag-junit",
+        ":platform-test-screenshot-rules",
+    ],
+    // Do not add any new libraries here, they should be added to SpaRoboApp above.
+    static_libs: [
+        "androidx.compose.runtime_runtime",
+        "androidx.test.uiautomator_uiautomator",
+        "androidx.test.ext.junit",
+        "inline-mockito-robolectric-prebuilt",
+        "platform-parametric-runner-lib",
+        "uiautomator-helpers",
+    ],
+    libs: [
+        "android.test.runner",
+        "android.test.base",
+        "android.test.mock",
+        "truth",
+    ],
+    upstream: true,
+    java_resource_dirs: ["config"],
+    instrumentation_for: "SpaRoboApp",
+}
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/AndroidManifest.xml b/packages/SettingsLib/Spa/screenshot/robotests/AndroidManifest.xml
new file mode 100644
index 0000000..1918e1c
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright (C) 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.settingslib.spa.screenshot">
+
+    <uses-sdk android:minSdkVersion="21"/>
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+</manifest>
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_actionButtons.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_actionButtons.png
new file mode 100644
index 0000000..b2f3cf1
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_actionButtons.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_barChart.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_barChart.png
new file mode 100644
index 0000000..dc7e756
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_barChart.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_footer.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_footer.png
new file mode 100644
index 0000000..95021fa
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_footer.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_imageIllustration.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_imageIllustration.png
new file mode 100644
index 0000000..281f572
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_imageIllustration.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_lineChart.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_lineChart.png
new file mode 100644
index 0000000..0dc707f
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_lineChart.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_mainSwitchPreference.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_mainSwitchPreference.png
new file mode 100644
index 0000000..7c135a0
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_mainSwitchPreference.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_pieChart.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_pieChart.png
new file mode 100644
index 0000000..7c3e993
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_pieChart.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_preference.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_preference.png
new file mode 100644
index 0000000..7b438c4
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_preference.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_progressBar.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_progressBar.png
new file mode 100644
index 0000000..ac64619
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_progressBar.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_slider.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_slider.png
new file mode 100644
index 0000000..5506c8c
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_slider.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_spinner.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_spinner.png
new file mode 100644
index 0000000..bb518c0
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_spinner.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_switchPreference.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_switchPreference.png
new file mode 100644
index 0000000..f4b9063
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_switchPreference.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_twoTargetSwitchPreference.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_twoTargetSwitchPreference.png
new file mode 100644
index 0000000..fc60c0f
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_landscape_twoTargetSwitchPreference.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_actionButtons.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_actionButtons.png
new file mode 100644
index 0000000..fa06927
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_actionButtons.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_barChart.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_barChart.png
new file mode 100644
index 0000000..698cb4e
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_barChart.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_footer.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_footer.png
new file mode 100644
index 0000000..95021fa
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_footer.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_imageIllustration.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_imageIllustration.png
new file mode 100644
index 0000000..a5f3fa5
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_imageIllustration.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_lineChart.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_lineChart.png
new file mode 100644
index 0000000..c1938b4
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_lineChart.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_mainSwitchPreference.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_mainSwitchPreference.png
new file mode 100644
index 0000000..81a181f
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_mainSwitchPreference.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_pieChart.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_pieChart.png
new file mode 100644
index 0000000..ffc6729
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_pieChart.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_preference.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_preference.png
new file mode 100644
index 0000000..a620040
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_preference.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_progressBar.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_progressBar.png
new file mode 100644
index 0000000..0b40aa8
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_progressBar.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_slider.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_slider.png
new file mode 100644
index 0000000..cfe8587
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_slider.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_spinner.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_spinner.png
new file mode 100644
index 0000000..bb518c0
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_spinner.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_switchPreference.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_switchPreference.png
new file mode 100644
index 0000000..3632755
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_switchPreference.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_twoTargetSwitchPreference.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_twoTargetSwitchPreference.png
new file mode 100644
index 0000000..7e5b602
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/phone/light_portrait_twoTargetSwitchPreference.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_actionButtons.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_actionButtons.png
new file mode 100644
index 0000000..a3692cd
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_actionButtons.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_barChart.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_barChart.png
new file mode 100644
index 0000000..233d088
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_barChart.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_footer.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_footer.png
new file mode 100644
index 0000000..10869f2
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_footer.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_imageIllustration.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_imageIllustration.png
new file mode 100644
index 0000000..3eaecc1
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_imageIllustration.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_lineChart.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_lineChart.png
new file mode 100644
index 0000000..ca61911
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_lineChart.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_mainSwitchPreference.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_mainSwitchPreference.png
new file mode 100644
index 0000000..8a0da31
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_mainSwitchPreference.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_pieChart.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_pieChart.png
new file mode 100644
index 0000000..19d0afd
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_pieChart.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_preference.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_preference.png
new file mode 100644
index 0000000..236a1a0
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_preference.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_progressBar.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_progressBar.png
new file mode 100644
index 0000000..72b7954
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_progressBar.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_slider.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_slider.png
new file mode 100644
index 0000000..36486c4
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_slider.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_spinner.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_spinner.png
new file mode 100644
index 0000000..5bb318f
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_spinner.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_switchPreference.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_switchPreference.png
new file mode 100644
index 0000000..fedce44
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_switchPreference.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_twoTargetSwitchPreference.png b/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_twoTargetSwitchPreference.png
new file mode 100644
index 0000000..3b389d7
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/assets/tablet/dark_portrait_twoTargetSwitchPreference.png
Binary files differ
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/config/robolectric.properties b/packages/SettingsLib/Spa/screenshot/robotests/config/robolectric.properties
new file mode 100644
index 0000000..83d7549
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/config/robolectric.properties
@@ -0,0 +1,15 @@
+# Copyright (C) 2022 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+sdk=NEWEST_SDK
\ No newline at end of file
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/res/drawable/accessibility_captioning_banner.xml b/packages/SettingsLib/Spa/screenshot/robotests/res/drawable/accessibility_captioning_banner.xml
new file mode 100644
index 0000000..6597ffb
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/res/drawable/accessibility_captioning_banner.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="412dp"
+    android:height="300dp"
+    android:viewportWidth="412"
+    android:viewportHeight="300">
+  <path
+      android:pathData="M383.9,300H28.1C12.6,300 0,287.4 0,271.9V28.1C0,12.6 12.6,0 28.1,0h355.8C399.4,0 412,12.6 412,28.1v243.8C412,287.4 399.4,300 383.9,300z"
+      android:fillColor="#FFFFFF"/>
+  <path
+      android:pathData="M79.2,179.6h53.6v8.5h-53.6z"
+      android:fillColor="#1A73E8"/>
+  <path
+      android:pathData="M142.5,179.6h30.4v8.5h-30.4z"
+      android:fillColor="#1A73E8"/>
+  <path
+      android:pathData="M79.2,195.5h79.2v8.5h-79.2z"
+      android:fillColor="#1A73E8"/>
+  <path
+      android:pathData="M168.1,195.5h34.1v8.5h-34.1z"
+      android:fillColor="#1A73E8"/>
+  <path
+      android:pathData="M211.9,195.5h34.1v8.5h-34.1z"
+      android:fillColor="#1A73E8"/>
+  <path
+      android:pathData="M182.7,179.6h73.1v8.5h-73.1z"
+      android:fillColor="#1A73E8"/>
+  <path
+      android:pathData="M265.5,179.6h26.8v8.5h-26.8z"
+      android:fillColor="#1A73E8"/>
+  <path
+      android:pathData="M302.1,179.6h26.8v8.5h-26.8z"
+      android:fillColor="#1A73E8"/>
+  <path
+      android:pathData="M142.7,67.9h-11.5c-1.6,0 -2.9,1.3 -2.9,2.9H67.8c-7.9,0 -14.4,6.5 -14.4,14.4v132.4c0,7.9 6.5,14.4 14.4,14.4h276.4c7.9,0 14.4,-6.5 14.4,-14.4V85.2c0,-7.9 -6.5,-14.4 -14.4,-14.4H203.1c0,-1.6 -1.3,-2.9 -2.9,-2.9h-28.8c-1.6,0 -2.9,1.3 -2.9,2.9h-23C145.5,69.2 144.3,67.9 142.7,67.9zM344.2,73.7c6.4,0 11.5,5.2 11.5,11.5v132.4c0,6.3 -5.2,11.5 -11.5,11.5H67.8c-6.4,0 -11.5,-5.2 -11.5,-11.5V85.2c0,-6.3 5.2,-11.5 11.5,-11.5H344.2z"
+      android:fillColor="#DADCE0"/>
+</vector>
diff --git a/packages/SettingsLib/Spa/screenshot/robotests/robo-manifest.xml b/packages/SettingsLib/Spa/screenshot/robotests/robo-manifest.xml
new file mode 100644
index 0000000..af1a11e
--- /dev/null
+++ b/packages/SettingsLib/Spa/screenshot/robotests/robo-manifest.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--- Include all the namespaces we will ever need anywhere, because this is the source the manifest merger uses for namespaces -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+          xmlns:tools="http://schemas.android.com/tools"
+          package="com.android.settingslib.spa.screenshot"
+          coreApp="true">
+  <application>
+    <activity
+        android:name="androidx.activity.ComponentActivity"
+        android:exported="true">
+    </activity>
+  </application>
+</manifest>
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/SettingsScreenshotTestRule.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/SettingsScreenshotTestRule.kt
index 3dcefe9..1cbdc33 100644
--- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/SettingsScreenshotTestRule.kt
+++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/util/SettingsScreenshotTestRule.kt
@@ -16,6 +16,7 @@
 
 package com.android.settingslib.spa.screenshot.util
 
+import android.os.Build
 import androidx.activity.ComponentActivity
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Surface
@@ -49,15 +50,19 @@
             )
         )
     private val composeRule = createAndroidComposeRule<ComponentActivity>()
-    private val delegateRule =
-        RuleChain.outerRule(colorsRule)
-            .around(deviceEmulationRule)
+    private val roboRule =
+        RuleChain.outerRule(deviceEmulationRule)
             .around(screenshotRule)
             .around(composeRule)
+    private val delegateRule =
+        RuleChain.outerRule(colorsRule)
+            .around(roboRule)
     private val matcher = UnitTestBitmapMatcher
+    private val isRobolectric = if (Build.FINGERPRINT.contains("robolectric")) true else false
 
     override fun apply(base: Statement, description: Description): Statement {
-        return delegateRule.apply(base, description)
+        val ruleToApply = if (isRobolectric) roboRule else delegateRule
+        return ruleToApply.apply(base, description)
     }
 
     /**
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/button/ActionButtonsScreenshotTest.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/button/ActionButtonsScreenshotTest.kt
index b74a243..2cb6044 100644
--- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/button/ActionButtonsScreenshotTest.kt
+++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/button/ActionButtonsScreenshotTest.kt
@@ -26,15 +26,16 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 import platform.test.screenshot.DeviceEmulationSpec
 import platform.test.screenshot.PhoneAndTabletMinimal
 
 /** A screenshot test for ExampleFeature. */
-@RunWith(Parameterized::class)
+@RunWith(ParameterizedAndroidJunit4::class)
 class ActionButtonsScreenshotTest(emulationSpec: DeviceEmulationSpec) {
     companion object {
-        @Parameterized.Parameters(name = "{0}")
+        @Parameters(name = "{0}")
         @JvmStatic
         fun getTestSpecs() = DeviceEmulationSpec.PhoneAndTabletMinimal
     }
diff --git a/core/java/android/companion/virtual/camera/IVirtualCameraSession.aidl b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/button/package-info.java
similarity index 71%
copy from core/java/android/companion/virtual/camera/IVirtualCameraSession.aidl
copy to packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/button/package-info.java
index 2529807..8e55695 100644
--- a/core/java/android/companion/virtual/camera/IVirtualCameraSession.aidl
+++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/button/package-info.java
@@ -13,16 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.companion.virtual.camera;
 
-/**
- * Counterpart of ICameraDeviceSession for virtual camera.
- *
- * @hide
- */
-interface IVirtualCameraSession {
+@GraphicsMode(GraphicsMode.Mode.NATIVE)
+package com.android.settingslib.spa.screenshot.widget.button;
 
-    void configureStream(int width, int height, int format);
-
-    void close();
-}
+import org.robolectric.annotation.GraphicsMode;
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/chart/BarChartScreenshotTest.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/chart/BarChartScreenshotTest.kt
index 051ef77..7ef9f10 100644
--- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/chart/BarChartScreenshotTest.kt
+++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/chart/BarChartScreenshotTest.kt
@@ -25,15 +25,16 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 import platform.test.screenshot.DeviceEmulationSpec
 import platform.test.screenshot.PhoneAndTabletMinimal
 
 /** A screenshot test for ExampleFeature. */
-@RunWith(Parameterized::class)
+@RunWith(ParameterizedAndroidJunit4::class)
 class BarChartScreenshotTest(emulationSpec: DeviceEmulationSpec) {
     companion object {
-        @Parameterized.Parameters(name = "{0}")
+        @Parameters(name = "{0}")
         @JvmStatic
         fun getTestSpecs() = DeviceEmulationSpec.PhoneAndTabletMinimal
     }
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/chart/LineChartScreenshotTest.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/chart/LineChartScreenshotTest.kt
index 3822571..3790164 100644
--- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/chart/LineChartScreenshotTest.kt
+++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/chart/LineChartScreenshotTest.kt
@@ -25,15 +25,16 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 import platform.test.screenshot.DeviceEmulationSpec
 import platform.test.screenshot.PhoneAndTabletMinimal
 
 /** A screenshot test for ExampleFeature. */
-@RunWith(Parameterized::class)
+@RunWith(ParameterizedAndroidJunit4::class)
 class LineChartScreenshotTest(emulationSpec: DeviceEmulationSpec) {
     companion object {
-        @Parameterized.Parameters(name = "{0}")
+        @Parameters(name = "{0}")
         @JvmStatic
         fun getTestSpecs() = DeviceEmulationSpec.PhoneAndTabletMinimal
     }
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/chart/PieChartScreenshotTest.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/chart/PieChartScreenshotTest.kt
index 6dd62ec..3c3cc85 100644
--- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/chart/PieChartScreenshotTest.kt
+++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/chart/PieChartScreenshotTest.kt
@@ -23,15 +23,16 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 import platform.test.screenshot.DeviceEmulationSpec
 import platform.test.screenshot.PhoneAndTabletMinimal
 
 /** A screenshot test for ExampleFeature. */
-@RunWith(Parameterized::class)
+@RunWith(ParameterizedAndroidJunit4::class)
 class PieChartScreenshotTest(emulationSpec: DeviceEmulationSpec) {
     companion object {
-        @Parameterized.Parameters(name = "{0}")
+        @Parameters(name = "{0}")
         @JvmStatic
         fun getTestSpecs() = DeviceEmulationSpec.PhoneAndTabletMinimal
     }
diff --git a/core/java/android/companion/virtual/camera/IVirtualCameraSession.aidl b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/chart/package-info.java
similarity index 71%
copy from core/java/android/companion/virtual/camera/IVirtualCameraSession.aidl
copy to packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/chart/package-info.java
index 2529807..afe3f07 100644
--- a/core/java/android/companion/virtual/camera/IVirtualCameraSession.aidl
+++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/chart/package-info.java
@@ -13,16 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.companion.virtual.camera;
 
-/**
- * Counterpart of ICameraDeviceSession for virtual camera.
- *
- * @hide
- */
-interface IVirtualCameraSession {
+@GraphicsMode(GraphicsMode.Mode.NATIVE)
+package com.android.settingslib.spa.screenshot.widget.chart;
 
-    void configureStream(int width, int height, int format);
-
-    void close();
-}
+import org.robolectric.annotation.GraphicsMode;
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/illustration/ImageIllustrationScreenshotTest.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/illustration/ImageIllustrationScreenshotTest.kt
index 0ccfc0b..616b225 100644
--- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/illustration/ImageIllustrationScreenshotTest.kt
+++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/illustration/ImageIllustrationScreenshotTest.kt
@@ -24,15 +24,16 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 import platform.test.screenshot.DeviceEmulationSpec
 import platform.test.screenshot.PhoneAndTabletMinimal
 
 /** A screenshot test for ExampleFeature. */
-@RunWith(Parameterized::class)
+@RunWith(ParameterizedAndroidJunit4::class)
 class ImageIllustrationScreenshotTest(emulationSpec: DeviceEmulationSpec) {
     companion object {
-        @Parameterized.Parameters(name = "{0}")
+        @Parameters(name = "{0}")
         @JvmStatic
         fun getTestSpecs() = DeviceEmulationSpec.PhoneAndTabletMinimal
     }
diff --git a/core/java/android/companion/virtual/camera/IVirtualCameraSession.aidl b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/illustration/package-info.java
similarity index 71%
copy from core/java/android/companion/virtual/camera/IVirtualCameraSession.aidl
copy to packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/illustration/package-info.java
index 2529807..0089c2e 100644
--- a/core/java/android/companion/virtual/camera/IVirtualCameraSession.aidl
+++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/illustration/package-info.java
@@ -13,16 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.companion.virtual.camera;
 
-/**
- * Counterpart of ICameraDeviceSession for virtual camera.
- *
- * @hide
- */
-interface IVirtualCameraSession {
+@GraphicsMode(GraphicsMode.Mode.NATIVE)
+package com.android.settingslib.spa.screenshot.widget.illustration;
 
-    void configureStream(int width, int height, int format);
-
-    void close();
-}
+import org.robolectric.annotation.GraphicsMode;
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/MainSwitchPreferenceScreenshotTest.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/MainSwitchPreferenceScreenshotTest.kt
index e547d26..8dd4ce7 100644
--- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/MainSwitchPreferenceScreenshotTest.kt
+++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/MainSwitchPreferenceScreenshotTest.kt
@@ -23,15 +23,16 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 import platform.test.screenshot.DeviceEmulationSpec
 import platform.test.screenshot.PhoneAndTabletMinimal
 
 /** A screenshot test for ExampleFeature. */
-@RunWith(Parameterized::class)
+@RunWith(ParameterizedAndroidJunit4::class)
 class MainSwitchPreferenceScreenshotTest(emulationSpec: DeviceEmulationSpec) {
     companion object {
-        @Parameterized.Parameters(name = "{0}")
+        @Parameters(name = "{0}")
         @JvmStatic
         fun getTestSpecs() = DeviceEmulationSpec.PhoneAndTabletMinimal
     }
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/PreferenceScreenshotTest.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/PreferenceScreenshotTest.kt
index dd6b553..1e1a785 100644
--- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/PreferenceScreenshotTest.kt
+++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/PreferenceScreenshotTest.kt
@@ -28,15 +28,16 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 import platform.test.screenshot.DeviceEmulationSpec
 import platform.test.screenshot.PhoneAndTabletMinimal
 
 /** A screenshot test for ExampleFeature. */
-@RunWith(Parameterized::class)
+@RunWith(ParameterizedAndroidJunit4::class)
 class PreferenceScreenshotTest(emulationSpec: DeviceEmulationSpec) {
     companion object {
-        @Parameterized.Parameters(name = "{0}")
+        @Parameters(name = "{0}")
         @JvmStatic
         fun getTestSpecs() = DeviceEmulationSpec.PhoneAndTabletMinimal
         private const val TITLE = "Title"
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/ProgressBarPreferenceScreenshotTest.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/ProgressBarPreferenceScreenshotTest.kt
index 357d815..d1878a74 100644
--- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/ProgressBarPreferenceScreenshotTest.kt
+++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/ProgressBarPreferenceScreenshotTest.kt
@@ -29,15 +29,16 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 import platform.test.screenshot.DeviceEmulationSpec
 import platform.test.screenshot.PhoneAndTabletMinimal
 
 /** A screenshot test for ExampleFeature. */
-@RunWith(Parameterized::class)
+@RunWith(ParameterizedAndroidJunit4::class)
 class ProgressBarPreferenceScreenshotTest(emulationSpec: DeviceEmulationSpec) {
     companion object {
-        @Parameterized.Parameters(name = "{0}")
+        @Parameters(name = "{0}")
         @JvmStatic
         fun getTestSpecs() = DeviceEmulationSpec.PhoneAndTabletMinimal
     }
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/SliderPreferenceScreenshotTest.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/SliderPreferenceScreenshotTest.kt
index fdee7ee..c9f098b 100644
--- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/SliderPreferenceScreenshotTest.kt
+++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/SliderPreferenceScreenshotTest.kt
@@ -25,15 +25,16 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 import platform.test.screenshot.DeviceEmulationSpec
 import platform.test.screenshot.PhoneAndTabletMinimal
 
 /** A screenshot test for ExampleFeature. */
-@RunWith(Parameterized::class)
+@RunWith(ParameterizedAndroidJunit4::class)
 class SliderPreferenceScreenshotTest(emulationSpec: DeviceEmulationSpec) {
     companion object {
-        @Parameterized.Parameters(name = "{0}")
+        @Parameters(name = "{0}")
         @JvmStatic
         fun getTestSpecs() = DeviceEmulationSpec.PhoneAndTabletMinimal
     }
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/SwitchPreferenceScreenshotTest.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/SwitchPreferenceScreenshotTest.kt
index 6966a74..eca40fb 100644
--- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/SwitchPreferenceScreenshotTest.kt
+++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/SwitchPreferenceScreenshotTest.kt
@@ -27,15 +27,16 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 import platform.test.screenshot.DeviceEmulationSpec
 import platform.test.screenshot.PhoneAndTabletMinimal
 
 /** A screenshot test for ExampleFeature. */
-@RunWith(Parameterized::class)
+@RunWith(ParameterizedAndroidJunit4::class)
 class SwitchPreferenceScreenshotTest(emulationSpec: DeviceEmulationSpec) {
     companion object {
-        @Parameterized.Parameters(name = "{0}")
+        @Parameters(name = "{0}")
         @JvmStatic
         fun getTestSpecs() = DeviceEmulationSpec.PhoneAndTabletMinimal
     }
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/TwoTargetSwitchPreferenceScreenshotTest.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/TwoTargetSwitchPreferenceScreenshotTest.kt
index 72c5cb8..f81a59f 100644
--- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/TwoTargetSwitchPreferenceScreenshotTest.kt
+++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/TwoTargetSwitchPreferenceScreenshotTest.kt
@@ -24,15 +24,16 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 import platform.test.screenshot.DeviceEmulationSpec
 import platform.test.screenshot.PhoneAndTabletMinimal
 
 /** A screenshot test for ExampleFeature. */
-@RunWith(Parameterized::class)
+@RunWith(ParameterizedAndroidJunit4::class)
 class TwoTargetSwitchPreferenceScreenshotTest(emulationSpec: DeviceEmulationSpec) {
     companion object {
-        @Parameterized.Parameters(name = "{0}")
+        @Parameters(name = "{0}")
         @JvmStatic
         fun getTestSpecs() = DeviceEmulationSpec.PhoneAndTabletMinimal
     }
diff --git a/core/java/android/companion/virtual/camera/IVirtualCameraSession.aidl b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/package-info.java
similarity index 71%
copy from core/java/android/companion/virtual/camera/IVirtualCameraSession.aidl
copy to packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/package-info.java
index 2529807..fd6a5dd 100644
--- a/core/java/android/companion/virtual/camera/IVirtualCameraSession.aidl
+++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/preference/package-info.java
@@ -13,16 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.companion.virtual.camera;
 
-/**
- * Counterpart of ICameraDeviceSession for virtual camera.
- *
- * @hide
- */
-interface IVirtualCameraSession {
+@GraphicsMode(GraphicsMode.Mode.NATIVE)
+package com.android.settingslib.spa.screenshot.widget.preference;
 
-    void configureStream(int width, int height, int format);
-
-    void close();
-}
+import org.robolectric.annotation.GraphicsMode;
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/ui/FooterScreenshotTest.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/ui/FooterScreenshotTest.kt
index fb01f77..98a4288 100644
--- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/ui/FooterScreenshotTest.kt
+++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/ui/FooterScreenshotTest.kt
@@ -21,15 +21,16 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 import platform.test.screenshot.DeviceEmulationSpec
 import platform.test.screenshot.PhoneAndTabletMinimal
 
 /** A screenshot test for ExampleFeature. */
-@RunWith(Parameterized::class)
+@RunWith(ParameterizedAndroidJunit4::class)
 class FooterScreenshotTest(emulationSpec: DeviceEmulationSpec) {
     companion object {
-        @Parameterized.Parameters(name = "{0}")
+        @Parameters(name = "{0}")
         @JvmStatic
         fun getTestSpecs() = DeviceEmulationSpec.PhoneAndTabletMinimal
     }
diff --git a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/ui/SpinnerScreenshotTest.kt b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/ui/SpinnerScreenshotTest.kt
index 2867741..5417095 100644
--- a/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/ui/SpinnerScreenshotTest.kt
+++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/ui/SpinnerScreenshotTest.kt
@@ -22,15 +22,16 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
 import platform.test.screenshot.DeviceEmulationSpec
 import platform.test.screenshot.PhoneAndTabletMinimal
 
 /** A screenshot test for ExampleFeature. */
-@RunWith(Parameterized::class)
+@RunWith(ParameterizedAndroidJunit4::class)
 class SpinnerScreenshotTest(emulationSpec: DeviceEmulationSpec) {
     companion object {
-        @Parameterized.Parameters(name = "{0}")
+        @Parameters(name = "{0}")
         @JvmStatic
         fun getTestSpecs() = DeviceEmulationSpec.PhoneAndTabletMinimal
     }
diff --git a/core/java/android/companion/virtual/camera/IVirtualCameraSession.aidl b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/ui/package-info.java
similarity index 71%
copy from core/java/android/companion/virtual/camera/IVirtualCameraSession.aidl
copy to packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/ui/package-info.java
index 2529807..45210ab 100644
--- a/core/java/android/companion/virtual/camera/IVirtualCameraSession.aidl
+++ b/packages/SettingsLib/Spa/screenshot/src/com/android/settingslib/spa/screenshot/widget/ui/package-info.java
@@ -13,16 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package android.companion.virtual.camera;
 
-/**
- * Counterpart of ICameraDeviceSession for virtual camera.
- *
- * @hide
- */
-interface IVirtualCameraSession {
+@GraphicsMode(GraphicsMode.Mode.NATIVE)
+package com.android.settingslib.spa.screenshot.widget.ui;
 
-    void configureStream(int width, int height, int format);
-
-    void close();
-}
+import org.robolectric.annotation.GraphicsMode;
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 80fd516..0be9f84 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -294,6 +294,8 @@
         "tests/src/com/android/systemui/bouncer/ui/viewmodel/KeyguardBouncerViewModelTest.kt",
         "tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt",
         "tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt",
+        "tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt",
+        "tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt",
         "tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt",
         "tests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt",
         // Keyguard helper
@@ -616,6 +618,9 @@
 
     instrumentation_for: "SystemUIRobo-stub",
     java_resource_dirs: ["tests/robolectric/config"],
+    plugins: [
+        "dagger2-compiler",
+    ],
 }
 
 // Opt-out config for optimizing the SystemUI target using R8.
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 1f2621d..1dcc540 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -59,6 +59,7 @@
 import com.android.systemui.keyboard.KeyboardModule;
 import com.android.systemui.keyevent.data.repository.KeyEventRepositoryModule;
 import com.android.systemui.keyguard.ui.view.layout.blueprints.KeyguardBlueprintModule;
+import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSectionsModule;
 import com.android.systemui.log.dagger.LogModule;
 import com.android.systemui.log.dagger.MonitorLog;
 import com.android.systemui.log.table.TableLogBuffer;
@@ -190,6 +191,7 @@
         KeyEventRepositoryModule.class,
         KeyboardModule.class,
         KeyguardBlueprintModule.class,
+        KeyguardSectionsModule.class,
         LetterboxModule.class,
         LogModule.class,
         MediaProjectionModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
index 8d5d73f..5c76be8 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
@@ -49,8 +49,10 @@
  * [TransitionInteractor]. These interactors will call [startTransition] and [updateTransition] on
  * this repository.
  *
- * To print all transitions to logcat to help with debugging, run this command: adb shell settings
- * put global systemui/buffer/KeyguardLog VERBOSE
+ * To print all transitions to logcat to help with debugging, run this command:
+ * ```
+ * adb shell cmd statusbar echo -b KeyguardLog:VERBOSE
+ * ```
  *
  * This will print all keyguard transitions to logcat with the KeyguardTransitionAuditLogger tag.
  */
@@ -175,9 +177,11 @@
                     override fun onAnimationStart(animation: Animator) {
                         emitTransition(TransitionStep(info, startingValue, TransitionState.STARTED))
                     }
+
                     override fun onAnimationCancel(animation: Animator) {
                         endAnimation(lastStep.value, TransitionState.CANCELED)
                     }
+
                     override fun onAnimationEnd(animation: Animator) {
                         endAnimation(1f, TransitionState.FINISHED)
                     }
@@ -222,7 +226,7 @@
     }
 
     private fun emitTransition(nextStep: TransitionStep, isManual: Boolean = false) {
-        trace(nextStep, isManual)
+        logAndTrace(nextStep, isManual)
         val emitted = _transitions.tryEmit(nextStep)
         if (!emitted) {
             Log.w(TAG, "Failed to emit next value without suspending")
@@ -230,26 +234,22 @@
         lastStep = nextStep
     }
 
-    private fun trace(step: TransitionStep, isManual: Boolean) {
+    private fun logAndTrace(step: TransitionStep, isManual: Boolean) {
         if (step.transitionState == TransitionState.RUNNING) {
             return
         }
-        val traceName =
-            "Transition: ${step.from} -> ${step.to} " +
-                if (isManual) {
-                    "(manual)"
-                } else {
-                    ""
-                }
+        val manualStr = if (isManual) " (manual)" else ""
+        val traceName = "Transition: ${step.from} -> ${step.to}$manualStr"
+
         val traceCookie = traceName.hashCode()
-        if (step.transitionState == TransitionState.STARTED) {
-            Trace.beginAsyncSection(traceName, traceCookie)
-        } else if (
-            step.transitionState == TransitionState.FINISHED ||
-                step.transitionState == TransitionState.CANCELED
-        ) {
-            Trace.endAsyncSection(traceName, traceCookie)
+        when (step.transitionState) {
+            TransitionState.STARTED -> Trace.beginAsyncSection(traceName, traceCookie)
+            TransitionState.FINISHED -> Trace.endAsyncSection(traceName, traceCookie)
+            TransitionState.CANCELED -> Trace.endAsyncSection(traceName, traceCookie)
+            else -> {}
         }
+
+        Log.i(TAG, "${step.transitionState.name} transition: $step$manualStr")
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt
index 419524fe..a03fa38 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt
@@ -75,24 +75,6 @@
         }
 
         scope.launch {
-            interactor.finishedKeyguardTransitionStep.collect {
-                logger.log(TAG, VERBOSE, "Finished transition", it)
-            }
-        }
-
-        scope.launch {
-            interactor.canceledKeyguardTransitionStep.collect {
-                logger.log(TAG, VERBOSE, "Canceled transition", it)
-            }
-        }
-
-        scope.launch {
-            interactor.startedKeyguardTransitionStep.collect {
-                logger.log(TAG, VERBOSE, "Started transition", it)
-            }
-        }
-
-        scope.launch {
             keyguardInteractor.dozeTransitionModel.collect {
                 logger.log(TAG, VERBOSE, "Doze transition", it)
             }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardAmbientIndicationAreaViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardAmbientIndicationAreaViewBinder.kt
deleted file mode 100644
index 5900a24..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardAmbientIndicationAreaViewBinder.kt
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.keyguard.ui.binder
-
-import android.view.View
-import android.view.ViewGroup
-import android.view.ViewPropertyAnimator
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.repeatOnLifecycle
-import com.android.systemui.res.R
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardAmbientIndicationViewModel
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
-import com.android.systemui.lifecycle.repeatWhenAttached
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.launch
-
-object KeyguardAmbientIndicationAreaViewBinder {
-    /**
-     * Defines interface for an object that acts as the binding between the view and its view-model.
-     *
-     * Users of the [KeyguardBottomAreaViewBinder] class should use this to control the binder after
-     * it is bound.
-     */
-    interface Binding {
-        /**
-         * Returns a collection of [ViewPropertyAnimator] instances that can be used to animate the
-         * indication areas.
-         */
-        fun getIndicationAreaAnimators(): List<ViewPropertyAnimator>
-
-        /** Notifies that device configuration has changed. */
-        fun onConfigurationChanged()
-
-        /** Destroys this binding, releases resources, and cancels any coroutines. */
-        fun destroy()
-    }
-
-    @OptIn(ExperimentalCoroutinesApi::class)
-    fun bind(
-        view: ViewGroup,
-        viewModel: KeyguardAmbientIndicationViewModel,
-        keyguardRootViewModel: KeyguardRootViewModel,
-    ): Binding {
-        val ambientIndicationArea: View? = view.findViewById(R.id.ambient_indication_container)
-        val configurationBasedDimensions = MutableStateFlow(loadFromResources(view))
-
-        val disposableHandle =
-            view.repeatWhenAttached {
-                repeatOnLifecycle(Lifecycle.State.STARTED) {
-                    launch {
-                        keyguardRootViewModel.alpha.collect { alpha ->
-                            ambientIndicationArea?.apply {
-                                this.importantForAccessibility =
-                                    if (alpha == 0f) {
-                                        View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
-                                    } else {
-                                        View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
-                                    }
-                                this.alpha = alpha
-                            }
-                        }
-                    }
-
-                    launch {
-                        viewModel.indicationAreaTranslationX.collect { translationX ->
-                            ambientIndicationArea?.translationX = translationX
-                        }
-                    }
-
-                    launch {
-                        configurationBasedDimensions
-                            .map { it.defaultBurnInPreventionYOffsetPx }
-                            .flatMapLatest { defaultBurnInOffsetY ->
-                                viewModel.indicationAreaTranslationY(defaultBurnInOffsetY)
-                            }
-                            .collect { translationY ->
-                                ambientIndicationArea?.translationY = translationY
-                            }
-                    }
-
-                }
-            }
-
-
-        return object : Binding {
-            override fun getIndicationAreaAnimators(): List<ViewPropertyAnimator> {
-                return listOf(ambientIndicationArea).mapNotNull { it?.animate() }
-            }
-
-            override fun onConfigurationChanged() {
-                configurationBasedDimensions.value = loadFromResources(view)
-            }
-
-            override fun destroy() {
-                disposableHandle.dispose()
-            }
-        }
-    }
-
-    private fun loadFromResources(view: View): ConfigurationBasedDimensions {
-        return ConfigurationBasedDimensions(
-            defaultBurnInPreventionYOffsetPx =
-                view.resources.getDimensionPixelOffset(R.dimen.default_burn_in_prevention_offset),
-        )
-    }
-
-    private data class ConfigurationBasedDimensions(
-        val defaultBurnInPreventionYOffsetPx: Int,
-    )
-}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt
index 2e64c41..0cf891c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt
@@ -20,10 +20,10 @@
 import com.android.systemui.communal.ui.view.layout.sections.CommunalTutorialIndicatorSection
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.shared.model.KeyguardBlueprint
+import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.keyguard.ui.view.layout.items.ClockSection
 import com.android.systemui.keyguard.ui.view.layout.sections.AodBurnInSection
 import com.android.systemui.keyguard.ui.view.layout.sections.AodNotificationIconsSection
-import com.android.systemui.keyguard.ui.view.layout.sections.DefaultAmbientIndicationAreaSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultDeviceEntryIconSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultIndicationAreaSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultNotificationStackScrollLayoutSection
@@ -31,8 +31,13 @@
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultShortcutsSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusBarSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusViewSection
+import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSectionsModule.Companion.KEYGUARD_AMBIENT_INDICATION_AREA_SECTION
 import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection
+import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeGuidelines
+import java.util.Optional
 import javax.inject.Inject
+import javax.inject.Named
+import kotlin.jvm.optionals.getOrNull
 
 /**
  * Positions elements of the lockscreen to the default position.
@@ -47,7 +52,8 @@
     defaultIndicationAreaSection: DefaultIndicationAreaSection,
     defaultDeviceEntryIconSection: DefaultDeviceEntryIconSection,
     defaultShortcutsSection: DefaultShortcutsSection,
-    defaultAmbientIndicationAreaSection: DefaultAmbientIndicationAreaSection,
+    @Named(KEYGUARD_AMBIENT_INDICATION_AREA_SECTION)
+    defaultAmbientIndicationAreaSection: Optional<KeyguardSection>,
     defaultSettingsPopupMenuSection: DefaultSettingsPopupMenuSection,
     defaultStatusViewSection: DefaultStatusViewSection,
     defaultStatusBarSection: DefaultStatusBarSection,
@@ -61,11 +67,11 @@
     override val id: String = DEFAULT
 
     override val sections =
-        listOf(
+        listOfNotNull(
             defaultIndicationAreaSection,
             defaultDeviceEntryIconSection,
             defaultShortcutsSection,
-            defaultAmbientIndicationAreaSection,
+            defaultAmbientIndicationAreaSection.getOrNull(),
             defaultSettingsPopupMenuSection,
             defaultStatusViewSection,
             defaultStatusBarSection,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/ShortcutsBesideUdfpsKeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/ShortcutsBesideUdfpsKeyguardBlueprint.kt
index d8b368b..14e8f89 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/ShortcutsBesideUdfpsKeyguardBlueprint.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/ShortcutsBesideUdfpsKeyguardBlueprint.kt
@@ -19,18 +19,22 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.shared.model.KeyguardBlueprint
+import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.keyguard.ui.view.layout.sections.AlignShortcutsToUdfpsSection
 import com.android.systemui.keyguard.ui.view.layout.sections.AodBurnInSection
 import com.android.systemui.keyguard.ui.view.layout.sections.AodNotificationIconsSection
-import com.android.systemui.keyguard.ui.view.layout.sections.DefaultAmbientIndicationAreaSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultDeviceEntryIconSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultIndicationAreaSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultNotificationStackScrollLayoutSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultSettingsPopupMenuSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusBarSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusViewSection
+import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSectionsModule
 import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeGuidelines
+import com.android.systemui.util.kotlin.getOrNull
+import java.util.Optional
 import javax.inject.Inject
+import javax.inject.Named
 
 /** Vertically aligns the shortcuts with the udfps. */
 @SysUISingleton
@@ -39,7 +43,8 @@
 constructor(
     defaultIndicationAreaSection: DefaultIndicationAreaSection,
     defaultDeviceEntryIconSection: DefaultDeviceEntryIconSection,
-    defaultAmbientIndicationAreaSection: DefaultAmbientIndicationAreaSection,
+    @Named(KeyguardSectionsModule.KEYGUARD_AMBIENT_INDICATION_AREA_SECTION)
+    defaultAmbientIndicationAreaSection: Optional<KeyguardSection>,
     defaultSettingsPopupMenuSection: DefaultSettingsPopupMenuSection,
     alignShortcutsToUdfpsSection: AlignShortcutsToUdfpsSection,
     defaultStatusViewSection: DefaultStatusViewSection,
@@ -52,10 +57,10 @@
     override val id: String = SHORTCUTS_BESIDE_UDFPS
 
     override val sections =
-        listOf(
+        listOfNotNull(
             defaultIndicationAreaSection,
             defaultDeviceEntryIconSection,
-            defaultAmbientIndicationAreaSection,
+            defaultAmbientIndicationAreaSection.getOrNull(),
             defaultSettingsPopupMenuSection,
             alignShortcutsToUdfpsSection,
             defaultStatusViewSection,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt
index 35679b8..0d397bf 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt
@@ -20,18 +20,22 @@
 import com.android.systemui.communal.ui.view.layout.sections.CommunalTutorialIndicatorSection
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.shared.model.KeyguardBlueprint
+import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.keyguard.ui.view.layout.sections.AodBurnInSection
 import com.android.systemui.keyguard.ui.view.layout.sections.AodNotificationIconsSection
-import com.android.systemui.keyguard.ui.view.layout.sections.DefaultAmbientIndicationAreaSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultDeviceEntryIconSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultIndicationAreaSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultSettingsPopupMenuSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultShortcutsSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusBarSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusViewSection
+import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSectionsModule
 import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeGuidelines
 import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeNotificationStackScrollLayoutSection
+import com.android.systemui.util.kotlin.getOrNull
+import java.util.Optional
 import javax.inject.Inject
+import javax.inject.Named
 
 /**
  * Split-shade layout, mostly used for larger devices like foldables and tablets when in landscape
@@ -45,7 +49,8 @@
     defaultIndicationAreaSection: DefaultIndicationAreaSection,
     defaultDeviceEntryIconSection: DefaultDeviceEntryIconSection,
     defaultShortcutsSection: DefaultShortcutsSection,
-    defaultAmbientIndicationAreaSection: DefaultAmbientIndicationAreaSection,
+    @Named(KeyguardSectionsModule.KEYGUARD_AMBIENT_INDICATION_AREA_SECTION)
+    defaultAmbientIndicationAreaSection: Optional<KeyguardSection>,
     defaultSettingsPopupMenuSection: DefaultSettingsPopupMenuSection,
     defaultStatusViewSection: DefaultStatusViewSection,
     defaultStatusBarSection: DefaultStatusBarSection,
@@ -58,11 +63,11 @@
     override val id: String = ID
 
     override val sections =
-        listOf(
+        listOfNotNull(
             defaultIndicationAreaSection,
             defaultDeviceEntryIconSection,
             defaultShortcutsSection,
-            defaultAmbientIndicationAreaSection,
+            defaultAmbientIndicationAreaSection.getOrNull(),
             defaultSettingsPopupMenuSection,
             defaultStatusViewSection,
             defaultStatusBarSection,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt
index eb01d4f6..b7a165c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt
@@ -28,6 +28,8 @@
 import com.android.systemui.Flags.keyguardBottomAreaRefactor
 import com.android.systemui.res.R
 import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.flags.FeatureFlagsClassic
+import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
@@ -46,6 +48,7 @@
     private val falsingManager: FalsingManager,
     private val indicationController: KeyguardIndicationController,
     private val vibratorHelper: VibratorHelper,
+    private val featureFlags: FeatureFlagsClassic,
 ) : BaseShortcutSection() {
     override fun addViews(constraintLayout: ConstraintLayout) {
         if (keyguardBottomAreaRefactor()) {
@@ -83,20 +86,26 @@
         val width = resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_width)
         val height = resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height)
 
+        val lockIconViewId = if (featureFlags.isEnabled(Flags.REFACTOR_UDFPS_KEYGUARD_VIEWS)) {
+            R.id.device_entry_icon_view
+        } else {
+            R.id.lock_icon_view
+        }
+
         constraintSet.apply {
             constrainWidth(R.id.start_button, width)
             constrainHeight(R.id.start_button, height)
             connect(R.id.start_button, LEFT, PARENT_ID, LEFT)
-            connect(R.id.start_button, RIGHT, R.id.lock_icon_view, LEFT)
-            connect(R.id.start_button, TOP, R.id.lock_icon_view, TOP)
-            connect(R.id.start_button, BOTTOM, R.id.lock_icon_view, BOTTOM)
+            connect(R.id.start_button, RIGHT, lockIconViewId, LEFT)
+            connect(R.id.start_button, TOP, lockIconViewId, TOP)
+            connect(R.id.start_button, BOTTOM, lockIconViewId, BOTTOM)
 
             constrainWidth(R.id.end_button, width)
             constrainHeight(R.id.end_button, height)
             connect(R.id.end_button, RIGHT, PARENT_ID, RIGHT)
-            connect(R.id.end_button, LEFT, R.id.lock_icon_view, RIGHT)
-            connect(R.id.end_button, TOP, R.id.lock_icon_view, TOP)
-            connect(R.id.end_button, BOTTOM, R.id.lock_icon_view, BOTTOM)
+            connect(R.id.end_button, LEFT, lockIconViewId, RIGHT)
+            connect(R.id.end_button, TOP, lockIconViewId, TOP)
+            connect(R.id.end_button, BOTTOM, lockIconViewId, BOTTOM)
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultAmbientIndicationAreaSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultAmbientIndicationAreaSection.kt
deleted file mode 100644
index 20cb9b0..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultAmbientIndicationAreaSection.kt
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.keyguard.ui.view.layout.sections
-
-import android.view.LayoutInflater
-import android.view.ViewGroup.LayoutParams.MATCH_PARENT
-import androidx.constraintlayout.widget.ConstraintLayout
-import androidx.constraintlayout.widget.ConstraintSet
-import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
-import androidx.constraintlayout.widget.ConstraintSet.END
-import androidx.constraintlayout.widget.ConstraintSet.MATCH_CONSTRAINT
-import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
-import androidx.constraintlayout.widget.ConstraintSet.START
-import androidx.constraintlayout.widget.ConstraintSet.TOP
-import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT
-import com.android.keyguard.KeyguardUpdateMonitor
-import com.android.systemui.Flags.keyguardBottomAreaRefactor
-import com.android.systemui.res.R
-import com.android.systemui.keyguard.shared.model.KeyguardSection
-import com.android.systemui.keyguard.ui.binder.KeyguardAmbientIndicationAreaViewBinder
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardAmbientIndicationViewModel
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
-import javax.inject.Inject
-
-class DefaultAmbientIndicationAreaSection
-@Inject
-constructor(
-    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
-    private val keyguardAmbientIndicationViewModel: KeyguardAmbientIndicationViewModel,
-    private val keyguardRootViewModel: KeyguardRootViewModel,
-) : KeyguardSection() {
-    private var ambientIndicationAreaHandle: KeyguardAmbientIndicationAreaViewBinder.Binding? = null
-
-    override fun addViews(constraintLayout: ConstraintLayout) {
-        if (keyguardBottomAreaRefactor()) {
-            val view =
-                LayoutInflater.from(constraintLayout.context)
-                    .inflate(R.layout.ambient_indication, constraintLayout, false)
-
-            constraintLayout.addView(view)
-        }
-    }
-
-    override fun bindData(constraintLayout: ConstraintLayout) {
-        if (keyguardBottomAreaRefactor()) {
-            ambientIndicationAreaHandle =
-                KeyguardAmbientIndicationAreaViewBinder.bind(
-                    constraintLayout,
-                    keyguardAmbientIndicationViewModel,
-                    keyguardRootViewModel,
-                )
-        }
-    }
-
-    override fun applyConstraints(constraintSet: ConstraintSet) {
-        constraintSet.apply {
-            constrainWidth(R.id.ambient_indication_container, MATCH_PARENT)
-
-            if (keyguardUpdateMonitor.isUdfpsSupported) {
-                // constrain below udfps and above indication area
-                constrainHeight(R.id.ambient_indication_container, MATCH_CONSTRAINT)
-                connect(R.id.ambient_indication_container, TOP, R.id.lock_icon_view, BOTTOM)
-                connect(
-                    R.id.ambient_indication_container,
-                    BOTTOM,
-                    R.id.keyguard_indication_area,
-                    TOP
-                )
-                connect(R.id.ambient_indication_container, START, PARENT_ID, START)
-                connect(R.id.ambient_indication_container, END, PARENT_ID, END)
-            } else {
-                // constrain above lock icon
-                constrainHeight(R.id.ambient_indication_container, WRAP_CONTENT)
-                connect(R.id.ambient_indication_container, BOTTOM, R.id.lock_icon_view, TOP)
-                connect(R.id.ambient_indication_container, START, PARENT_ID, START)
-                connect(R.id.ambient_indication_container, END, PARENT_ID, END)
-            }
-        }
-    }
-
-    override fun removeViews(constraintLayout: ConstraintLayout) {
-        ambientIndicationAreaHandle?.destroy()
-
-        constraintLayout.removeView(R.id.ambient_indication_container)
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardSectionsModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardSectionsModule.kt
new file mode 100644
index 0000000..37c00b6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardSectionsModule.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.keyguard.ui.view.layout.sections
+
+import com.android.systemui.keyguard.shared.model.KeyguardSection
+import dagger.BindsOptionalOf
+import dagger.Module
+import javax.inject.Named
+
+@Module
+abstract class KeyguardSectionsModule {
+
+    companion object {
+        const val KEYGUARD_AMBIENT_INDICATION_AREA_SECTION =
+                "keyguard_ambient_indication_area_section"
+    }
+
+    @BindsOptionalOf
+    @Named(KEYGUARD_AMBIENT_INDICATION_AREA_SECTION)
+    abstract fun defaultAmbientIndicationAreaSection(): KeyguardSection
+
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardAmbientIndicationViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardAmbientIndicationViewModel.kt
deleted file mode 100644
index dd3967a..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardAmbientIndicationViewModel.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.keyguard.ui.viewmodel
-
-import com.android.systemui.doze.util.BurnInHelperWrapper
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.map
-import javax.inject.Inject
-
-class KeyguardAmbientIndicationViewModel
-@Inject
-constructor(
-    private val keyguardInteractor: KeyguardInteractor,
-    private val burnInHelperWrapper: BurnInHelperWrapper,
-) {
-
-    /** An observable for the x-offset by which the indication area should be translated. */
-    val indicationAreaTranslationX: Flow<Float> =
-        keyguardInteractor.clockPosition.map { it.x.toFloat() }.distinctUntilChanged()
-
-    /** Returns an observable for the y-offset by which the indication area should be translated. */
-    fun indicationAreaTranslationY(defaultBurnInOffset: Int): Flow<Float> {
-        return keyguardInteractor.dozeAmount
-            .map { dozeAmount ->
-                dozeAmount *
-                    (burnInHelperWrapper.burnInOffset(
-                        /* amplitude = */ defaultBurnInOffset * 2,
-                        /* xAxis= */ false,
-                    ) - defaultBurnInOffset)
-            }
-            .distinctUntilChanged()
-    }
-}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegate.kt
index 8453af1..0f54e93 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionDialogDelegate.kt
@@ -80,12 +80,13 @@
                     R.string.media_projection_entry_app_permission_dialog_warning_entire_screen
                 }
 
+            val singleAppOptionDisabled =
+                appName != null &&
+                    mediaProjectionConfig?.regionToCapture ==
+                        MediaProjectionConfig.CAPTURE_REGION_FIXED_DISPLAY
+
             val singleAppDisabledText =
-                if (
-                    appName != null &&
-                        mediaProjectionConfig?.regionToCapture ==
-                            MediaProjectionConfig.CAPTURE_REGION_FIXED_DISPLAY
-                ) {
+                if (singleAppOptionDisabled) {
                     context.getString(
                         R.string.media_projection_entry_app_permission_dialog_single_app_disabled,
                         appName
@@ -93,19 +94,26 @@
                 } else {
                     null
                 }
-            return listOf(
-                ScreenShareOption(
-                    mode = SINGLE_APP,
-                    spinnerText = R.string.screen_share_permission_dialog_option_single_app,
-                    warningText = singleAppWarningText,
-                    spinnerDisabledText = singleAppDisabledText,
-                ),
-                ScreenShareOption(
-                    mode = ENTIRE_SCREEN,
-                    spinnerText = R.string.screen_share_permission_dialog_option_entire_screen,
-                    warningText = entireScreenWarningText
+            val options =
+                listOf(
+                    ScreenShareOption(
+                        mode = SINGLE_APP,
+                        spinnerText = R.string.screen_share_permission_dialog_option_single_app,
+                        warningText = singleAppWarningText,
+                        spinnerDisabledText = singleAppDisabledText,
+                    ),
+                    ScreenShareOption(
+                        mode = ENTIRE_SCREEN,
+                        spinnerText = R.string.screen_share_permission_dialog_option_entire_screen,
+                        warningText = entireScreenWarningText
+                    )
                 )
-            )
+            return if (singleAppOptionDisabled) {
+                // Make sure "Entire screen" is the first option when "Single App" is disabled.
+                options.reversed()
+            } else {
+                options
+            }
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
index ead10d6..9f4ea27 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
@@ -181,12 +181,16 @@
             if (mCancelTimeoutRunnable != null) {
                 mCancelTimeoutRunnable.run();
             }
-            finish();
+            requestFinish();
         }
 
         return super.onKeyDown(keyCode, event);
     }
 
+    protected void requestFinish() {
+        finish();
+    }
+
     private boolean triggeredByBrightnessKey() {
         return getIntent().getBooleanExtra(EXTRA_FROM_BRIGHTNESS_KEY, false);
     }
@@ -197,6 +201,6 @@
         }
         final int timeout = mAccessibilityMgr.getRecommendedTimeoutMillis(DIALOG_TIMEOUT_MILLIS,
                 AccessibilityManager.FLAG_CONTENT_CONTROLS);
-        mCancelTimeoutRunnable = mMainExecutor.executeDelayed(this::finish, timeout);
+        mCancelTimeoutRunnable = mMainExecutor.executeDelayed(this::requestFinish, timeout);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 5a007fc..7201b35 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -29,7 +29,6 @@
 import static com.android.systemui.classifier.Classifier.GENERIC;
 import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS;
 import static com.android.systemui.classifier.Classifier.UNLOCK;
-import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION;
 import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadScroll;
 import static com.android.systemui.navigationbar.gestural.Utilities.isTrackpadThreeFingerSwipe;
 import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_CLOSED;
@@ -41,7 +40,6 @@
 import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
 import static com.android.systemui.statusbar.StatusBarState.SHADE;
 import static com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED;
-import static com.android.systemui.statusbar.VibratorHelper.TOUCH_VIBRATION_ATTRIBUTES;
 import static com.android.systemui.statusbar.notification.stack.StackStateAnimator.ANIMATION_DURATION_FOLD_TO_AOD;
 import static com.android.systemui.util.DumpUtilsKt.asIndenting;
 import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow;
@@ -64,7 +62,6 @@
 import android.os.Bundle;
 import android.os.Handler;
 import android.os.PowerManager;
-import android.os.Process;
 import android.os.Trace;
 import android.os.UserManager;
 import android.os.VibrationEffect;
@@ -2841,16 +2838,7 @@
         }
 
         if (!mStatusBarStateController.isDozing()) {
-            if (mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
-                mVibratorHelper.performHapticFeedback(mView, HapticFeedbackConstants.REJECT);
-            } else {
-                mVibratorHelper.vibrate(
-                        Process.myUid(),
-                        mView.getContext().getPackageName(),
-                        ADDITIONAL_TAP_REQUIRED_VIBRATION_EFFECT,
-                        "falsing-additional-tap-required",
-                        TOUCH_VIBRATION_ATTRIBUTES);
-            }
+            mVibratorHelper.performHapticFeedback(mView, HapticFeedbackConstants.REJECT);
         }
     }
 
@@ -3678,14 +3666,10 @@
     private void maybeVibrateOnOpening(boolean openingWithTouch) {
         if (mVibrateOnOpening && mBarState != KEYGUARD && mBarState != SHADE_LOCKED) {
             if (!openingWithTouch || !mHasVibratedOnOpen) {
-                if (mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
-                    mVibratorHelper.performHapticFeedback(
-                            mView,
-                            HapticFeedbackConstants.GESTURE_START
-                    );
-                } else {
-                    mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK);
-                }
+                mVibratorHelper.performHapticFeedback(
+                        mView,
+                        HapticFeedbackConstants.GESTURE_START
+                );
                 mHasVibratedOnOpen = true;
                 mShadeLog.v("Vibrating on opening, mHasVibratedOnOpen=true");
             }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/OWNERS b/packages/SystemUI/src/com/android/systemui/shade/OWNERS
index c8b6a2e..bbcf10b 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/OWNERS
+++ b/packages/SystemUI/src/com/android/systemui/shade/OWNERS
@@ -6,8 +6,12 @@
 
 per-file NotificationsQuickSettingsContainer.java = kozynski@google.com, asc@google.com
 per-file NotificationsQSContainerController.kt = kozynski@google.com, asc@google.com
-per-file *ShadeHeader* = kozynski@google.com, asc@google.com
-per-file *Shade* = justinweir@google.com
+per-file *ShadeHeader* = syeonlee@google.com, kozynski@google.com, asc@google.com
+
+per-file *Interactor* = set noparent
+per-file *Interactor* = justinweir@google.com, syeonlee@google.com, nijamkin@google.com
+per-file *Repository* = set noparent
+per-file *Repository* = justinweir@google.com, syeonlee@google.com, nijamkin@google.com
 
 per-file NotificationShadeWindowViewController.java = pixel@google.com, cinek@google.com, juliacr@google.com
 per-file NotificationShadeWindowView.java = pixel@google.com, cinek@google.com, juliacr@google.com
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 14ec08f35..46488c6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -4668,22 +4668,6 @@
         return mClearAllInProgress;
     }
 
-    public boolean isFooterViewNotGone() {
-        return mFooterView != null
-                && mFooterView.getVisibility() != View.GONE
-                && !mFooterView.willBeGone();
-    }
-
-    public boolean isFooterViewContentVisible() {
-        return mFooterView != null && mFooterView.isContentVisible();
-    }
-
-    public int getFooterViewHeightWithPadding() {
-        return mFooterView == null ? 0 : mFooterView.getHeight()
-                + mPaddingBetweenElements
-                + mGapHeight;
-    }
-
     /**
      * @return the padding after the media header on the lockscreen
      */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 2cf0c26..3e140a4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -858,10 +858,6 @@
         return row.getVisibility() == View.VISIBLE;
     }
 
-    public boolean isViewAffectedBySwipe(ExpandableView expandableView) {
-        return mNotificationRoundnessManager.isViewAffectedBySwipe(expandableView);
-    }
-
     public void addOnExpandedHeightChangedListener(BiConsumer<Float, Float> listener) {
         mView.addOnExpandedHeightChangedListener(listener);
     }
@@ -1261,18 +1257,6 @@
         mView.setPanelFlinging(flinging);
     }
 
-    public boolean isFooterViewNotGone() {
-        return mView.isFooterViewNotGone();
-    }
-
-    public boolean isFooterViewContentVisible() {
-        return mView.isFooterViewContentVisible();
-    }
-
-    public int getFooterViewHeightWithPadding() {
-        return mView.getFooterViewHeightWithPadding();
-    }
-
     /**
      * Sets whether the bouncer is currently showing. Should only be called from
      * {@link CentralSurfaces}.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
index 818ba95..3304b98 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
@@ -71,6 +71,7 @@
 import com.android.systemui.statusbar.policy.bluetooth.BluetoothRepository;
 import com.android.systemui.statusbar.policy.bluetooth.BluetoothRepositoryImpl;
 import com.android.systemui.statusbar.policy.data.repository.DeviceProvisioningRepositoryModule;
+import com.android.systemui.statusbar.policy.data.repository.ZenModeRepositoryModule;
 
 import dagger.Binds;
 import dagger.Module;
@@ -81,7 +82,7 @@
 import javax.inject.Named;
 
 /** Dagger Module for code in the statusbar.policy package. */
-@Module(includes = { DeviceProvisioningRepositoryModule.class })
+@Module(includes = { DeviceProvisioningRepositoryModule.class, ZenModeRepositoryModule.class })
 public interface StatusBarPolicyModule {
 
     String DEVICE_STATE_ROTATION_LOCK_DEFAULTS = "DEVICE_STATE_ROTATION_LOCK_DEFAULTS";
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepository.kt
new file mode 100644
index 0000000..94ab58a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepository.kt
@@ -0,0 +1,77 @@
+/*
+ * 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.policy.data.repository
+
+import android.app.NotificationManager
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.statusbar.policy.ZenModeController
+import dagger.Binds
+import dagger.Module
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * A repository that holds information about the status and configuration of Zen Mode (or Do Not
+ * Disturb/DND Mode).
+ */
+interface ZenModeRepository {
+    val zenMode: Flow<Int>
+    val consolidatedNotificationPolicy: Flow<NotificationManager.Policy?>
+}
+
+class ZenModeRepositoryImpl
+@Inject
+constructor(
+    private val zenModeController: ZenModeController,
+) : ZenModeRepository {
+    // TODO(b/308591859): ZenModeController should use flows instead of callbacks. The
+    // conflatedCallbackFlows here should be replaced eventually, see:
+    // https://docs.google.com/document/d/1gAiuYupwUAFdbxkDXa29A4aFNu7XoCd7sCIk31WTnHU/edit?resourcekey=0-J4ZBiUhLhhQnNobAcI2vIw
+
+    override val zenMode: Flow<Int> = conflatedCallbackFlow {
+        val callback =
+            object : ZenModeController.Callback {
+                override fun onZenChanged(zen: Int) {
+                    trySend(zen)
+                }
+            }
+        zenModeController.addCallback(callback)
+        trySend(zenModeController.zen)
+
+        awaitClose { zenModeController.removeCallback(callback) }
+    }
+
+    override val consolidatedNotificationPolicy: Flow<NotificationManager.Policy?> =
+        conflatedCallbackFlow {
+            val callback =
+                object : ZenModeController.Callback {
+                    override fun onConsolidatedPolicyChanged(policy: NotificationManager.Policy?) {
+                        trySend(policy)
+                    }
+                }
+            zenModeController.addCallback(callback)
+            trySend(zenModeController.consolidatedPolicy)
+
+            awaitClose { zenModeController.removeCallback(callback) }
+        }
+}
+
+@Module
+interface ZenModeRepositoryModule {
+    @Binds fun bindImpl(impl: ZenModeRepositoryImpl): ZenModeRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
new file mode 100644
index 0000000..ae31851
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractor.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      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.policy.domain.interactor
+
+import android.provider.Settings
+import com.android.systemui.statusbar.policy.data.repository.ZenModeRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+
+/**
+ * An interactor that performs business logic related to the status and configuration of Zen Mode
+ * (or Do Not Disturb/DND Mode).
+ */
+class ZenModeInteractor @Inject constructor(repository: ZenModeRepository) {
+    val isZenModeEnabled: Flow<Boolean> =
+        repository.zenMode
+            .map {
+                when (it) {
+                    Settings.Global.ZEN_MODE_ALARMS -> true
+                    Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS -> true
+                    Settings.Global.ZEN_MODE_NO_INTERRUPTIONS -> true
+                    Settings.Global.ZEN_MODE_OFF -> false
+                    else -> false
+                }
+            }
+            .distinctUntilChanged()
+
+    val areNotificationsHiddenInShade: Flow<Boolean> =
+        combine(isZenModeEnabled, repository.consolidatedNotificationPolicy) { dndEnabled, policy ->
+                if (!dndEnabled) {
+                    false
+                } else {
+                    val showInNotificationList = policy?.showInNotificationList() ?: true
+                    !showInNotificationList
+                }
+            }
+            .distinctUntilChanged()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
index fc414b6..2f2c4b0 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt
@@ -39,7 +39,6 @@
 import com.android.app.animation.Interpolators
 import com.android.internal.widget.CachingIconView
 import com.android.systemui.Gefingerpoken
-import com.android.systemui.res.R
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
 import com.android.systemui.common.shared.model.Text.Companion.loadText
@@ -48,9 +47,8 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION
 import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.res.R
 import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.temporarydisplay.TemporaryViewDisplayController
@@ -96,7 +94,6 @@
     wakeLockBuilder: WakeLock.Builder,
     systemClock: SystemClock,
     tempViewUiEventLogger: TemporaryViewUiEventLogger,
-    private val featureFlags: FeatureFlags,
 ) :
     TemporaryViewDisplayController<ChipbarInfo, ChipbarLogger>(
         context,
@@ -234,18 +231,14 @@
         maybeGetAccessibilityFocus(newInfo, currentView)
 
         // ---- Haptics ----
-        if (featureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
-            vibratorHelper.performHapticFeedback(parent, newInfo.vibrationConstant)
-        } else {
-            newInfo.vibrationEffect?.let {
-                vibratorHelper.vibrate(
-                    Process.myUid(),
-                    context.getApplicationContext().getPackageName(),
-                    it,
-                    newInfo.windowTitle,
-                    VIBRATION_ATTRIBUTES,
-                )
-            }
+        newInfo.vibrationEffect?.let {
+            vibratorHelper.vibrate(
+                Process.myUid(),
+                context.getApplicationContext().getPackageName(),
+                it,
+                newInfo.windowTitle,
+                VIBRATION_ATTRIBUTES,
+            )
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt
index 4449f8d0..7475388 100644
--- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt
+++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarInfo.kt
@@ -17,13 +17,12 @@
 package com.android.systemui.temporarydisplay.chipbar
 
 import android.os.VibrationEffect
-import android.view.HapticFeedbackConstants
 import android.view.View
 import androidx.annotation.AttrRes
 import com.android.internal.logging.InstanceId
-import com.android.systemui.res.R
 import com.android.systemui.common.shared.model.Text
 import com.android.systemui.common.shared.model.TintedIcon
+import com.android.systemui.res.R
 import com.android.systemui.temporarydisplay.TemporaryViewInfo
 import com.android.systemui.temporarydisplay.ViewPriority
 
@@ -43,7 +42,6 @@
     val text: Text,
     val endItem: ChipbarEndItem?,
     val vibrationEffect: VibrationEffect? = null,
-    val vibrationConstant: Int = HapticFeedbackConstants.NO_HAPTICS,
     val allowSwipeToDismiss: Boolean = false,
     override val windowTitle: String,
     override val wakeReason: String,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt
index d2b81e0..00ea78f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt
@@ -17,14 +17,14 @@
 package com.android.systemui.biometrics
 
 import androidx.test.filters.SmallTest
-import com.android.SysUITestComponent
-import com.android.SysUITestModule
-import com.android.runCurrent
-import com.android.runTest
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.flags.FakeFeatureFlagsClassicModule
 import com.android.systemui.flags.Flags
+import com.android.systemui.runCurrent
+import com.android.systemui.runTest
 import com.android.systemui.shade.data.repository.FakeShadeRepository
 import com.android.systemui.user.domain.UserDomainLayerModule
 import dagger.BindsInstance
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt
index b439fcf..722c11d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt
@@ -18,9 +18,9 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.SysUITestModule
-import com.android.TestMocksModule
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.TestMocksModule
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.data.repository.FakeKeyguardSurfaceBehindRepository
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/InWindowLauncherUnlockAnimationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/InWindowLauncherUnlockAnimationInteractorTest.kt
index 7fb0dd5..3d094fd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/InWindowLauncherUnlockAnimationInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/InWindowLauncherUnlockAnimationInteractorTest.kt
@@ -18,9 +18,9 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.SysUITestModule
-import com.android.TestMocksModule
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.TestMocksModule
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.data.repository.FakeKeyguardSurfaceBehindRepository
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/InWindowLauncherUnlockAnimationManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/InWindowLauncherUnlockAnimationManagerTest.kt
index 570dfb3..e9399cc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/InWindowLauncherUnlockAnimationManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/InWindowLauncherUnlockAnimationManagerTest.kt
@@ -18,7 +18,7 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.SysUITestModule
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprintTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprintTest.kt
index 43d70ad..76c2589 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprintTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprintTest.kt
@@ -30,7 +30,6 @@
 import com.android.systemui.keyguard.ui.view.layout.items.ClockSection
 import com.android.systemui.keyguard.ui.view.layout.sections.AodBurnInSection
 import com.android.systemui.keyguard.ui.view.layout.sections.AodNotificationIconsSection
-import com.android.systemui.keyguard.ui.view.layout.sections.DefaultAmbientIndicationAreaSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultDeviceEntryIconSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultIndicationAreaSection
 import com.android.systemui.keyguard.ui.view.layout.sections.DefaultNotificationStackScrollLayoutSection
@@ -49,6 +48,7 @@
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
+import java.util.Optional
 
 @RunWith(AndroidTestingRunner::class)
 @RunWithLooper(setAsMainLooper = true)
@@ -60,7 +60,7 @@
     @Mock private lateinit var mDefaultDeviceEntryIconSection: DefaultDeviceEntryIconSection
     @Mock private lateinit var defaultShortcutsSection: DefaultShortcutsSection
     @Mock
-    private lateinit var defaultAmbientIndicationAreaSection: DefaultAmbientIndicationAreaSection
+    private lateinit var defaultAmbientIndicationAreaSection: Optional<KeyguardSection>
     @Mock private lateinit var defaultSettingsPopupMenuSection: DefaultSettingsPopupMenuSection
     @Mock private lateinit var defaultStatusViewSection: DefaultStatusViewSection
     @Mock private lateinit var defaultStatusBarViewSection: DefaultStatusBarSection
@@ -98,7 +98,7 @@
     fun replaceViews() {
         val constraintLayout = ConstraintLayout(context, null)
         underTest.replaceViews(null, constraintLayout)
-        underTest.sections.forEach { verify(it).addViews(constraintLayout) }
+        underTest.sections.forEach { verify(it)?.addViews(constraintLayout) }
     }
 
     @Test
@@ -110,7 +110,7 @@
         val constraintLayout = ConstraintLayout(context, null)
         underTest.replaceViews(prevBlueprint, constraintLayout)
         underTest.sections.minus(mDefaultDeviceEntryIconSection).forEach {
-            verify(it, never()).addViews(constraintLayout)
+            verify(it, never())?.addViews(constraintLayout)
         }
 
         verify(mDefaultDeviceEntryIconSection).addViews(constraintLayout)
@@ -121,6 +121,6 @@
     fun applyConstraints() {
         val cs = ConstraintSet()
         underTest.applyConstraints(cs)
-        underTest.sections.forEach { verify(it).applyConstraints(cs) }
+        underTest.sections.forEach { verify(it)?.applyConstraints(cs) }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
index 259c74f..d3019f1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt
@@ -21,18 +21,16 @@
 
 import android.view.View
 import androidx.test.filters.SmallTest
-import com.android.SysUITestComponent
-import com.android.SysUITestModule
-import com.android.TestMocksModule
-import com.android.collectLastValue
-import com.android.runCurrent
-import com.android.runTest
+import com.android.systemui.Flags as AConfigFlags
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.TestMocksModule
+import com.android.systemui.collectLastValue
 import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository
-import com.android.systemui.Flags as AConfigFlags
 import com.android.systemui.flags.FakeFeatureFlagsClassic
 import com.android.systemui.flags.FakeFeatureFlagsClassicModule
 import com.android.systemui.flags.Flags
@@ -46,6 +44,8 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.plugins.ClockController
+import com.android.systemui.runCurrent
+import com.android.systemui.runTest
 import com.android.systemui.statusbar.notification.data.repository.FakeNotificationsKeyguardViewStateRepository
 import com.android.systemui.statusbar.phone.DozeParameters
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController
@@ -64,7 +64,6 @@
 import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.test.StandardTestDispatcher
 import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
@@ -109,10 +108,7 @@
 
         mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
 
-        val featureFlags =
-            FakeFeatureFlagsClassic().apply {
-                set(Flags.FACE_AUTH_REFACTOR, true)
-            }
+        val featureFlags = FakeFeatureFlagsClassic().apply { set(Flags.FACE_AUTH_REFACTOR, true) }
 
         val withDeps = KeyguardInteractorFactory.create(featureFlags = featureFlags)
         keyguardInteractor = withDeps.keyguardInteractor
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModelTest.kt
index 4074851..c50be04 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToAodTransitionViewModelTest.kt
@@ -18,15 +18,13 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.SysUITestComponent
-import com.android.SysUITestModule
-import com.android.TestMocksModule
-import com.android.collectLastValue
-import com.android.collectValues
-import com.android.runCurrent
-import com.android.runTest
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.TestMocksModule
 import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository
+import com.android.systemui.collectLastValue
+import com.android.systemui.collectValues
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository
 import com.android.systemui.flags.FakeFeatureFlagsClassicModule
@@ -39,6 +37,8 @@
 import com.android.systemui.keyguard.shared.model.StatusBarState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.runCurrent
+import com.android.systemui.runTest
 import com.android.systemui.shade.data.repository.FakeShadeRepository
 import com.android.systemui.user.domain.UserDomainLayerModule
 import com.google.common.collect.Range
@@ -77,15 +77,15 @@
                 mocks: TestMocksModule,
             ): TestComponent
         }
+    }
 
-        fun shadeExpanded(expanded: Boolean) {
-            if (expanded) {
-                shadeRepository.setQsExpansion(1f)
-            } else {
-                keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
-                shadeRepository.setQsExpansion(0f)
-                shadeRepository.setLockscreenShadeExpansion(0f)
-            }
+    private fun TestComponent.shadeExpanded(expanded: Boolean) {
+        if (expanded) {
+            shadeRepository.setQsExpansion(1f)
+        } else {
+            keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
+            shadeRepository.setQsExpansion(0f)
+            shadeRepository.setLockscreenShadeExpansion(0f)
         }
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
index 5c85357..26704da 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
@@ -18,14 +18,12 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.SysUITestComponent
-import com.android.SysUITestModule
-import com.android.TestMocksModule
-import com.android.collectLastValue
-import com.android.collectValues
-import com.android.runCurrent
-import com.android.runTest
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.TestMocksModule
+import com.android.systemui.collectLastValue
+import com.android.systemui.collectValues
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.flags.FakeFeatureFlagsClassicModule
 import com.android.systemui.flags.Flags
@@ -35,13 +33,14 @@
 import com.android.systemui.keyguard.shared.model.StatusBarState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.runCurrent
+import com.android.systemui.runTest
 import com.android.systemui.shade.data.repository.FakeShadeRepository
 import com.android.systemui.user.domain.UserDomainLayerModule
 import com.google.common.collect.Range
 import com.google.common.truth.Truth.assertThat
 import dagger.BindsInstance
 import dagger.Component
-import kotlinx.coroutines.test.runTest
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -69,15 +68,15 @@
                 mocks: TestMocksModule,
             ): TestComponent
         }
+    }
 
-        fun shadeExpanded(expanded: Boolean) {
-            if (expanded) {
-                shadeRepository.setQsExpansion(1f)
-            } else {
-                keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
-                shadeRepository.setQsExpansion(0f)
-                shadeRepository.setLockscreenShadeExpansion(0f)
-            }
+    private fun TestComponent.shadeExpanded(expanded: Boolean) {
+        if (expanded) {
+            shadeRepository.setQsExpansion(1f)
+        } else {
+            keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
+            shadeRepository.setQsExpansion(0f)
+            shadeRepository.setLockscreenShadeExpansion(0f)
         }
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
index 4cbefa3d..ff3135a6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
@@ -18,14 +18,12 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.SysUITestComponent
-import com.android.SysUITestModule
-import com.android.TestMocksModule
-import com.android.collectLastValue
-import com.android.collectValues
-import com.android.runCurrent
-import com.android.runTest
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.TestMocksModule
+import com.android.systemui.collectLastValue
+import com.android.systemui.collectValues
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.flags.FakeFeatureFlagsClassicModule
 import com.android.systemui.flags.Flags
@@ -35,6 +33,8 @@
 import com.android.systemui.keyguard.shared.model.StatusBarState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.runCurrent
+import com.android.systemui.runTest
 import com.android.systemui.shade.data.repository.FakeShadeRepository
 import com.android.systemui.user.domain.UserDomainLayerModule
 import com.google.common.collect.Range
@@ -68,15 +68,15 @@
                 mocks: TestMocksModule,
             ): TestComponent
         }
+    }
 
-        fun shadeExpanded(expanded: Boolean) {
-            if (expanded) {
-                shadeRepository.setQsExpansion(1f)
-            } else {
-                keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
-                shadeRepository.setQsExpansion(0f)
-                shadeRepository.setLockscreenShadeExpansion(0f)
-            }
+    private fun TestComponent.shadeExpanded(expanded: Boolean) {
+        if (expanded) {
+            shadeRepository.setQsExpansion(1f)
+        } else {
+            keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
+            shadeRepository.setQsExpansion(0f)
+            shadeRepository.setLockscreenShadeExpansion(0f)
         }
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt
index 4f56435..8afd8e4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToPrimaryBouncerTransitionViewModelTest.kt
@@ -18,13 +18,11 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.SysUITestComponent
-import com.android.SysUITestModule
-import com.android.TestMocksModule
-import com.android.collectLastValue
-import com.android.runCurrent
-import com.android.runTest
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.TestMocksModule
+import com.android.systemui.collectLastValue
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.flags.FakeFeatureFlagsClassicModule
 import com.android.systemui.flags.Flags
@@ -34,6 +32,8 @@
 import com.android.systemui.keyguard.shared.model.StatusBarState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.runCurrent
+import com.android.systemui.runTest
 import com.android.systemui.shade.data.repository.FakeShadeRepository
 import com.android.systemui.user.domain.UserDomainLayerModule
 import com.google.common.collect.Range
@@ -69,15 +69,15 @@
                 mocks: TestMocksModule,
             ): TestComponent
         }
+    }
 
-        fun shadeExpanded(expanded: Boolean) {
-            if (expanded) {
-                shadeRepository.setQsExpansion(1f)
-            } else {
-                keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
-                shadeRepository.setQsExpansion(0f)
-                shadeRepository.setLockscreenShadeExpansion(0f)
-            }
+    private fun TestComponent.shadeExpanded(expanded: Boolean) {
+        if (expanded) {
+            shadeRepository.setQsExpansion(1f)
+        } else {
+            keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD)
+            shadeRepository.setQsExpansion(0f)
+            shadeRepository.setLockscreenShadeExpansion(0f)
         }
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
index 83145bd..3be50b1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttSenderCoordinatorTest.kt
@@ -35,15 +35,13 @@
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.testing.UiEventLoggerFake
 import com.android.internal.statusbar.IUndoMediaTransferCallback
-import com.android.systemui.res.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.common.shared.model.Text.Companion.loadText
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION
 import com.android.systemui.media.taptotransfer.MediaTttFlags
 import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.res.R
 import com.android.systemui.statusbar.CommandQueue
 import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.policy.ConfigurationController
@@ -113,7 +111,6 @@
     private lateinit var uiEventLogger: MediaTttSenderUiEventLogger
     private lateinit var tempViewUiEventLogger: TemporaryViewUiEventLogger
     private val defaultTimeout = context.resources.getInteger(R.integer.heads_up_notification_decay)
-    private val featureFlags = FakeFeatureFlags()
 
     @Before
     fun setUp() {
@@ -163,9 +160,7 @@
                 fakeWakeLockBuilder,
                 fakeClock,
                 tempViewUiEventLogger,
-                featureFlags
             )
-        featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false)
         chipbarCoordinator.start()
 
         underTest =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt
index 152be65..6e487cd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt
@@ -25,9 +25,9 @@
 import android.view.WindowManagerPolicyConstants.EXTRA_FROM_BRIGHTNESS_KEY
 import androidx.test.filters.SmallTest
 import androidx.test.rule.ActivityTestRule
-import com.android.systemui.res.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.activity.SingleActivityFactory
+import com.android.systemui.res.R
 import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
 import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.concurrency.FakeExecutor
@@ -182,5 +182,15 @@
             brightnessControllerFactory,
             mainExecutor,
             accessibilityMgr
-        )
+        ) {
+        private var finishing = false
+
+        override fun isFinishing(): Boolean {
+            return finishing
+        }
+
+        override fun requestFinish() {
+            finishing = true
+        }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index 0f90007..f1429b5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -374,7 +374,6 @@
         mFeatureFlags.set(Flags.TRACKPAD_GESTURE_FEATURES, false);
         mFeatureFlags.set(Flags.LOCKSCREEN_ENABLE_LANDSCAPE, false);
         mFeatureFlags.set(Flags.QS_USER_DETAIL_SHORTCUT, false);
-        mFeatureFlags.set(Flags.ONE_WAY_HAPTICS_API_MIGRATION, false);
         mFeatureFlags.set(Flags.MIGRATE_CLOCKS_TO_BLUEPRINT, false);
 
         mSetFlagsRule.disableFlags(KeyguardShadeMigrationNssl.FLAG_NAME);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
index 7ad84d6..4df7ef5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerWithCoroutinesTest.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.shade
 
-import android.os.VibrationEffect
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.view.HapticFeedbackConstants
@@ -26,13 +25,10 @@
 import com.android.internal.util.CollectionUtils
 import com.android.keyguard.KeyguardClockSwitch.LARGE
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.StatusBarState.KEYGUARD
 import com.android.systemui.statusbar.StatusBarState.SHADE
 import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED
-import com.android.systemui.statusbar.VibratorHelper
-import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
@@ -62,9 +58,6 @@
 
     override fun getMainDispatcher() = Dispatchers.Main.immediate
 
-    private val ADDITIONAL_TAP_REQUIRED_VIBRATION_EFFECT =
-        VibrationEffect.get(VibrationEffect.EFFECT_STRENGTH_MEDIUM, false)
-
     @Test
     fun testDisableUserSwitcherAfterEnabling_returnsViewStubToTheViewHierarchy() = runTest {
         launch(Dispatchers.Main.immediate) { givenViewAttached() }
@@ -158,31 +151,8 @@
     }
 
     @Test
-    fun doubleTapRequired_onKeyguard_oneWayHapticsDisabled_usesOldVibrate() = runTest {
+    fun doubleTapRequired_onKeyguard_usesPerformHapticFeedback() = runTest {
         launch(Dispatchers.Main.immediate) {
-            mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false)
-            val listener = getFalsingTapListener()
-            mStatusBarStateController.setState(KEYGUARD)
-
-            listener.onAdditionalTapRequired()
-            val packageName = mView.context.packageName
-            verify(mKeyguardIndicationController).showTransientIndication(anyInt())
-            verify(mVibratorHelper)
-                .vibrate(
-                    any(),
-                    eq(packageName),
-                    eq(ADDITIONAL_TAP_REQUIRED_VIBRATION_EFFECT),
-                    eq("falsing-additional-tap-required"),
-                    eq(VibratorHelper.TOUCH_VIBRATION_ATTRIBUTES)
-                )
-        }
-        advanceUntilIdle()
-    }
-
-    @Test
-    fun doubleTapRequired_onKeyguard_oneWayHapticsEnabled_usesPerformHapticFeedback() = runTest {
-        launch(Dispatchers.Main.immediate) {
-            mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true)
             val listener = getFalsingTapListener()
             mStatusBarStateController.setState(KEYGUARD)
 
@@ -208,32 +178,8 @@
     }
 
     @Test
-    fun doubleTapRequired_shadeLocked_oneWayHapticsDisabled_usesOldVibrate() = runTest {
+    fun doubleTapRequired_shadeLocked_usesPerformHapticFeedback() = runTest {
         launch(Dispatchers.Main.immediate) {
-            mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false)
-            val listener = getFalsingTapListener()
-            val packageName = mView.context.packageName
-            mStatusBarStateController.setState(SHADE_LOCKED)
-
-            listener.onAdditionalTapRequired()
-            verify(mVibratorHelper)
-                .vibrate(
-                    any(),
-                    eq(packageName),
-                    eq(ADDITIONAL_TAP_REQUIRED_VIBRATION_EFFECT),
-                    eq("falsing-additional-tap-required"),
-                    eq(VibratorHelper.TOUCH_VIBRATION_ATTRIBUTES)
-                )
-
-            verify(mTapAgainViewController).show()
-        }
-        advanceUntilIdle()
-    }
-
-    @Test
-    fun doubleTapRequired_shadeLocked_oneWayHapticsEnabled_usesPerformHapticFeedback() = runTest {
-        launch(Dispatchers.Main.immediate) {
-            mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true)
             val listener = getFalsingTapListener()
             mStatusBarStateController.setState(SHADE_LOCKED)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt
index ff7443f..32daccb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt
@@ -22,13 +22,11 @@
 import android.content.pm.UserInfo
 import android.os.UserManager
 import androidx.test.filters.SmallTest
-import com.android.SysUITestComponent
-import com.android.SysUITestModule
-import com.android.TestMocksModule
-import com.android.collectLastValue
-import com.android.runCurrent
-import com.android.runTest
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.TestMocksModule
+import com.android.systemui.collectLastValue
 import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.flags.FakeFeatureFlagsClassicModule
@@ -45,6 +43,8 @@
 import com.android.systemui.power.shared.model.WakeSleepReason
 import com.android.systemui.power.shared.model.WakefulnessState
 import com.android.systemui.res.R
+import com.android.systemui.runCurrent
+import com.android.systemui.runTest
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.model.ObservableTransitionState
 import com.android.systemui.scene.shared.model.SceneKey
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
index 970a0f7..fdbdc79 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
@@ -5,8 +5,8 @@
 import android.testing.TestableLooper
 import android.testing.TestableLooper.RunWithLooper
 import androidx.test.filters.SmallTest
-import com.android.SysUITestModule
-import com.android.TestMocksModule
+import com.android.systemui.SysUITestModule
+import com.android.systemui.TestMocksModule
 import com.android.systemui.ExpandHelper
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingCollectorFake
@@ -607,9 +607,9 @@
         @Component.Factory
         interface Factory {
             fun create(
-                @BindsInstance test: SysuiTestCase,
-                featureFlags: FakeFeatureFlagsClassicModule,
-                mocks: TestMocksModule,
+                    @BindsInstance test: SysuiTestCase,
+                    featureFlags: FakeFeatureFlagsClassicModule,
+                    mocks: TestMocksModule,
             ): TestComponent
         }
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryTest.kt
index d479937..f3094cd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryTest.kt
@@ -17,13 +17,13 @@
 package com.android.systemui.statusbar.notification.data.repository
 
 import androidx.test.filters.SmallTest
-import com.android.SysUITestComponent
-import com.android.SysUITestModule
-import com.android.collectLastValue
-import com.android.runCurrent
-import com.android.runTest
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.collectLastValue
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.runCurrent
+import com.android.systemui.runTest
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.mockito.withArgCaptor
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractorTest.kt
index 707026e..b775079 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractorTest.kt
@@ -16,8 +16,8 @@
 
 import android.app.StatusBarManager
 import androidx.test.filters.SmallTest
-import com.android.SysUITestComponent
-import com.android.SysUITestModule
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.statusbar.disableflags.data.model.DisableFlagsModel
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractorTest.kt
index bb6f1b6..bb3113a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractorTest.kt
@@ -14,20 +14,17 @@
 package com.android.systemui.statusbar.notification.domain.interactor
 
 import androidx.test.filters.SmallTest
-import com.android.SysUITestComponent
-import com.android.SysUITestModule
-import com.android.collectLastValue
-import com.android.runCurrent
-import com.android.runTest
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.collectLastValue
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.runCurrent
+import com.android.systemui.runTest
 import com.android.systemui.statusbar.notification.data.repository.FakeNotificationsKeyguardViewStateRepository
 import com.google.common.truth.Truth.assertThat
 import dagger.BindsInstance
 import dagger.Component
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
 import org.junit.Test
 
 @SmallTest
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt
index 05deb1c..0341035 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/domain/interactor/NotificationIconsInteractorTest.kt
@@ -17,14 +17,14 @@
 
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
-import com.android.SysUITestComponent
-import com.android.SysUITestModule
-import com.android.TestMocksModule
-import com.android.collectLastValue
-import com.android.runTest
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.TestMocksModule
+import com.android.systemui.collectLastValue
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository
+import com.android.systemui.runTest
 import com.android.systemui.statusbar.data.repository.NotificationListenerSettingsRepository
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt
index c2c33de..68761ef 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt
@@ -18,14 +18,12 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.SysUITestComponent
-import com.android.SysUITestModule
-import com.android.TestMocksModule
-import com.android.collectLastValue
-import com.android.runCurrent
-import com.android.runTest
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.TestMocksModule
 import com.android.systemui.biometrics.domain.BiometricsDomainLayerModule
+import com.android.systemui.collectLastValue
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.flags.FakeFeatureFlagsClassicModule
 import com.android.systemui.flags.Flags
@@ -39,6 +37,8 @@
 import com.android.systemui.power.data.repository.FakePowerRepository
 import com.android.systemui.power.shared.model.WakeSleepReason
 import com.android.systemui.power.shared.model.WakefulnessState
+import com.android.systemui.runCurrent
+import com.android.systemui.runTest
 import com.android.systemui.statusbar.phone.DozeParameters
 import com.android.systemui.statusbar.phone.ScreenOffAnimationController
 import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepository
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt
index 87e9735..c2a1519 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt
@@ -20,14 +20,12 @@
 import android.graphics.drawable.Icon
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.SysUITestComponent
-import com.android.SysUITestModule
-import com.android.TestMocksModule
-import com.android.collectLastValue
-import com.android.runCurrent
-import com.android.runTest
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.TestMocksModule
 import com.android.systemui.biometrics.domain.BiometricsDomainLayerModule
+import com.android.systemui.collectLastValue
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.flags.FakeFeatureFlagsClassicModule
 import com.android.systemui.flags.Flags
@@ -42,6 +40,8 @@
 import com.android.systemui.power.data.repository.FakePowerRepository
 import com.android.systemui.power.shared.model.WakeSleepReason
 import com.android.systemui.power.shared.model.WakefulnessState
+import com.android.systemui.runCurrent
+import com.android.systemui.runTest
 import com.android.systemui.shade.data.repository.FakeShadeRepository
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt
index 7423c2d..917569c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/shelf/ui/viewmodel/NotificationShelfViewModelTest.kt
@@ -19,16 +19,16 @@
 import android.os.PowerManager
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
-import com.android.SysUITestComponent
-import com.android.SysUITestModule
-import com.android.TestMocksModule
-import com.android.collectLastValue
-import com.android.runTest
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.TestMocksModule
+import com.android.systemui.collectLastValue
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.power.data.repository.FakePowerRepository
+import com.android.systemui.runTest
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
 import com.android.systemui.statusbar.SysuiStatusBarStateController
 import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModelModule
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index db8f217..9c70c82 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -19,13 +19,11 @@
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
-import com.android.SysUITestComponent
-import com.android.SysUITestModule
-import com.android.TestMocksModule
-import com.android.collectLastValue
-import com.android.runCurrent
-import com.android.runTest
+import com.android.systemui.SysUITestComponent
+import com.android.systemui.SysUITestModule
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.TestMocksModule
+import com.android.systemui.collectLastValue
 import com.android.systemui.common.shared.model.SharedNotificationContainerPosition
 import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
 import com.android.systemui.dagger.SysUISingleton
@@ -39,6 +37,8 @@
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.res.R
+import com.android.systemui.runCurrent
+import com.android.systemui.runTest
 import com.android.systemui.shade.data.repository.FakeShadeRepository
 import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
 import com.android.systemui.user.domain.UserDomainLayerModule
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryImplTest.kt
new file mode 100644
index 0000000..004f679
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/data/repository/ZenModeRepositoryImplTest.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.statusbar.policy.data.repository
+
+import android.app.NotificationManager
+import android.provider.Settings
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.statusbar.policy.ZenModeController
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.mockito.withArgCaptor
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ZenModeRepositoryImplTest : SysuiTestCase() {
+    @Mock lateinit var zenModeController: ZenModeController
+
+    lateinit var underTest: ZenModeRepositoryImpl
+
+    private val testPolicy = NotificationManager.Policy(0, 1, 0)
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        underTest = ZenModeRepositoryImpl(zenModeController)
+    }
+
+    @Test
+    fun zenMode_reflectsCurrentControllerState() = runTest {
+        whenever(zenModeController.zen).thenReturn(Settings.Global.ZEN_MODE_NO_INTERRUPTIONS)
+        val zenMode by collectLastValue(underTest.zenMode)
+        assertThat(zenMode).isEqualTo(Settings.Global.ZEN_MODE_NO_INTERRUPTIONS)
+    }
+
+    @Test
+    fun zenMode_updatesWhenControllerStateChanges() = runTest {
+        val zenMode by collectLastValue(underTest.zenMode)
+        runCurrent()
+        whenever(zenModeController.zen).thenReturn(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
+        withArgCaptor { Mockito.verify(zenModeController).addCallback(capture()) }
+            .onZenChanged(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
+        assertThat(zenMode).isEqualTo(Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS)
+    }
+
+    @Test
+    fun policy_reflectsCurrentControllerState() {
+        runTest {
+            whenever(zenModeController.consolidatedPolicy).thenReturn(testPolicy)
+            val policy by collectLastValue(underTest.consolidatedNotificationPolicy)
+            assertThat(policy).isEqualTo(testPolicy)
+        }
+    }
+
+    @Test
+    fun policy_updatesWhenControllerStateChanges() = runTest {
+        val policy by collectLastValue(underTest.consolidatedNotificationPolicy)
+        runCurrent()
+        whenever(zenModeController.consolidatedPolicy).thenReturn(testPolicy)
+        withArgCaptor { Mockito.verify(zenModeController).addCallback(capture()) }
+            .onConsolidatedPolicyChanged(testPolicy)
+        assertThat(policy).isEqualTo(testPolicy)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
new file mode 100644
index 0000000..78e7971
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
@@ -0,0 +1,172 @@
+/*
+ * 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.policy.domain.interactor
+
+import android.app.NotificationManager.Policy
+import android.provider.Settings
+import androidx.test.filters.SmallTest
+import com.android.SysUITestComponent
+import com.android.SysUITestModule
+import com.android.collectLastValue
+import com.android.runCurrent
+import com.android.runTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.policy.data.repository.FakeZenModeRepository
+import com.android.systemui.user.domain.UserDomainLayerModule
+import com.google.common.truth.Truth.assertThat
+import dagger.BindsInstance
+import dagger.Component
+import org.junit.Test
+
+@SmallTest
+class ZenModeInteractorTest : SysuiTestCase() {
+    @SysUISingleton
+    @Component(
+        modules =
+            [
+                SysUITestModule::class,
+                UserDomainLayerModule::class,
+            ]
+    )
+    interface TestComponent : SysUITestComponent<ZenModeInteractor> {
+
+        val repository: FakeZenModeRepository
+
+        @Component.Factory
+        interface Factory {
+            fun create(@BindsInstance test: SysuiTestCase): TestComponent
+        }
+    }
+
+    private val testComponent: TestComponent =
+        DaggerZenModeInteractorTest_TestComponent.factory().create(test = this)
+
+    @Test
+    fun testIsZenModeEnabled_off() =
+        testComponent.runTest {
+            val enabled by collectLastValue(underTest.isZenModeEnabled)
+
+            repository.zenMode.value = Settings.Global.ZEN_MODE_OFF
+            runCurrent()
+
+            assertThat(enabled).isFalse()
+        }
+
+    @Test
+    fun testIsZenModeEnabled_alarms() =
+        testComponent.runTest {
+            val enabled by collectLastValue(underTest.isZenModeEnabled)
+
+            repository.zenMode.value = Settings.Global.ZEN_MODE_ALARMS
+            runCurrent()
+
+            assertThat(enabled).isTrue()
+        }
+
+    @Test
+    fun testIsZenModeEnabled_importantInterruptions() =
+        testComponent.runTest {
+            val enabled by collectLastValue(underTest.isZenModeEnabled)
+
+            repository.zenMode.value = Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
+            runCurrent()
+
+            assertThat(enabled).isTrue()
+        }
+
+    @Test
+    fun testIsZenModeEnabled_noInterruptions() =
+        testComponent.runTest {
+            val enabled by collectLastValue(underTest.isZenModeEnabled)
+
+            repository.zenMode.value = Settings.Global.ZEN_MODE_NO_INTERRUPTIONS
+            runCurrent()
+
+            assertThat(enabled).isTrue()
+        }
+
+    @Test
+    fun testIsZenModeEnabled_unknown() =
+        testComponent.runTest {
+            val enabled by collectLastValue(underTest.isZenModeEnabled)
+
+            repository.zenMode.value = 4 // this should fail if we ever add another zen mode type
+            runCurrent()
+
+            assertThat(enabled).isFalse()
+        }
+
+    @Test
+    fun testAreNotificationsHiddenInShade_noPolicy() =
+        testComponent.runTest {
+            val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
+
+            repository.consolidatedNotificationPolicy.value = null
+            repository.zenMode.value = Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
+            runCurrent()
+
+            assertThat(hidden).isFalse()
+        }
+
+    @Test
+    fun testAreNotificationsHiddenInShade_zenOffShadeSuppressed() =
+        testComponent.runTest {
+            val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
+
+            repository.consolidatedNotificationPolicy.value =
+                policyWithSuppressedVisualEffects(Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST)
+            repository.zenMode.value = Settings.Global.ZEN_MODE_OFF
+            runCurrent()
+
+            assertThat(hidden).isFalse()
+        }
+
+    @Test
+    fun testAreNotificationsHiddenInShade_zenOnShadeNotSuppressed() =
+        testComponent.runTest {
+            val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
+
+            repository.consolidatedNotificationPolicy.value =
+                policyWithSuppressedVisualEffects(Policy.SUPPRESSED_EFFECT_STATUS_BAR)
+            repository.zenMode.value = Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
+            runCurrent()
+
+            assertThat(hidden).isFalse()
+        }
+
+    @Test
+    fun testAreNotificationsHiddenInShade_zenOnShadeSuppressed() =
+        testComponent.runTest {
+            val hidden by collectLastValue(underTest.areNotificationsHiddenInShade)
+
+            repository.consolidatedNotificationPolicy.value =
+                policyWithSuppressedVisualEffects(Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST)
+            repository.zenMode.value = Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS
+            runCurrent()
+
+            assertThat(hidden).isTrue()
+        }
+}
+
+fun policyWithSuppressedVisualEffects(suppressedVisualEffects: Int) =
+    Policy(
+        /* priorityCategories = */ 0,
+        /* priorityCallSenders = */ 0,
+        /* priorityMessageSenders = */ 0,
+        /* suppressedVisualEffects = */ suppressedVisualEffects
+    )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
index 1e7e184..1466d24 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt
@@ -21,7 +21,6 @@
 import android.os.VibrationEffect
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
-import android.view.HapticFeedbackConstants
 import android.view.MotionEvent
 import android.view.View
 import android.view.ViewGroup
@@ -33,7 +32,6 @@
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.InstanceId
 import com.android.internal.logging.testing.UiEventLoggerFake
-import com.android.systemui.res.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.common.shared.model.ContentDescription
@@ -42,9 +40,8 @@
 import com.android.systemui.common.shared.model.Text
 import com.android.systemui.common.shared.model.TintedIcon
 import com.android.systemui.dump.DumpManager
-import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION
 import com.android.systemui.plugins.FalsingManager
+import com.android.systemui.res.R
 import com.android.systemui.statusbar.VibratorHelper
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.temporarydisplay.TemporaryViewUiEvent
@@ -94,7 +91,6 @@
     private lateinit var fakeExecutor: FakeExecutor
     private lateinit var uiEventLoggerFake: UiEventLoggerFake
     private lateinit var uiEventLogger: TemporaryViewUiEventLogger
-    private val featureFlags = FakeFeatureFlags()
 
     @Before
     fun setUp() {
@@ -131,10 +127,8 @@
                 fakeWakeLockBuilder,
                 fakeClock,
                 uiEventLogger,
-                featureFlags
             )
         underTest.start()
-        featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false)
     }
 
     @Test
@@ -494,23 +488,6 @@
             )
     }
 
-    @Test
-    fun displayView_oneWayHapticsEnabled_usesPerformHapticFeedback() {
-        featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true)
-        val constant: Int = HapticFeedbackConstants.CONFIRM
-        underTest.displayView(
-            createChipbarInfo(
-                Icon.Resource(R.id.check_box, null),
-                Text.Loaded("text"),
-                endItem = null,
-                vibrationEffect = null,
-                vibrationConstant = constant
-            )
-        )
-
-        verify(vibratorHelper).performHapticFeedback(any(), eq(constant))
-    }
-
     /** Regression test for b/266119467. */
     @Test
     fun displayView_animationFailure_viewsStillBecomeVisible() {
@@ -729,14 +706,12 @@
         endItem: ChipbarEndItem?,
         vibrationEffect: VibrationEffect? = null,
         allowSwipeToDismiss: Boolean = false,
-        vibrationConstant: Int = HapticFeedbackConstants.NO_HAPTICS,
     ): ChipbarInfo {
         return ChipbarInfo(
             TintedIcon(startIcon, tint = null),
             text,
             endItem,
             vibrationEffect,
-            vibrationConstant,
             allowSwipeToDismiss,
             windowTitle = WINDOW_TITLE,
             wakeReason = WAKE_REASON,
diff --git a/packages/SystemUI/tests/src/com/android/CoroutineTestScopeModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/CoroutineTestScopeModule.kt
similarity index 98%
rename from packages/SystemUI/tests/src/com/android/CoroutineTestScopeModule.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/CoroutineTestScopeModule.kt
index 360aa0f..de310b4 100644
--- a/packages/SystemUI/tests/src/com/android/CoroutineTestScopeModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/CoroutineTestScopeModule.kt
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android
+package com.android.systemui
 
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
diff --git a/packages/SystemUI/tests/src/com/android/SysUITestModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt
similarity index 95%
rename from packages/SystemUI/tests/src/com/android/SysUITestModule.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt
index 97e43ad..f2c32e3d 100644
--- a/packages/SystemUI/tests/src/com/android/SysUITestModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt
@@ -13,15 +13,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android
+package com.android.systemui
 
 import android.content.Context
 import android.content.res.Resources
 import android.testing.TestableContext
 import android.testing.TestableResources
-import com.android.systemui.FakeSystemUiModule
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.SysuiTestableContext
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.broadcast.FakeBroadcastDispatcher
 import com.android.systemui.coroutines.collectLastValue
diff --git a/packages/SystemUI/tests/src/com/android/TestMocksModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
similarity index 98%
rename from packages/SystemUI/tests/src/com/android/TestMocksModule.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
index fd50f15..37a4f61 100644
--- a/packages/SystemUI/tests/src/com/android/TestMocksModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android
+package com.android.systemui
 
 import android.app.ActivityManager
 import android.app.admin.DevicePolicyManager
@@ -24,7 +24,6 @@
 import com.android.keyguard.KeyguardSecurityModel
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.KeyguardViewController
-import com.android.systemui.GuestResumeSessionReceiver
 import com.android.systemui.animation.DialogLaunchAnimator
 import com.android.systemui.demomode.DemoModeController
 import com.android.systemui.dump.DumpManager
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/FakeStatusBarPolicyDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/FakeStatusBarPolicyDataLayerModule.kt
index 5aece1b..16dab40 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/FakeStatusBarPolicyDataLayerModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/FakeStatusBarPolicyDataLayerModule.kt
@@ -16,7 +16,14 @@
 package com.android.systemui.statusbar.policy.data
 
 import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepositoryModule
+import com.android.systemui.statusbar.policy.data.repository.FakeZenModeRepositoryModule
 import dagger.Module
 
-@Module(includes = [FakeDeviceProvisioningRepositoryModule::class])
+@Module(
+    includes =
+        [
+            FakeDeviceProvisioningRepositoryModule::class,
+            FakeZenModeRepositoryModule::class,
+        ]
+)
 object FakeStatusBarPolicyDataLayerModule
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeZenModeRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeZenModeRepository.kt
new file mode 100644
index 0000000..4059930
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeZenModeRepository.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy.data.repository
+
+import android.app.NotificationManager
+import android.provider.Settings
+import com.android.systemui.dagger.SysUISingleton
+import dagger.Binds
+import dagger.Module
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+
+@SysUISingleton
+class FakeZenModeRepository @Inject constructor() : ZenModeRepository {
+    override val zenMode: MutableStateFlow<Int> = MutableStateFlow(Settings.Global.ZEN_MODE_OFF)
+    override val consolidatedNotificationPolicy: MutableStateFlow<NotificationManager.Policy?> =
+        MutableStateFlow(
+            NotificationManager.Policy(
+                /* priorityCategories = */ 0,
+                /* priorityCallSenders = */ 0,
+                /* priorityMessageSenders = */ 0,
+            )
+        )
+}
+
+@Module
+interface FakeZenModeRepositoryModule {
+    @Binds fun bindFake(fake: FakeZenModeRepository): ZenModeRepository
+}
diff --git a/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java b/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java
index 4c224fb..fb521e1 100644
--- a/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java
+++ b/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java
@@ -359,7 +359,7 @@
     }
 
     @Test
-    public void testUpdateWallpaperComponent_doesApplyLater() throws IOException {
+    public void testUpdateWallpaperComponent_systemAndLock() throws IOException {
         mWallpaperBackupAgent.mIsDeviceInRestore = true;
         mWallpaperBackupAgent.updateWallpaperComponent(mWallpaperComponent,
                 /* which */ FLAG_LOCK | FLAG_SYSTEM);
@@ -377,7 +377,7 @@
     }
 
     @Test
-    public void testUpdateWallpaperComponent_applyToLockFalse_doesApplyLaterOnlyToMainScreen()
+    public void testUpdateWallpaperComponent_systemOnly()
             throws IOException {
         mWallpaperBackupAgent.mIsDeviceInRestore = true;
 
@@ -617,7 +617,7 @@
 
         mWallpaperBackupAgent.onRestoreFinished();
 
-        // wallpaper will be applied to home & lock screen, a success for both screens in expected
+        // wallpaper will be applied to home & lock screen, a success for both screens is expected
         DataTypeResult result = getLoggingResult(WALLPAPER_IMG_SYSTEM,
                 mWallpaperBackupAgent.getBackupRestoreEventLogger().getLoggingResults());
         assertThat(result).isNotNull();
diff --git a/services/companion/Android.bp b/services/companion/Android.bp
index fb8db21..550e17b 100644
--- a/services/companion/Android.bp
+++ b/services/companion/Android.bp
@@ -21,7 +21,6 @@
     defaults: ["platform_service_defaults"],
     srcs: [
         ":services.companion-sources",
-        ":VirtualCamera-aidl-sources",
     ],
     libs: [
         "app-compat-annotations",
@@ -30,13 +29,6 @@
     static_libs: [
         "ukey2_jni",
         "virtualdevice_flags_lib",
+        "virtual_camera_service_aidl-java",
     ],
 }
-
-filegroup {
-    name: "VirtualCamera-aidl-sources",
-    srcs: [
-        "java/com/android/server/companion/virtual/camera/*.aidl",
-    ],
-    path: "java",
-}
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index 562fe3b..118943d 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -51,7 +51,7 @@
 import android.companion.virtual.VirtualDeviceParams;
 import android.companion.virtual.audio.IAudioConfigChangedCallback;
 import android.companion.virtual.audio.IAudioRoutingCallback;
-import android.companion.virtual.camera.IVirtualCamera;
+import android.companion.virtual.camera.VirtualCameraConfig;
 import android.companion.virtual.flags.Flags;
 import android.companion.virtual.sensor.VirtualSensor;
 import android.companion.virtual.sensor.VirtualSensorEvent;
@@ -277,7 +277,7 @@
                 runningAppsChangedCallback,
                 params,
                 DisplayManagerGlobal.getInstance(),
-                Flags.virtualCamera() ? new VirtualCameraController(context) : null);
+                Flags.virtualCamera() ? new VirtualCameraController() : null);
     }
 
     @VisibleForTesting
@@ -692,7 +692,8 @@
         final long ident = Binder.clearCallingIdentity();
         try {
             mInputController.createDpad(config.getInputDeviceName(), config.getVendorId(),
-                    config.getProductId(), deviceToken, config.getAssociatedDisplayId());
+                    config.getProductId(), deviceToken,
+                    getTargetDisplayIdForInput(config.getAssociatedDisplayId()));
         } finally {
             Binder.restoreCallingIdentity(ident);
         }
@@ -710,7 +711,8 @@
         final long ident = Binder.clearCallingIdentity();
         try {
             mInputController.createKeyboard(config.getInputDeviceName(), config.getVendorId(),
-                    config.getProductId(), deviceToken, config.getAssociatedDisplayId(),
+                    config.getProductId(), deviceToken,
+                    getTargetDisplayIdForInput(config.getAssociatedDisplayId()),
                     config.getLanguageTag(), config.getLayoutType());
         } finally {
             Binder.restoreCallingIdentity(ident);
@@ -776,7 +778,8 @@
         try {
             mInputController.createNavigationTouchpad(
                     config.getInputDeviceName(), config.getVendorId(),
-                    config.getProductId(), deviceToken, config.getAssociatedDisplayId(),
+                    config.getProductId(), deviceToken,
+                    getTargetDisplayIdForInput(config.getAssociatedDisplayId()),
                     touchpadHeight, touchpadWidth);
         } finally {
             Binder.restoreCallingIdentity(ident);
@@ -954,13 +957,28 @@
         }
     }
 
+    @Override // Binder call
     @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
-    public void registerVirtualCamera(@NonNull IVirtualCamera camera) {
+    public void registerVirtualCamera(@NonNull VirtualCameraConfig cameraConfig)
+            throws RemoteException {
         super.registerVirtualCamera_enforcePermission();
+        Objects.requireNonNull(cameraConfig);
         if (mVirtualCameraController == null) {
-            return;
+            throw new UnsupportedOperationException("Virtual camera controller is not available");
         }
-        mVirtualCameraController.registerCamera(Objects.requireNonNull(camera));
+        mVirtualCameraController.registerCamera(Objects.requireNonNull(cameraConfig));
+    }
+
+    @Override // Binder call
+    @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+    public void unregisterVirtualCamera(@NonNull VirtualCameraConfig cameraConfig)
+            throws RemoteException {
+        super.unregisterVirtualCamera_enforcePermission();
+        Objects.requireNonNull(cameraConfig);
+        if (mVirtualCameraController == null) {
+            throw new UnsupportedOperationException("Virtual camera controller is not available");
+        }
+        mVirtualCameraController.unregisterCamera(Objects.requireNonNull(cameraConfig));
     }
 
     @Override
@@ -987,6 +1005,20 @@
         }
     }
 
+    // For display mirroring, we want to dispatch all key events to the source (default) display,
+    // as the virtual display doesn't have any focused windows. Hence, call this for
+    // associating any input device to the source display if the input device emits any key events.
+    private int getTargetDisplayIdForInput(int displayId) {
+        if (!Flags.interactiveScreenMirror()) {
+            return displayId;
+        }
+
+        DisplayManagerInternal displayManager = LocalServices.getService(
+                DisplayManagerInternal.class);
+        int mirroredDisplayId = displayManager.getDisplayIdToMirror(displayId);
+        return mirroredDisplayId == Display.INVALID_DISPLAY ? displayId : mirroredDisplayId;
+    }
+
     @GuardedBy("mVirtualDeviceLock")
     private GenericWindowPolicyController createWindowPolicyControllerLocked(
             @NonNull Set<String> displayCategories) {
diff --git a/services/companion/java/com/android/server/companion/virtual/camera/IVirtualCameraService.aidl b/services/companion/java/com/android/server/companion/virtual/camera/IVirtualCameraService.aidl
deleted file mode 100644
index a4c1c42..0000000
--- a/services/companion/java/com/android/server/companion/virtual/camera/IVirtualCameraService.aidl
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.companion.virtual.camera;
-
-import android.companion.virtual.camera.IVirtualCamera;
-import android.companion.virtual.camera.VirtualCameraHalConfig;
-
-/**
- * AIDL Interface to communicate with the VirtualCamera HAL
- * @hide
- */
-interface IVirtualCameraService {
-
-    /**
-     * Registers a new camera with the virtual camera hal.
-     * @return true if the camera was successfully registered
-     */
-    boolean registerCamera(in IVirtualCamera camera);
-
-    /**
-     * Unregisters the camera from the virtual camera hal. After this call the virtual camera won't
-     * be visible to the camera framework anymore.
-     */
-    void unregisterCamera(in IVirtualCamera camera);
-}
diff --git a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java
index 031d949..06be3f3 100644
--- a/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java
+++ b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraController.java
@@ -16,207 +16,127 @@
 
 package com.android.server.companion.virtual.camera;
 
+import static com.android.server.companion.virtual.camera.VirtualCameraConversionUtil.getServiceCameraConfiguration;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.companion.virtual.camera.IVirtualCamera;
-import android.companion.virtual.camera.VirtualCameraHalConfig;
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.content.ServiceConnection;
+import android.companion.virtual.camera.VirtualCameraConfig;
+import android.companion.virtualcamera.IVirtualCameraService;
+import android.companion.virtualcamera.VirtualCameraConfiguration;
 import android.os.Binder;
 import android.os.IBinder;
 import android.os.RemoteException;
-import android.os.UserHandle;
-import android.util.Log;
+import android.os.ServiceManager;
+import android.util.ArraySet;
+import android.util.Slog;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 
 import java.io.PrintWriter;
-import java.util.HashMap;
-import java.util.Map;
+import java.util.Set;
 
 /**
  * Manages the registration and removal of virtual camera from the server side.
  *
  * <p>This classes delegate calls to the virtual camera service, so it is dependent on the service
- * to be up and running
+ * to be up and running.
  */
-public class VirtualCameraController implements IBinder.DeathRecipient, ServiceConnection {
+public final class VirtualCameraController implements IBinder.DeathRecipient {
 
-    private static class VirtualCameraInfo {
-
-        private final IVirtualCamera mVirtualCamera;
-        private boolean mIsRegistered;
-
-        VirtualCameraInfo(IVirtualCamera virtualCamera) {
-            mVirtualCamera = virtualCamera;
-        }
-    }
-
+    private static final String VIRTUAL_CAMERA_SERVICE_NAME = "virtual_camera";
     private static final String TAG = "VirtualCameraController";
 
-    private static final String VIRTUAL_CAMERA_SERVICE_PACKAGE = "com.android.virtualcamera";
-    private static final String VIRTUAL_CAMERA_SERVICE_CLASS = ".VirtualCameraService";
-    private final Context mContext;
-
-    @Nullable private IVirtualCameraService mVirtualCameraService = null;
+    @Nullable private IVirtualCameraService mVirtualCameraService;
 
     @GuardedBy("mCameras")
-    private final Map<IVirtualCamera, VirtualCameraInfo> mCameras = new HashMap<>(1);
+    private final Set<VirtualCameraConfig> mCameras = new ArraySet<>();
 
-    public VirtualCameraController(Context context) {
-        mContext = context;
+    public VirtualCameraController() {
         connectVirtualCameraService();
     }
 
-    private void connectVirtualCameraService() {
-        final long callingId = Binder.clearCallingIdentity();
+    @VisibleForTesting
+    VirtualCameraController(IVirtualCameraService virtualCameraService) {
+        mVirtualCameraService = virtualCameraService;
+    }
+
+    /**
+     * Register a new virtual camera with the given config.
+     *
+     * @param cameraConfig The {@link VirtualCameraConfig} sent by the client.
+     */
+    public void registerCamera(@NonNull VirtualCameraConfig cameraConfig) {
+        // Try to connect to service if not connected already.
+        if (mVirtualCameraService == null) {
+            connectVirtualCameraService();
+        }
+        // Throw exception if we are unable to connect to service.
+        if (mVirtualCameraService == null) {
+            throw new IllegalStateException("Virtual camera service is not connected.");
+        }
+
         try {
-            Intent intent = new Intent();
-            intent.setPackage(VIRTUAL_CAMERA_SERVICE_PACKAGE);
-            intent.setComponent(
-                    ComponentName.createRelative(
-                            VIRTUAL_CAMERA_SERVICE_PACKAGE, VIRTUAL_CAMERA_SERVICE_CLASS));
-            mContext.startServiceAsUser(intent, UserHandle.SYSTEM);
-            if (!mContext.bindServiceAsUser(
-                    intent,
-                    this,
-                    Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT,
-                    UserHandle.SYSTEM)) {
-                mContext.unbindService(this);
-                Log.w(
-                        TAG,
-                        "connectVirtualCameraService: Failed to connect to the virtual camera "
-                                + "service");
-            }
-        } finally {
-            Binder.restoreCallingIdentity(callingId);
-        }
-    }
-
-    private void forwardPendingRegistrations() {
-        IVirtualCameraService cameraService = mVirtualCameraService;
-        if (cameraService == null) {
-            return;
-        }
-        synchronized (mCameras) {
-            for (VirtualCameraInfo cameraInfo : mCameras.values()) {
-                if (cameraInfo.mIsRegistered) {
-                    continue;
-                }
-                try {
-                    cameraService.registerCamera(cameraInfo.mVirtualCamera);
-                    cameraInfo.mIsRegistered = true;
-                } catch (RemoteException e) {
-                    e.rethrowFromSystemServer();
-                }
-            }
-        }
-    }
-
-    /**
-     * Remove the virtual camera with the provided name
-     *
-     * @param camera The name of the camera to remove
-     */
-    public void unregisterCamera(@NonNull IVirtualCamera camera) {
-        IVirtualCameraService virtualCameraService = mVirtualCameraService;
-        if (virtualCameraService != null) {
-            try {
-                virtualCameraService.unregisterCamera(camera);
+            if (registerCameraWithService(cameraConfig)) {
                 synchronized (mCameras) {
-                    VirtualCameraInfo cameraInfo = mCameras.remove(camera);
-                    cameraInfo.mIsRegistered = false;
+                    mCameras.add(cameraConfig);
                 }
-            } catch (RemoteException e) {
-                e.rethrowFromSystemServer();
+            } else {
+                // TODO(b/310857519): Revisit this to find a better way of indicating failure.
+                throw new RuntimeException("Failed to register virtual camera.");
             }
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
         }
     }
 
     /**
-     * Register a new virtual camera with the provided characteristics.
+     * Unregister the virtual camera with the given config.
      *
-     * @param camera The {@link IVirtualCamera} producing the image to communicate with the client.
-     * @throws IllegalArgumentException if the characteristics could not be parsed.
+     * @param cameraConfig The {@link VirtualCameraConfig} sent by the client.
      */
-    public void registerCamera(@NonNull IVirtualCamera camera) {
-        IVirtualCameraService service = mVirtualCameraService;
-        VirtualCameraInfo virtualCameraInfo = new VirtualCameraInfo(camera);
-        synchronized (mCameras) {
-            mCameras.put(camera, virtualCameraInfo);
-        }
-        if (service != null) {
-            try {
-                if (service.registerCamera(camera)) {
-                    virtualCameraInfo.mIsRegistered = true;
-                    return;
-                }
-            } catch (RemoteException e) {
-                e.rethrowFromSystemServer();
+    public void unregisterCamera(@NonNull VirtualCameraConfig cameraConfig) {
+        try {
+            if (mVirtualCameraService == null) {
+                Slog.w(TAG, "Virtual camera service is not connected.");
+            } else {
+                mVirtualCameraService.unregisterCamera(cameraConfig.getCallback().asBinder());
             }
+            synchronized (mCameras) {
+                mCameras.remove(cameraConfig);
+            }
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
         }
-
-        // Service was not available or registration failed, save the registration for later
-        connectVirtualCameraService();
     }
 
     @Override
     public void binderDied() {
-        Log.d(TAG, "binderDied");
+        Slog.d(TAG, "Virtual camera service died.");
         mVirtualCameraService = null;
-    }
-
-    @Override
-    public void onBindingDied(ComponentName name) {
-        mVirtualCameraService = null;
-        Log.d(TAG, "onBindingDied() called with: name = [" + name + "]");
-    }
-
-    @Override
-    public void onNullBinding(ComponentName name) {
-        mVirtualCameraService = null;
-        Log.d(TAG, "onNullBinding() called with: name = [" + name + "]");
-    }
-
-    @Override
-    public void onServiceConnected(ComponentName name, IBinder service) {
-        Log.d(TAG, "onServiceConnected: " + name.toString());
-        mVirtualCameraService = IVirtualCameraService.Stub.asInterface(service);
-        try {
-            service.linkToDeath(this, 0);
-        } catch (RemoteException e) {
-            e.rethrowAsRuntimeException();
+        synchronized (mCameras) {
+            mCameras.clear();
         }
-        forwardPendingRegistrations();
-    }
-
-    @Override
-    public void onServiceDisconnected(ComponentName name) {
-        Log.d(TAG, "onServiceDisconnected() called with: name = [" + name + "]");
-        mVirtualCameraService = null;
     }
 
     /** Release resources associated with this controller. */
     public void close() {
-        if (mVirtualCameraService == null) {
-            return;
-        }
         synchronized (mCameras) {
-            mCameras.forEach(
-                    (name, cameraInfo) -> {
-                        try {
-                            mVirtualCameraService.unregisterCamera(name);
-                        } catch (RemoteException e) {
-                            Log.w(
-                                    TAG,
-                                    "close(): Camera failed to be removed on camera service.",
-                                    e);
-                        }
-                    });
+            if (mVirtualCameraService == null) {
+                Slog.w(TAG, "Virtual camera service is not connected.");
+            } else {
+                for (VirtualCameraConfig config : mCameras) {
+                    try {
+                        mVirtualCameraService.unregisterCamera(config.getCallback().asBinder());
+                    } catch (RemoteException e) {
+                        Slog.w(TAG, "close(): Camera failed to be removed on camera "
+                                + "service.", e);
+                    }
+                }
+            }
+            mCameras.clear();
         }
-        mContext.unbindService(this);
+        mVirtualCameraService = null;
     }
 
     /** Dumps information about this {@link VirtualCameraController} for debugging purposes. */
@@ -226,20 +146,34 @@
         fout.printf("%sService:%s\n", indent, mVirtualCameraService);
         synchronized (mCameras) {
             fout.printf("%sRegistered cameras:%d%n\n", indent, mCameras.size());
-            for (VirtualCameraInfo info : mCameras.values()) {
-                VirtualCameraHalConfig config = null;
-                try {
-                    config = info.mVirtualCamera.getHalConfig();
-                } catch (RemoteException ex) {
-                    Log.w(TAG, ex);
-                }
-                fout.printf(
-                        "%s- %s isRegistered: %s, token: %s\n",
-                        indent,
-                        config == null ? "" : config.displayName,
-                        info.mIsRegistered,
-                        info.mVirtualCamera);
+            for (VirtualCameraConfig config : mCameras) {
+                fout.printf("%s token: %s\n", indent, config);
             }
         }
     }
+
+    private void connectVirtualCameraService() {
+        final long callingId = Binder.clearCallingIdentity();
+        try {
+            IBinder virtualCameraBinder =
+                    ServiceManager.waitForService(VIRTUAL_CAMERA_SERVICE_NAME);
+            if (virtualCameraBinder == null) {
+                Slog.e(TAG, "connectVirtualCameraService: Failed to connect to the virtual "
+                        + "camera service");
+                return;
+            }
+            virtualCameraBinder.linkToDeath(this, 0);
+            mVirtualCameraService = IVirtualCameraService.Stub.asInterface(virtualCameraBinder);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        } finally {
+            Binder.restoreCallingIdentity(callingId);
+        }
+    }
+
+    private boolean registerCameraWithService(VirtualCameraConfig config) throws RemoteException {
+        VirtualCameraConfiguration serviceConfiguration = getServiceCameraConfiguration(config);
+        return mVirtualCameraService.registerCamera(config.getCallback().asBinder(),
+                serviceConfiguration);
+    }
 }
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
new file mode 100644
index 0000000..202f68b
--- /dev/null
+++ b/services/companion/java/com/android/server/companion/virtual/camera/VirtualCameraConversionUtil.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.companion.virtual.camera;
+
+import android.annotation.NonNull;
+import android.companion.virtual.camera.IVirtualCameraCallback;
+import android.companion.virtual.camera.VirtualCameraConfig;
+import android.companion.virtual.camera.VirtualCameraStreamConfig;
+import android.companion.virtualcamera.IVirtualCameraService;
+import android.companion.virtualcamera.SupportedStreamConfiguration;
+import android.companion.virtualcamera.VirtualCameraConfiguration;
+import android.os.RemoteException;
+import android.view.Surface;
+
+/** Utilities to convert the client side classes to the virtual camera service ones. */
+public final class VirtualCameraConversionUtil {
+
+    /**
+     * Fetches the configuration of the provided virtual cameraConfig that was provided by its owner
+     * and convert it into the {@link IVirtualCameraService} types: {@link
+     * VirtualCameraConfiguration}.
+     *
+     * @param cameraConfig The cameraConfig sent by the client.
+     * @return The converted configuration to be sent to the {@link IVirtualCameraService}.
+     * @throws RemoteException If there was an issue fetching the configuration from the client.
+     */
+    @NonNull
+    public static android.companion.virtualcamera.VirtualCameraConfiguration
+            getServiceCameraConfiguration(@NonNull VirtualCameraConfig cameraConfig)
+                    throws RemoteException {
+        VirtualCameraConfiguration serviceConfiguration = new VirtualCameraConfiguration();
+
+        serviceConfiguration.supportedStreamConfigs =
+                cameraConfig.getStreamConfigs().stream()
+                        .map(VirtualCameraConversionUtil::convertSupportedStreamConfiguration)
+                        .toArray(SupportedStreamConfiguration[]::new);
+
+        serviceConfiguration.virtualCameraCallback = convertCallback(cameraConfig.getCallback());
+        return serviceConfiguration;
+    }
+
+    @NonNull
+    private static android.companion.virtualcamera.IVirtualCameraCallback convertCallback(
+            @NonNull IVirtualCameraCallback camera) {
+        return new android.companion.virtualcamera.IVirtualCameraCallback.Stub() {
+            @Override
+            public void onStreamConfigured(
+                    int streamId, Surface surface, int width, int height, int pixelFormat)
+                    throws RemoteException {
+                VirtualCameraStreamConfig streamConfig =
+                        createStreamConfig(width, height, pixelFormat);
+                camera.onStreamConfigured(streamId, surface, streamConfig);
+            }
+
+            @Override
+            public void onStreamClosed(int streamId) throws RemoteException {
+                camera.onStreamClosed(streamId);
+            }
+        };
+    }
+
+    @NonNull
+    private static VirtualCameraStreamConfig createStreamConfig(
+            int width, int height, int pixelFormat) {
+        return new VirtualCameraStreamConfig(width, height, pixelFormat);
+    }
+
+    @NonNull
+    private static SupportedStreamConfiguration convertSupportedStreamConfiguration(
+            VirtualCameraStreamConfig stream) {
+        SupportedStreamConfiguration supportedConfig = new SupportedStreamConfiguration();
+        supportedConfig.height = stream.getHeight();
+        supportedConfig.width = stream.getWidth();
+        supportedConfig.pixelFormat = stream.getFormat();
+        return supportedConfig;
+    }
+
+    private VirtualCameraConversionUtil() {
+    }
+}
diff --git a/services/core/java/com/android/server/TEST_MAPPING b/services/core/java/com/android/server/TEST_MAPPING
index cd9c53b..80c4c58 100644
--- a/services/core/java/com/android/server/TEST_MAPPING
+++ b/services/core/java/com/android/server/TEST_MAPPING
@@ -41,6 +41,18 @@
             "file_patterns": ["StorageManagerService\\.java"]
         },
         {
+            "name": "CtsScopedStorageBypassDatabaseOperationsTest",
+            "file_patterns": ["StorageManagerService\\.java"]
+        },
+        {
+            "name": "CtsScopedStorageGeneralTest",
+            "file_patterns": ["StorageManagerService\\.java"]
+        },
+        {
+            "name": "CtsScopedStorageRedactUriTest",
+            "file_patterns": ["StorageManagerService\\.java"]
+        },
+        {
             "name": "FrameworksMockingServicesTests",
             "options": [
                 {
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index bdcde66..f8078d2 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -869,8 +869,7 @@
                 if (!mWallpaper.wallpaperUpdating && mWallpaper.userId == mCurrentUserId) {
                     Slog.w(TAG, "Wallpaper reconnect timed out for " + mWallpaper.wallpaperComponent
                             + ", reverting to built-in wallpaper!");
-                    int which = mWallpaper.mWhich;
-                    clearWallpaperLocked(which, mWallpaper.userId, false, null);
+                    clearWallpaperLocked(mWallpaper.mWhich, mWallpaper.userId, false, null);
                 }
             }
         };
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 3e23fab..f700944 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -173,7 +173,6 @@
     private final TransitionController mController;
     private final BLASTSyncEngine mSyncEngine;
     private final Token mToken;
-    private IApplicationThread mRemoteAnimApp;
 
     private @Nullable ActivityRecord mPipActivity;
 
@@ -1485,13 +1484,14 @@
         return mForcePlaying;
     }
 
+    /** Adjusts the priority of the process which will run the transition animation. */
     void setRemoteAnimationApp(IApplicationThread app) {
-        mRemoteAnimApp = app;
-    }
-
-    /** Returns the app which will run the transition animation. */
-    IApplicationThread getRemoteAnimationApp() {
-        return mRemoteAnimApp;
+        final WindowProcessController wpc = mController.mAtm.getProcessController(app);
+        if (wpc != null) {
+            // This is an early prediction. If the process doesn't ack the animation in 200 ms,
+            // the priority will be restored.
+            mController.mRemotePlayer.update(wpc, true /* running */, true /* predict */);
+        }
     }
 
     void setNoAnimation(WindowContainer wc) {
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index 1f01778..a736874 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -1251,13 +1251,7 @@
         } else if (mPlayingTransitions.isEmpty()) {
             mTransitionPlayerProc.setRunningRemoteAnimation(false);
             mRemotePlayer.clear();
-            return;
         }
-        final IApplicationThread appThread = transition.getRemoteAnimationApp();
-        if (appThread == null || appThread == mTransitionPlayerProc.getThread()) return;
-        final WindowProcessController delegate = mAtm.getProcessController(appThread);
-        if (delegate == null) return;
-        mRemotePlayer.update(delegate, isPlaying, true /* predict */);
     }
 
     /** Called when a transition is aborted. This should only be called by {@link Transition} */
@@ -1483,7 +1477,7 @@
      * {@link #mTransitionPlayerProc}.
      */
     static class RemotePlayer {
-        private static final long REPORT_RUNNING_GRACE_PERIOD_MS = 100;
+        private static final long REPORT_RUNNING_GRACE_PERIOD_MS = 200;
         @GuardedBy("itself")
         private final ArrayMap<IBinder, DelegateProcess> mDelegateProcesses = new ArrayMap<>();
         private final ActivityTaskManagerService mAtm;
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualCameraTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualCameraTest.java
deleted file mode 100644
index 8f77e9b..0000000
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualCameraTest.java
+++ /dev/null
@@ -1,224 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.companion.virtual;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import android.Manifest;
-import android.companion.virtual.VirtualDeviceParams;
-import android.companion.virtual.camera.IVirtualCamera;
-import android.companion.virtual.camera.IVirtualCameraSession;
-import android.companion.virtual.camera.VirtualCamera;
-import android.companion.virtual.camera.VirtualCameraConfig;
-import android.companion.virtual.camera.VirtualCameraHalConfig;
-import android.companion.virtual.camera.VirtualCameraSession;
-import android.companion.virtual.camera.VirtualCameraStreamConfig;
-import android.companion.virtual.flags.Flags;
-import android.content.ComponentName;
-import android.graphics.ImageFormat;
-import android.os.Handler;
-import android.os.HandlerExecutor;
-import android.os.Looper;
-import android.os.RemoteException;
-import android.platform.test.annotations.Presubmit;
-import android.platform.test.annotations.RequiresFlagsDisabled;
-import android.platform.test.annotations.RequiresFlagsEnabled;
-import android.platform.test.flag.junit.CheckFlagsRule;
-import android.platform.test.flag.junit.DeviceFlagsValueProvider;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableContext;
-import android.testing.TestableLooper;
-
-import androidx.annotation.NonNull;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import com.android.compatibility.common.util.AdoptShellPermissionsRule;
-import com.android.server.companion.virtual.camera.IVirtualCameraService;
-import com.android.server.companion.virtual.camera.VirtualCameraController;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.HashSet;
-import java.util.Set;
-
-@Presubmit
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
-public class VirtualCameraTest {
-
-    private static final String PKG = "com.android.virtualcamera";
-    private static final String CLS = ".VirtualCameraService";
-    public static final String CAMERA_DISPLAY_NAME = "testCamera";
-
-    private final TestableContext mContext =
-            new TestableContext(InstrumentationRegistry.getInstrumentation().getContext());
-    private FakeVirtualCameraService mFakeVirtualCameraService;
-    private VirtualCameraController mVirtualCameraController;
-
-    @Rule public final VirtualDeviceRule mVirtualDeviceRule = new VirtualDeviceRule(mContext);
-
-    @Rule
-    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
-
-    @Rule
-    public AdoptShellPermissionsRule mAdoptShellPermissionsRule =
-            new AdoptShellPermissionsRule(
-                    InstrumentationRegistry.getInstrumentation().getUiAutomation(),
-                    Manifest.permission.CREATE_VIRTUAL_DEVICE);
-
-    @Before
-    public void setUp() {
-        mVirtualDeviceRule.withVirtualCameraControllerSupplier(() -> mVirtualCameraController);
-        mFakeVirtualCameraService = new FakeVirtualCameraService();
-        connectFakeService();
-        mVirtualCameraController = new VirtualCameraController(mContext);
-    }
-
-    private VirtualDeviceImpl createVirtualDevice() {
-        return mVirtualDeviceRule.createVirtualDevice(new VirtualDeviceParams.Builder().build());
-    }
-
-    private void connectFakeService() {
-        mContext.addMockService(
-                ComponentName.createRelative(PKG, CLS), mFakeVirtualCameraService.asBinder());
-    }
-
-    @RequiresFlagsEnabled(Flags.FLAG_VIRTUAL_CAMERA)
-    @Test
-    public void addVirtualCamera() {
-        VirtualDeviceImpl virtualDevice = createVirtualDevice();
-        VirtualCameraConfig config = createVirtualCameraConfig(null);
-        IVirtualCamera.Default camera = new IVirtualCamera.Default();
-        virtualDevice.registerVirtualCamera(camera);
-
-        assertThat(mFakeVirtualCameraService.mCameras).contains(camera);
-    }
-
-    @RequiresFlagsEnabled(Flags.FLAG_VIRTUAL_CAMERA)
-    @Test
-    public void addVirtualCamera_serviceNotReady() {
-        TestableContext context =
-                new TestableContext(InstrumentationRegistry.getInstrumentation().getContext());
-        VirtualCameraController virtualCameraController = new VirtualCameraController(context);
-        mVirtualDeviceRule.withVirtualCameraControllerSupplier(() -> virtualCameraController);
-
-        VirtualDeviceImpl virtualDevice =
-                mVirtualDeviceRule.createVirtualDevice(new VirtualDeviceParams.Builder().build());
-        IVirtualCamera.Default camera = new IVirtualCamera.Default();
-        VirtualCameraConfig config = createVirtualCameraConfig(null);
-        virtualDevice.registerVirtualCamera(camera);
-        FakeVirtualCameraService fakeVirtualCameraService = new FakeVirtualCameraService();
-
-        // Only add the service after connecting the camera
-        virtualCameraController.onServiceConnected(
-                ComponentName.createRelative(PKG, CLS), fakeVirtualCameraService.asBinder());
-
-        assertThat(fakeVirtualCameraService.mCameras).contains(camera);
-    }
-
-    @RequiresFlagsEnabled(Flags.FLAG_VIRTUAL_CAMERA)
-    @Test
-    public void getCameraConfiguration() {
-        VirtualDeviceImpl virtualDevice = createVirtualDevice();
-        VirtualCameraSession virtualCameraSession = new VirtualCameraSession() {};
-        VirtualCameraConfig config =
-                new VirtualCameraConfig.Builder()
-                        .addStreamConfiguration(10, 10, ImageFormat.RGB_565)
-                        .setDisplayName(CAMERA_DISPLAY_NAME)
-                        .setCallback(
-                                new HandlerExecutor(new Handler(Looper.getMainLooper())),
-                                () -> virtualCameraSession)
-                        .build();
-
-        VirtualCamera virtualCamera = new VirtualCamera(virtualDevice, config);
-
-        VirtualCameraConfig returnedConfig = virtualCamera.getConfig();
-        assertThat(returnedConfig).isNotNull();
-        assertThat(returnedConfig.getDisplayName()).isEqualTo(CAMERA_DISPLAY_NAME);
-        Set<VirtualCameraStreamConfig> streamConfigs = returnedConfig.getStreamConfigs();
-        assertThat(streamConfigs).hasSize(1);
-        VirtualCameraStreamConfig streamConfig =
-                streamConfigs.toArray(new VirtualCameraStreamConfig[0])[0];
-        assertThat(streamConfig.format).isEqualTo(ImageFormat.RGB_565);
-        assertThat(streamConfig.width).isEqualTo(10);
-        assertThat(streamConfig.height).isEqualTo(10);
-
-        VirtualCameraHalConfig halConfig = virtualCamera.getHalConfig();
-        assertThat(halConfig).isNotNull();
-        assertThat(halConfig.displayName).isEqualTo(CAMERA_DISPLAY_NAME);
-        assertThat(halConfig.streamConfigs).asList().hasSize(1);
-        assertThat(halConfig.streamConfigs[0].format).isEqualTo(ImageFormat.RGB_565);
-        assertThat(halConfig.streamConfigs[0].width).isEqualTo(10);
-        assertThat(halConfig.streamConfigs[0].height).isEqualTo(10);
-    }
-
-    @RequiresFlagsEnabled(Flags.FLAG_VIRTUAL_CAMERA)
-    @Test
-    public void createCameraWithVirtualCameraInstance() {
-        VirtualDeviceImpl virtualDevice = createVirtualDevice();
-
-        VirtualCameraSession virtualCameraSession = new VirtualCameraSession() {};
-        VirtualCameraConfig config = createVirtualCameraConfig(virtualCameraSession);
-        VirtualCamera virtualCamera = new VirtualCamera(virtualDevice, config);
-
-        assertThat(mFakeVirtualCameraService.mCameras).contains(virtualCamera);
-        assertThat(virtualCamera.open()).isInstanceOf(IVirtualCameraSession.class);
-    }
-
-    @RequiresFlagsDisabled(Flags.FLAG_VIRTUAL_CAMERA)
-    @Test
-    public void createCameraDoesNothingWhenControllerIsNull() {
-        mVirtualDeviceRule.withVirtualCameraControllerSupplier(() -> null);
-        VirtualDeviceImpl virtualDevice = createVirtualDevice();
-        IVirtualCamera.Default camera = new IVirtualCamera.Default();
-        VirtualCameraConfig config = createVirtualCameraConfig(null);
-        virtualDevice.registerVirtualCamera(camera);
-
-        assertThat(mFakeVirtualCameraService.mCameras).doesNotContain(camera);
-    }
-
-    @NonNull
-    private static VirtualCameraConfig createVirtualCameraConfig(
-            VirtualCameraSession virtualCameraSession) {
-        return new VirtualCameraConfig.Builder()
-                .addStreamConfiguration(10, 10, ImageFormat.RGB_565)
-                .setDisplayName(CAMERA_DISPLAY_NAME)
-                .setCallback(
-                        new HandlerExecutor(new Handler(Looper.getMainLooper())),
-                        () -> virtualCameraSession)
-                .build();
-    }
-
-    private static class FakeVirtualCameraService extends IVirtualCameraService.Stub {
-
-        final Set<IVirtualCamera> mCameras = new HashSet<>();
-
-        @Override
-        public boolean registerCamera(IVirtualCamera camera) throws RemoteException {
-            mCameras.add(camera);
-            return true;
-        }
-
-        @Override
-        public void unregisterCamera(IVirtualCamera camera) throws RemoteException {
-            mCameras.remove(camera);
-        }
-    }
-}
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 f978990..30300ec 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
@@ -1940,7 +1940,7 @@
                         mRunningAppsChangedCallback,
                         params,
                         new DisplayManagerGlobal(mIDisplayManager),
-                        new VirtualCameraController(mContext));
+                        new VirtualCameraController());
         mVdms.addVirtualDevice(virtualDeviceImpl);
         assertThat(virtualDeviceImpl.getAssociationId()).isEqualTo(mAssociationInfo.getId());
         assertThat(virtualDeviceImpl.getPersistentDeviceId())
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceRule.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceRule.java
deleted file mode 100644
index dbd6c88..0000000
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceRule.java
+++ /dev/null
@@ -1,222 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.companion.virtual;
-
-import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyFloat;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-
-import android.app.admin.DevicePolicyManager;
-import android.companion.AssociationInfo;
-import android.companion.virtual.IVirtualDeviceActivityListener;
-import android.companion.virtual.IVirtualDeviceSoundEffectListener;
-import android.companion.virtual.VirtualDeviceParams;
-import android.companion.virtual.flags.Flags;
-import android.content.AttributionSource;
-import android.content.Context;
-import android.hardware.display.DisplayManagerGlobal;
-import android.hardware.display.DisplayManagerInternal;
-import android.hardware.display.IDisplayManager;
-import android.net.MacAddress;
-import android.os.Binder;
-import android.testing.TestableContext;
-import android.util.ArraySet;
-import android.view.Display;
-import android.view.DisplayInfo;
-import android.view.WindowManager;
-
-import androidx.annotation.NonNull;
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import com.android.server.LocalServices;
-import com.android.server.companion.virtual.camera.VirtualCameraController;
-import com.android.server.input.InputManagerInternal;
-import com.android.server.sensors.SensorManagerInternal;
-
-import org.junit.rules.TestRule;
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.util.Objects;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
-
-/** Test rule to generate instances of {@link VirtualDeviceImpl}. */
-public class VirtualDeviceRule implements TestRule {
-
-    private static final int DEVICE_OWNER_UID = 50;
-    private static final int VIRTUAL_DEVICE_ID = 42;
-
-    private final Context mContext;
-    private InputController mInputController;
-    private CameraAccessController mCameraAccessController;
-    private AssociationInfo mAssociationInfo;
-    private VirtualDeviceManagerService mVdms;
-    private VirtualDeviceManagerInternal mLocalService;
-    private VirtualDeviceLog mVirtualDeviceLog;
-
-    // Mocks
-    @Mock private InputController.NativeWrapper mNativeWrapperMock;
-    @Mock private DisplayManagerInternal mDisplayManagerInternalMock;
-    @Mock private IDisplayManager mIDisplayManager;
-    @Mock private VirtualDeviceImpl.PendingTrampolineCallback mPendingTrampolineCallback;
-    @Mock private DevicePolicyManager mDevicePolicyManagerMock;
-    @Mock private InputManagerInternal mInputManagerInternalMock;
-    @Mock private SensorManagerInternal mSensorManagerInternalMock;
-    @Mock private IVirtualDeviceActivityListener mActivityListener;
-    @Mock private IVirtualDeviceSoundEffectListener mSoundEffectListener;
-    @Mock private Consumer<ArraySet<Integer>> mRunningAppsChangedCallback;
-    @Mock private CameraAccessController.CameraAccessBlockedCallback mCameraAccessBlockedCallback;
-
-    // Test instance suppliers
-    private Supplier<VirtualCameraController> mVirtualCameraControllerSupplier;
-
-    /**
-     * Create a new {@link VirtualDeviceRule}
-     *
-     * @param context The context to be used with the rule.
-     */
-    public VirtualDeviceRule(@NonNull Context context) {
-        Objects.requireNonNull(context);
-        mContext = context;
-    }
-
-    /**
-     * Sets a supplier that will supply an instance of {@link VirtualCameraController}. If the
-     * supplier returns null, a new instance will be created.
-     */
-    public VirtualDeviceRule withVirtualCameraControllerSupplier(
-            Supplier<VirtualCameraController> virtualCameraControllerSupplier) {
-        mVirtualCameraControllerSupplier = virtualCameraControllerSupplier;
-        return this;
-    }
-
-    @Override
-    public Statement apply(Statement base, Description description) {
-        return new Statement() {
-            @Override
-            public void evaluate() throws Throwable {
-                init(new TestableContext(mContext));
-                base.evaluate();
-            }
-        };
-    }
-
-    private void init(@NonNull TestableContext context) {
-        MockitoAnnotations.initMocks(this);
-
-        LocalServices.removeServiceForTest(DisplayManagerInternal.class);
-        LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock);
-
-        doReturn(true).when(mInputManagerInternalMock).setVirtualMousePointerDisplayId(anyInt());
-        doNothing().when(mInputManagerInternalMock).setPointerAcceleration(anyFloat(), anyInt());
-        doNothing().when(mInputManagerInternalMock).setPointerIconVisible(anyBoolean(), anyInt());
-        LocalServices.removeServiceForTest(InputManagerInternal.class);
-        LocalServices.addService(InputManagerInternal.class, mInputManagerInternalMock);
-
-        LocalServices.removeServiceForTest(SensorManagerInternal.class);
-        LocalServices.addService(SensorManagerInternal.class, mSensorManagerInternalMock);
-
-        final DisplayInfo displayInfo = new DisplayInfo();
-        displayInfo.uniqueId = "uniqueId";
-        doReturn(displayInfo).when(mDisplayManagerInternalMock).getDisplayInfo(anyInt());
-        doReturn(Display.INVALID_DISPLAY).when(mDisplayManagerInternalMock)
-                .getDisplayIdToMirror(anyInt());
-        LocalServices.removeServiceForTest(DisplayManagerInternal.class);
-        LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock);
-
-        context.addMockSystemService(DevicePolicyManager.class, mDevicePolicyManagerMock);
-
-        // Allow virtual devices to be created on the looper thread for testing.
-        final InputController.DeviceCreationThreadVerifier threadVerifier = () -> true;
-        mInputController =
-                new InputController(
-                        mNativeWrapperMock,
-                        InstrumentationRegistry.getInstrumentation()
-                                .getContext()
-                                .getMainThreadHandler(),
-                        context.getSystemService(WindowManager.class),
-                        threadVerifier);
-        mCameraAccessController =
-                new CameraAccessController(context, mLocalService, mCameraAccessBlockedCallback);
-
-        mAssociationInfo =
-                new AssociationInfo(
-                        /* associationId= */ 1,
-                        0,
-                        null,
-                        null,
-                        MacAddress.BROADCAST_ADDRESS,
-                        "",
-                        null,
-                        null,
-                        true,
-                        false,
-                        false,
-                        0,
-                        0,
-                        -1);
-
-        mVdms = new VirtualDeviceManagerService(context);
-        mLocalService = mVdms.getLocalServiceInstance();
-        mVirtualDeviceLog = new VirtualDeviceLog(context);
-    }
-
-    /**
-     * Create a {@link VirtualDeviceImpl} with the required mocks
-     *
-     * @param params See {@link
-     *     android.companion.virtual.VirtualDeviceManager#createVirtualDevice(int,
-     *     VirtualDeviceParams)}
-     */
-    public VirtualDeviceImpl createVirtualDevice(VirtualDeviceParams params) {
-        VirtualCameraController virtualCameraController = mVirtualCameraControllerSupplier.get();
-        if (Flags.virtualCamera()) {
-            if (virtualCameraController == null) {
-                virtualCameraController = new VirtualCameraController(mContext);
-            }
-        }
-
-        VirtualDeviceImpl virtualDeviceImpl =
-                new VirtualDeviceImpl(
-                        mContext,
-                        mAssociationInfo,
-                        mVdms,
-                        mVirtualDeviceLog,
-                        new Binder(),
-                        new AttributionSource(
-                                DEVICE_OWNER_UID,
-                                "com.android.virtualdevice.test",
-                                "virtualdevicerule"),
-                        VIRTUAL_DEVICE_ID,
-                        mInputController,
-                        mCameraAccessController,
-                        mPendingTrampolineCallback,
-                        mActivityListener,
-                        mSoundEffectListener,
-                        mRunningAppsChangedCallback,
-                        params,
-                        new DisplayManagerGlobal(mIDisplayManager),
-                        virtualCameraController);
-        mVdms.addVirtualDevice(virtualDeviceImpl);
-        return virtualDeviceImpl;
-    }
-}
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java
new file mode 100644
index 0000000..2583023
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.companion.virtual.camera;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.companion.virtual.camera.VirtualCameraCallback;
+import android.companion.virtual.camera.VirtualCameraConfig;
+import android.companion.virtual.camera.VirtualCameraMetadata;
+import android.companion.virtual.camera.VirtualCameraStreamConfig;
+import android.companion.virtualcamera.IVirtualCameraService;
+import android.companion.virtualcamera.VirtualCameraConfiguration;
+import android.graphics.ImageFormat;
+import android.os.Handler;
+import android.os.HandlerExecutor;
+import android.os.Looper;
+import android.platform.test.annotations.Presubmit;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.Surface;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.List;
+
+@Presubmit
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+public class VirtualCameraControllerTest {
+
+    private static final int CAMERA_DISPLAY_NAME_RES_ID_1 = 10;
+    private static final int CAMERA_WIDTH_1 = 100;
+    private static final int CAMERA_HEIGHT_1 = 200;
+    private static final int CAMERA_FORMAT_1 = ImageFormat.RGB_565;
+
+    private static final int CAMERA_DISPLAY_NAME_RES_ID_2 = 11;
+    private static final int CAMERA_WIDTH_2 = 400;
+    private static final int CAMERA_HEIGHT_2 = 600;
+    private static final int CAMERA_FORMAT_2 = ImageFormat.YUY2;
+
+    @Mock
+    private IVirtualCameraService mVirtualCameraServiceMock;
+
+    private VirtualCameraController mVirtualCameraController;
+    private final HandlerExecutor mCallbackHandler =
+            new HandlerExecutor(new Handler(Looper.getMainLooper()));
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mVirtualCameraController = new VirtualCameraController(mVirtualCameraServiceMock);
+        when(mVirtualCameraServiceMock.registerCamera(any(), any())).thenReturn(true);
+    }
+
+    @Test
+    public void registerCamera_registersCamera() throws Exception {
+        mVirtualCameraController.registerCamera(createVirtualCameraConfig(
+                CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_DISPLAY_NAME_RES_ID_1));
+
+        ArgumentCaptor<VirtualCameraConfiguration> configurationCaptor =
+                ArgumentCaptor.forClass(VirtualCameraConfiguration.class);
+        verify(mVirtualCameraServiceMock).registerCamera(any(), configurationCaptor.capture());
+        VirtualCameraConfiguration virtualCameraConfiguration = configurationCaptor.getValue();
+        assertThat(virtualCameraConfiguration.supportedStreamConfigs.length).isEqualTo(1);
+        assertVirtualCameraConfiguration(virtualCameraConfiguration, CAMERA_WIDTH_1,
+                CAMERA_HEIGHT_1, CAMERA_FORMAT_1);
+    }
+
+    @Test
+    public void unregisterCamera_unregistersCamera() throws Exception {
+        VirtualCameraConfig config = createVirtualCameraConfig(
+                CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_DISPLAY_NAME_RES_ID_1);
+        mVirtualCameraController.unregisterCamera(config);
+
+        verify(mVirtualCameraServiceMock).unregisterCamera(any());
+    }
+
+    @Test
+    public void close_unregistersAllCameras() throws Exception {
+        mVirtualCameraController.registerCamera(createVirtualCameraConfig(
+                CAMERA_WIDTH_1, CAMERA_HEIGHT_1, CAMERA_FORMAT_1, CAMERA_DISPLAY_NAME_RES_ID_1));
+        mVirtualCameraController.registerCamera(createVirtualCameraConfig(
+                CAMERA_WIDTH_2, CAMERA_HEIGHT_2, CAMERA_FORMAT_2, CAMERA_DISPLAY_NAME_RES_ID_2));
+
+        ArgumentCaptor<VirtualCameraConfiguration> configurationCaptor =
+                ArgumentCaptor.forClass(VirtualCameraConfiguration.class);
+        mVirtualCameraController.close();
+        verify(mVirtualCameraServiceMock, times(2)).registerCamera(any(),
+                configurationCaptor.capture());
+        List<VirtualCameraConfiguration> virtualCameraConfigurations =
+                configurationCaptor.getAllValues();
+        assertThat(virtualCameraConfigurations).hasSize(2);
+        assertVirtualCameraConfiguration(virtualCameraConfigurations.get(0), CAMERA_WIDTH_1,
+                CAMERA_HEIGHT_1, CAMERA_FORMAT_1);
+        assertVirtualCameraConfiguration(virtualCameraConfigurations.get(1), CAMERA_WIDTH_2,
+                CAMERA_HEIGHT_2, CAMERA_FORMAT_2);
+    }
+
+    private VirtualCameraConfig createVirtualCameraConfig(
+            int width, int height, int format, int displayNameResId) {
+        return new VirtualCameraConfig.Builder()
+                .addStreamConfig(width, height, format)
+                .setDisplayNameStringRes(displayNameResId)
+                .setVirtualCameraCallback(mCallbackHandler, createNoOpCallback())
+                .build();
+    }
+
+    private static void assertVirtualCameraConfiguration(
+            VirtualCameraConfiguration configuration, int width, int height, int format) {
+        assertThat(configuration.supportedStreamConfigs[0].width).isEqualTo(width);
+        assertThat(configuration.supportedStreamConfigs[0].height).isEqualTo(height);
+        assertThat(configuration.supportedStreamConfigs[0].pixelFormat).isEqualTo(format);
+    }
+
+    private static VirtualCameraCallback createNoOpCallback() {
+        return new VirtualCameraCallback() {
+
+            @Override
+            public void onStreamConfigured(
+                    int streamId,
+                    @NonNull Surface surface,
+                    @NonNull VirtualCameraStreamConfig streamConfig) {}
+
+            @Override
+            public void onProcessCaptureRequest(
+                    int streamId, long frameId, @Nullable VirtualCameraMetadata metadata) {}
+
+            @Override
+            public void onStreamClosed(int streamId) {}
+        };
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
index a83caa4..dade3b9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java
@@ -636,6 +636,7 @@
         transition.collect(app);
         controller.requestStartTransition(transition, null /* startTask */, remoteTransition,
                 null /* displayChange */);
+        assertTrue(delegateProc.isRunningRemoteTransition());
         testPlayer.startTransition();
         app.onStartingWindowDrawn();
         // The task appeared event should be deferred until transition ready.
@@ -643,7 +644,6 @@
         testPlayer.onTransactionReady(app.getSyncTransaction());
         assertTrue(task.taskAppearedReady());
         assertTrue(playerProc.isRunningRemoteTransition());
-        assertTrue(delegateProc.isRunningRemoteTransition());
         assertTrue(controller.mRemotePlayer.reportRunning(delegateProc.getThread()));
         assertTrue(app.isVisible());