Merge "[PM] Send ACTION_PACKAGE_CHANGED when the app is installed" into main
diff --git a/core/api/current.txt b/core/api/current.txt
index b8c2d90..1cfc025 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -9242,140 +9242,140 @@
 
 package android.app.slice {
 
-  public final class Slice implements android.os.Parcelable {
-    ctor protected Slice(android.os.Parcel);
-    method public int describeContents();
-    method public java.util.List<java.lang.String> getHints();
-    method public java.util.List<android.app.slice.SliceItem> getItems();
-    method @Nullable public android.app.slice.SliceSpec getSpec();
-    method public android.net.Uri getUri();
-    method public boolean isCallerNeeded();
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.app.slice.Slice> CREATOR;
-    field public static final String EXTRA_RANGE_VALUE = "android.app.slice.extra.RANGE_VALUE";
-    field public static final String EXTRA_TOGGLE_STATE = "android.app.slice.extra.TOGGLE_STATE";
-    field public static final String HINT_ACTIONS = "actions";
-    field public static final String HINT_ERROR = "error";
-    field public static final String HINT_HORIZONTAL = "horizontal";
-    field public static final String HINT_KEYWORDS = "keywords";
-    field public static final String HINT_LARGE = "large";
-    field public static final String HINT_LAST_UPDATED = "last_updated";
-    field public static final String HINT_LIST = "list";
-    field public static final String HINT_LIST_ITEM = "list_item";
-    field public static final String HINT_NO_TINT = "no_tint";
-    field public static final String HINT_PARTIAL = "partial";
-    field public static final String HINT_PERMISSION_REQUEST = "permission_request";
-    field public static final String HINT_SEE_MORE = "see_more";
-    field public static final String HINT_SELECTED = "selected";
-    field public static final String HINT_SHORTCUT = "shortcut";
-    field public static final String HINT_SUMMARY = "summary";
-    field public static final String HINT_TITLE = "title";
-    field public static final String HINT_TTL = "ttl";
-    field public static final String SUBTYPE_COLOR = "color";
-    field public static final String SUBTYPE_CONTENT_DESCRIPTION = "content_description";
-    field public static final String SUBTYPE_LAYOUT_DIRECTION = "layout_direction";
-    field public static final String SUBTYPE_MAX = "max";
-    field public static final String SUBTYPE_MESSAGE = "message";
-    field public static final String SUBTYPE_MILLIS = "millis";
-    field public static final String SUBTYPE_PRIORITY = "priority";
-    field public static final String SUBTYPE_RANGE = "range";
-    field public static final String SUBTYPE_SOURCE = "source";
-    field public static final String SUBTYPE_TOGGLE = "toggle";
-    field public static final String SUBTYPE_VALUE = "value";
+  @Deprecated public final class Slice implements android.os.Parcelable {
+    ctor @Deprecated protected Slice(android.os.Parcel);
+    method @Deprecated public int describeContents();
+    method @Deprecated public java.util.List<java.lang.String> getHints();
+    method @Deprecated public java.util.List<android.app.slice.SliceItem> getItems();
+    method @Deprecated @Nullable public android.app.slice.SliceSpec getSpec();
+    method @Deprecated public android.net.Uri getUri();
+    method @Deprecated public boolean isCallerNeeded();
+    method @Deprecated public void writeToParcel(android.os.Parcel, int);
+    field @Deprecated @NonNull public static final android.os.Parcelable.Creator<android.app.slice.Slice> CREATOR;
+    field @Deprecated public static final String EXTRA_RANGE_VALUE = "android.app.slice.extra.RANGE_VALUE";
+    field @Deprecated public static final String EXTRA_TOGGLE_STATE = "android.app.slice.extra.TOGGLE_STATE";
+    field @Deprecated public static final String HINT_ACTIONS = "actions";
+    field @Deprecated public static final String HINT_ERROR = "error";
+    field @Deprecated public static final String HINT_HORIZONTAL = "horizontal";
+    field @Deprecated public static final String HINT_KEYWORDS = "keywords";
+    field @Deprecated public static final String HINT_LARGE = "large";
+    field @Deprecated public static final String HINT_LAST_UPDATED = "last_updated";
+    field @Deprecated public static final String HINT_LIST = "list";
+    field @Deprecated public static final String HINT_LIST_ITEM = "list_item";
+    field @Deprecated public static final String HINT_NO_TINT = "no_tint";
+    field @Deprecated public static final String HINT_PARTIAL = "partial";
+    field @Deprecated public static final String HINT_PERMISSION_REQUEST = "permission_request";
+    field @Deprecated public static final String HINT_SEE_MORE = "see_more";
+    field @Deprecated public static final String HINT_SELECTED = "selected";
+    field @Deprecated public static final String HINT_SHORTCUT = "shortcut";
+    field @Deprecated public static final String HINT_SUMMARY = "summary";
+    field @Deprecated public static final String HINT_TITLE = "title";
+    field @Deprecated public static final String HINT_TTL = "ttl";
+    field @Deprecated public static final String SUBTYPE_COLOR = "color";
+    field @Deprecated public static final String SUBTYPE_CONTENT_DESCRIPTION = "content_description";
+    field @Deprecated public static final String SUBTYPE_LAYOUT_DIRECTION = "layout_direction";
+    field @Deprecated public static final String SUBTYPE_MAX = "max";
+    field @Deprecated public static final String SUBTYPE_MESSAGE = "message";
+    field @Deprecated public static final String SUBTYPE_MILLIS = "millis";
+    field @Deprecated public static final String SUBTYPE_PRIORITY = "priority";
+    field @Deprecated public static final String SUBTYPE_RANGE = "range";
+    field @Deprecated public static final String SUBTYPE_SOURCE = "source";
+    field @Deprecated public static final String SUBTYPE_TOGGLE = "toggle";
+    field @Deprecated public static final String SUBTYPE_VALUE = "value";
   }
 
-  public static class Slice.Builder {
-    ctor public Slice.Builder(@NonNull android.net.Uri, android.app.slice.SliceSpec);
-    ctor public Slice.Builder(@NonNull android.app.slice.Slice.Builder);
-    method public android.app.slice.Slice.Builder addAction(@NonNull android.app.PendingIntent, @NonNull android.app.slice.Slice, @Nullable String);
-    method public android.app.slice.Slice.Builder addBundle(android.os.Bundle, @Nullable String, java.util.List<java.lang.String>);
-    method public android.app.slice.Slice.Builder addHints(java.util.List<java.lang.String>);
-    method public android.app.slice.Slice.Builder addIcon(android.graphics.drawable.Icon, @Nullable String, java.util.List<java.lang.String>);
-    method public android.app.slice.Slice.Builder addInt(int, @Nullable String, java.util.List<java.lang.String>);
-    method public android.app.slice.Slice.Builder addLong(long, @Nullable String, java.util.List<java.lang.String>);
-    method public android.app.slice.Slice.Builder addRemoteInput(android.app.RemoteInput, @Nullable String, java.util.List<java.lang.String>);
-    method public android.app.slice.Slice.Builder addSubSlice(@NonNull android.app.slice.Slice, @Nullable String);
-    method public android.app.slice.Slice.Builder addText(CharSequence, @Nullable String, java.util.List<java.lang.String>);
-    method public android.app.slice.Slice build();
-    method public android.app.slice.Slice.Builder setCallerNeeded(boolean);
+  @Deprecated public static class Slice.Builder {
+    ctor @Deprecated public Slice.Builder(@NonNull android.net.Uri, android.app.slice.SliceSpec);
+    ctor @Deprecated public Slice.Builder(@NonNull android.app.slice.Slice.Builder);
+    method @Deprecated public android.app.slice.Slice.Builder addAction(@NonNull android.app.PendingIntent, @NonNull android.app.slice.Slice, @Nullable String);
+    method @Deprecated public android.app.slice.Slice.Builder addBundle(android.os.Bundle, @Nullable String, java.util.List<java.lang.String>);
+    method @Deprecated public android.app.slice.Slice.Builder addHints(java.util.List<java.lang.String>);
+    method @Deprecated public android.app.slice.Slice.Builder addIcon(android.graphics.drawable.Icon, @Nullable String, java.util.List<java.lang.String>);
+    method @Deprecated public android.app.slice.Slice.Builder addInt(int, @Nullable String, java.util.List<java.lang.String>);
+    method @Deprecated public android.app.slice.Slice.Builder addLong(long, @Nullable String, java.util.List<java.lang.String>);
+    method @Deprecated public android.app.slice.Slice.Builder addRemoteInput(android.app.RemoteInput, @Nullable String, java.util.List<java.lang.String>);
+    method @Deprecated public android.app.slice.Slice.Builder addSubSlice(@NonNull android.app.slice.Slice, @Nullable String);
+    method @Deprecated public android.app.slice.Slice.Builder addText(CharSequence, @Nullable String, java.util.List<java.lang.String>);
+    method @Deprecated public android.app.slice.Slice build();
+    method @Deprecated public android.app.slice.Slice.Builder setCallerNeeded(boolean);
   }
 
-  public final class SliceItem implements android.os.Parcelable {
-    method public int describeContents();
-    method public android.app.PendingIntent getAction();
-    method public android.os.Bundle getBundle();
-    method public String getFormat();
-    method @NonNull public java.util.List<java.lang.String> getHints();
-    method public android.graphics.drawable.Icon getIcon();
-    method public int getInt();
-    method public long getLong();
-    method public android.app.RemoteInput getRemoteInput();
-    method public android.app.slice.Slice getSlice();
-    method public String getSubType();
-    method public CharSequence getText();
-    method public boolean hasHint(String);
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.app.slice.SliceItem> CREATOR;
-    field public static final String FORMAT_ACTION = "action";
-    field public static final String FORMAT_BUNDLE = "bundle";
-    field public static final String FORMAT_IMAGE = "image";
-    field public static final String FORMAT_INT = "int";
-    field public static final String FORMAT_LONG = "long";
-    field public static final String FORMAT_REMOTE_INPUT = "input";
-    field public static final String FORMAT_SLICE = "slice";
-    field public static final String FORMAT_TEXT = "text";
+  @Deprecated public final class SliceItem implements android.os.Parcelable {
+    method @Deprecated public int describeContents();
+    method @Deprecated public android.app.PendingIntent getAction();
+    method @Deprecated public android.os.Bundle getBundle();
+    method @Deprecated public String getFormat();
+    method @Deprecated @NonNull public java.util.List<java.lang.String> getHints();
+    method @Deprecated public android.graphics.drawable.Icon getIcon();
+    method @Deprecated public int getInt();
+    method @Deprecated public long getLong();
+    method @Deprecated public android.app.RemoteInput getRemoteInput();
+    method @Deprecated public android.app.slice.Slice getSlice();
+    method @Deprecated public String getSubType();
+    method @Deprecated public CharSequence getText();
+    method @Deprecated public boolean hasHint(String);
+    method @Deprecated public void writeToParcel(android.os.Parcel, int);
+    field @Deprecated @NonNull public static final android.os.Parcelable.Creator<android.app.slice.SliceItem> CREATOR;
+    field @Deprecated public static final String FORMAT_ACTION = "action";
+    field @Deprecated public static final String FORMAT_BUNDLE = "bundle";
+    field @Deprecated public static final String FORMAT_IMAGE = "image";
+    field @Deprecated public static final String FORMAT_INT = "int";
+    field @Deprecated public static final String FORMAT_LONG = "long";
+    field @Deprecated public static final String FORMAT_REMOTE_INPUT = "input";
+    field @Deprecated public static final String FORMAT_SLICE = "slice";
+    field @Deprecated public static final String FORMAT_TEXT = "text";
   }
 
-  public class SliceManager {
-    method @Nullable public android.app.slice.Slice bindSlice(@NonNull android.net.Uri, @NonNull java.util.Set<android.app.slice.SliceSpec>);
-    method @Nullable public android.app.slice.Slice bindSlice(@NonNull android.content.Intent, @NonNull java.util.Set<android.app.slice.SliceSpec>);
-    method public int checkSlicePermission(@NonNull android.net.Uri, int, int);
-    method @NonNull public java.util.List<android.net.Uri> getPinnedSlices();
-    method @NonNull public java.util.Set<android.app.slice.SliceSpec> getPinnedSpecs(android.net.Uri);
-    method @NonNull @WorkerThread public java.util.Collection<android.net.Uri> getSliceDescendants(@NonNull android.net.Uri);
-    method public void grantSlicePermission(@NonNull String, @NonNull android.net.Uri);
-    method @Nullable public android.net.Uri mapIntentToUri(@NonNull android.content.Intent);
-    method public void pinSlice(@NonNull android.net.Uri, @NonNull java.util.Set<android.app.slice.SliceSpec>);
-    method public void revokeSlicePermission(@NonNull String, @NonNull android.net.Uri);
-    method public void unpinSlice(@NonNull android.net.Uri);
-    field public static final String CATEGORY_SLICE = "android.app.slice.category.SLICE";
-    field public static final String SLICE_METADATA_KEY = "android.metadata.SLICE_URI";
+  @Deprecated public class SliceManager {
+    method @Deprecated @Nullable public android.app.slice.Slice bindSlice(@NonNull android.net.Uri, @NonNull java.util.Set<android.app.slice.SliceSpec>);
+    method @Deprecated @Nullable public android.app.slice.Slice bindSlice(@NonNull android.content.Intent, @NonNull java.util.Set<android.app.slice.SliceSpec>);
+    method @Deprecated public int checkSlicePermission(@NonNull android.net.Uri, int, int);
+    method @Deprecated @NonNull public java.util.List<android.net.Uri> getPinnedSlices();
+    method @Deprecated @NonNull public java.util.Set<android.app.slice.SliceSpec> getPinnedSpecs(android.net.Uri);
+    method @Deprecated @NonNull @WorkerThread public java.util.Collection<android.net.Uri> getSliceDescendants(@NonNull android.net.Uri);
+    method @Deprecated public void grantSlicePermission(@NonNull String, @NonNull android.net.Uri);
+    method @Deprecated @Nullable public android.net.Uri mapIntentToUri(@NonNull android.content.Intent);
+    method @Deprecated public void pinSlice(@NonNull android.net.Uri, @NonNull java.util.Set<android.app.slice.SliceSpec>);
+    method @Deprecated public void revokeSlicePermission(@NonNull String, @NonNull android.net.Uri);
+    method @Deprecated public void unpinSlice(@NonNull android.net.Uri);
+    field @Deprecated public static final String CATEGORY_SLICE = "android.app.slice.category.SLICE";
+    field @Deprecated public static final String SLICE_METADATA_KEY = "android.metadata.SLICE_URI";
   }
 
-  public class SliceMetrics {
-    ctor public SliceMetrics(@NonNull android.content.Context, @NonNull android.net.Uri);
-    method public void logHidden();
-    method public void logTouch(int, @NonNull android.net.Uri);
-    method public void logVisible();
+  @Deprecated public class SliceMetrics {
+    ctor @Deprecated public SliceMetrics(@NonNull android.content.Context, @NonNull android.net.Uri);
+    method @Deprecated public void logHidden();
+    method @Deprecated public void logTouch(int, @NonNull android.net.Uri);
+    method @Deprecated public void logVisible();
   }
 
-  public abstract class SliceProvider extends android.content.ContentProvider {
-    ctor public SliceProvider(@NonNull java.lang.String...);
-    ctor public SliceProvider();
-    method public final int delete(android.net.Uri, String, String[]);
-    method public final String getType(android.net.Uri);
-    method public final android.net.Uri insert(android.net.Uri, android.content.ContentValues);
-    method public android.app.slice.Slice onBindSlice(android.net.Uri, java.util.Set<android.app.slice.SliceSpec>);
-    method @NonNull public android.app.PendingIntent onCreatePermissionRequest(android.net.Uri);
-    method @NonNull public java.util.Collection<android.net.Uri> onGetSliceDescendants(@NonNull android.net.Uri);
-    method @NonNull public android.net.Uri onMapIntentToUri(android.content.Intent);
-    method public void onSlicePinned(android.net.Uri);
-    method public void onSliceUnpinned(android.net.Uri);
-    method public final android.database.Cursor query(android.net.Uri, String[], String, String[], String);
-    method public final android.database.Cursor query(android.net.Uri, String[], String, String[], String, android.os.CancellationSignal);
-    method public final android.database.Cursor query(android.net.Uri, String[], android.os.Bundle, android.os.CancellationSignal);
-    method public final int update(android.net.Uri, android.content.ContentValues, String, String[]);
-    field public static final String SLICE_TYPE = "vnd.android.slice";
+  @Deprecated public abstract class SliceProvider extends android.content.ContentProvider {
+    ctor @Deprecated public SliceProvider(@NonNull java.lang.String...);
+    ctor @Deprecated public SliceProvider();
+    method @Deprecated public final int delete(android.net.Uri, String, String[]);
+    method @Deprecated public final String getType(android.net.Uri);
+    method @Deprecated public final android.net.Uri insert(android.net.Uri, android.content.ContentValues);
+    method @Deprecated public android.app.slice.Slice onBindSlice(android.net.Uri, java.util.Set<android.app.slice.SliceSpec>);
+    method @Deprecated @NonNull public android.app.PendingIntent onCreatePermissionRequest(android.net.Uri);
+    method @Deprecated @NonNull public java.util.Collection<android.net.Uri> onGetSliceDescendants(@NonNull android.net.Uri);
+    method @Deprecated @NonNull public android.net.Uri onMapIntentToUri(android.content.Intent);
+    method @Deprecated public void onSlicePinned(android.net.Uri);
+    method @Deprecated public void onSliceUnpinned(android.net.Uri);
+    method @Deprecated public final android.database.Cursor query(android.net.Uri, String[], String, String[], String);
+    method @Deprecated public final android.database.Cursor query(android.net.Uri, String[], String, String[], String, android.os.CancellationSignal);
+    method @Deprecated public final android.database.Cursor query(android.net.Uri, String[], android.os.Bundle, android.os.CancellationSignal);
+    method @Deprecated public final int update(android.net.Uri, android.content.ContentValues, String, String[]);
+    field @Deprecated public static final String SLICE_TYPE = "vnd.android.slice";
   }
 
-  public final class SliceSpec implements android.os.Parcelable {
-    ctor public SliceSpec(@NonNull String, int);
-    method public boolean canRender(@NonNull android.app.slice.SliceSpec);
-    method public int describeContents();
-    method public int getRevision();
-    method public String getType();
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.app.slice.SliceSpec> CREATOR;
+  @Deprecated public final class SliceSpec implements android.os.Parcelable {
+    ctor @Deprecated public SliceSpec(@NonNull String, int);
+    method @Deprecated public boolean canRender(@NonNull android.app.slice.SliceSpec);
+    method @Deprecated public int describeContents();
+    method @Deprecated public int getRevision();
+    method @Deprecated public String getType();
+    method @Deprecated public void writeToParcel(android.os.Parcel, int);
+    field @Deprecated @NonNull public static final android.os.Parcelable.Creator<android.app.slice.SliceSpec> CREATOR;
   }
 
 }
@@ -17982,7 +17982,6 @@
     method @NonNull public android.graphics.fonts.FontFamily.Builder addFont(@NonNull android.graphics.fonts.Font);
     method @NonNull public android.graphics.fonts.FontFamily build();
     method @FlaggedApi("com.android.text.flags.new_fonts_fallback_xml") @Nullable public android.graphics.fonts.FontFamily buildVariableFamily();
-    method @FlaggedApi("com.android.text.flags.new_fonts_fallback_xml") public boolean canBuildVariableFamily();
   }
 
   public final class FontStyle {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 5547028..501203e 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -2186,7 +2186,7 @@
     field @NonNull public static final android.os.Parcelable.Creator<android.app.contextualsearch.CallbackToken> CREATOR;
   }
 
-  @FlaggedApi("android.app.contextualsearch.flags.enable_service") public class ContextualSearchManager {
+  @FlaggedApi("android.app.contextualsearch.flags.enable_service") public final class ContextualSearchManager {
     method @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXTUAL_SEARCH) public void startContextualSearch(int);
     field public static final String ACTION_LAUNCH_CONTEXTUAL_SEARCH = "android.app.contextualsearch.action.LAUNCH_CONTEXTUAL_SEARCH";
     field public static final int ENTRYPOINT_LONG_PRESS_HOME = 2; // 0x2
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index 0d46688..a73aa71 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -1527,6 +1527,13 @@
 
 package android.hardware {
 
+  @Deprecated public class Camera {
+    method @Deprecated public static void getCameraInfo(int, @NonNull android.content.Context, boolean, android.hardware.Camera.CameraInfo);
+    method @Deprecated public static int getNumberOfCameras(@NonNull android.content.Context);
+    method @Deprecated public static android.hardware.Camera open(int, @NonNull android.content.Context, boolean);
+    method @Deprecated public final void setPreviewSurface(android.view.Surface) throws java.io.IOException;
+  }
+
   public final class SensorPrivacyManager {
     method @FlaggedApi("com.android.internal.camera.flags.camera_privacy_allowlist") @RequiresPermission(android.Manifest.permission.MANAGE_SENSOR_PRIVACY) public void setCameraPrivacyAllowlist(@NonNull java.util.List<java.lang.String>);
     method @RequiresPermission(android.Manifest.permission.MANAGE_SENSOR_PRIVACY) public void setSensorPrivacy(int, int, boolean);
@@ -3116,6 +3123,14 @@
 
 }
 
+package android.service.ondeviceintelligence {
+
+  @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public abstract class OnDeviceIntelligenceService extends android.app.Service {
+    method public void onReady();
+  }
+
+}
+
 package android.service.quickaccesswallet {
 
   public interface QuickAccessWalletClient extends java.io.Closeable {
diff --git a/core/java/android/accessibilityservice/BrailleDisplayController.java b/core/java/android/accessibilityservice/BrailleDisplayController.java
index 5282aa3..7334676 100644
--- a/core/java/android/accessibilityservice/BrailleDisplayController.java
+++ b/core/java/android/accessibilityservice/BrailleDisplayController.java
@@ -305,4 +305,6 @@
     @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
     @TestApi
     String TEST_BRAILLE_DISPLAY_UNIQUE_ID = "UNIQUE_ID";
+    /** @hide */
+    String TEST_BRAILLE_DISPLAY_NAME = "NAME";
 }
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index 4bf8879..8913d6d 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -365,7 +365,7 @@
     @UnsupportedAppUsage
     private ContextImpl mSystemContext;
     @GuardedBy("this")
-    private SparseArray<ContextImpl> mDisplaySystemUiContexts;
+    private ArrayList<WeakReference<ContextImpl>> mDisplaySystemUiContexts;
 
     @UnsupportedAppUsage
     static volatile IPackageManager sPackageManager;
@@ -3033,14 +3033,19 @@
     public ContextImpl getSystemUiContext(int displayId) {
         synchronized (this) {
             if (mDisplaySystemUiContexts == null) {
-                mDisplaySystemUiContexts = new SparseArray<>();
+                mDisplaySystemUiContexts = new ArrayList<>();
             }
-            ContextImpl systemUiContext = mDisplaySystemUiContexts.get(displayId);
-            if (systemUiContext == null) {
-                systemUiContext = ContextImpl.createSystemUiContext(getSystemContext(), displayId);
-                mDisplaySystemUiContexts.put(displayId, systemUiContext);
+
+            mDisplaySystemUiContexts.removeIf(contextRef -> contextRef.refersTo(null));
+
+            ContextImpl context = getSystemUiContextNoCreateLocked(displayId);
+            if (context != null) {
+                return context;
             }
-            return systemUiContext;
+
+            context = ContextImpl.createSystemUiContext(getSystemContext(), displayId);
+            mDisplaySystemUiContexts.add(new WeakReference<>(context));
+            return context;
         }
     }
 
@@ -3048,18 +3053,30 @@
     @Override
     public ContextImpl getSystemUiContextNoCreate() {
         synchronized (this) {
-            if (mDisplaySystemUiContexts == null) return null;
-            return mDisplaySystemUiContexts.get(DEFAULT_DISPLAY);
+            if (mDisplaySystemUiContexts == null) {
+                return null;
+            }
+            return getSystemUiContextNoCreateLocked(DEFAULT_DISPLAY);
         }
     }
 
+    @GuardedBy("this")
+    @Nullable
+    private ContextImpl getSystemUiContextNoCreateLocked(int displayId) {
+        for (int i = 0; i < mDisplaySystemUiContexts.size(); i++) {
+            ContextImpl context = mDisplaySystemUiContexts.get(i).get();
+            if (context != null && context.getDisplayId() == displayId) {
+                return context;
+            }
+        }
+        return null;
+    }
+
     void onSystemUiContextCleanup(ContextImpl context) {
         synchronized (this) {
             if (mDisplaySystemUiContexts == null) return;
-            final int index = mDisplaySystemUiContexts.indexOfValue(context);
-            if (index >= 0) {
-                mDisplaySystemUiContexts.removeAt(index);
-            }
+            mDisplaySystemUiContexts.removeIf(
+                    contextRef -> contextRef.refersTo(null) || contextRef.refersTo(context));
         }
     }
 
diff --git a/core/java/android/app/OWNERS b/core/java/android/app/OWNERS
index 41b97d0..97c2e43 100644
--- a/core/java/android/app/OWNERS
+++ b/core/java/android/app/OWNERS
@@ -42,6 +42,7 @@
 # ActivityThread
 per-file ActivityThread.java = file:/services/core/java/com/android/server/am/OWNERS
 per-file ActivityThread.java = file:/services/core/java/com/android/server/wm/OWNERS
+per-file ActivityThread.java = file:RESOURCES_OWNERS
 
 # Alarm
 per-file *Alarm* = file:/apex/jobscheduler/OWNERS
diff --git a/core/java/android/app/RESOURCES_OWNERS b/core/java/android/app/RESOURCES_OWNERS
index 5582803..fe37c0c 100644
--- a/core/java/android/app/RESOURCES_OWNERS
+++ b/core/java/android/app/RESOURCES_OWNERS
@@ -1,3 +1,5 @@
-rtmitchell@google.com
-toddke@google.com
+zyy@google.com
+jakmcbane@google.com
+branliu@google.com
+markpun@google.com
 patb@google.com
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index 4fa45be..be0aaff 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -174,6 +174,17 @@
 }
 
 flag {
+  name: "copy_account_with_retry_enabled"
+  namespace: "enterprise"
+  description: "Retry copy and remove account from personal to work profile in case of failure"
+  bug: "329424312"
+  metadata {
+      purpose: PURPOSE_BUGFIX
+  }
+}
+
+
+flag {
   name: "esim_management_ux_enabled"
   namespace: "enterprise"
   description: "Enable UX changes for esim management"
diff --git a/core/java/android/app/contextualsearch/CallbackToken.java b/core/java/android/app/contextualsearch/CallbackToken.java
index 0bbd1e5..378193f 100644
--- a/core/java/android/app/contextualsearch/CallbackToken.java
+++ b/core/java/android/app/contextualsearch/CallbackToken.java
@@ -51,6 +51,7 @@
     private static final String TAG = CallbackToken.class.getSimpleName();
     private final IBinder mToken;
 
+    private final Object mLock = new Object();
     private boolean mTokenUsed = false;
 
     public CallbackToken() {
@@ -75,10 +76,14 @@
     public void getContextualSearchState(@NonNull @CallbackExecutor Executor executor,
             @NonNull OutcomeReceiver<ContextualSearchState, Throwable> callback) {
         if (DEBUG) Log.d(TAG, "getContextualSearchState for token:" + mToken);
-        if (mTokenUsed) {
-            callback.onError(new IllegalAccessException("Token already used."));
+        boolean tokenUsed;
+        synchronized (mLock) {
+            tokenUsed = markUsedLocked();
         }
-        mTokenUsed = true;
+        if (tokenUsed) {
+            callback.onError(new IllegalAccessException("Token already used."));
+            return;
+        }
         try {
             // Get the service from the system server.
             IBinder b = ServiceManager.getService(Context.CONTEXTUAL_SEARCH_SERVICE);
@@ -96,6 +101,12 @@
         }
     }
 
+    private boolean markUsedLocked() {
+        boolean oldValue = mTokenUsed;
+        mTokenUsed = true;
+        return oldValue;
+    }
+
     /**
      * Return the token necessary for validating the caller of {@link #getContextualSearchState}.
      *
diff --git a/core/java/android/app/contextualsearch/ContextualSearchManager.java b/core/java/android/app/contextualsearch/ContextualSearchManager.java
index a894a0e..c080a6b 100644
--- a/core/java/android/app/contextualsearch/ContextualSearchManager.java
+++ b/core/java/android/app/contextualsearch/ContextualSearchManager.java
@@ -43,7 +43,7 @@
  */
 @SystemApi
 @FlaggedApi(Flags.FLAG_ENABLE_SERVICE)
-public class ContextualSearchManager {
+public final class ContextualSearchManager {
 
     /**
      * Key to get the entrypoint from the extras of the activity launched by contextual search.
diff --git a/core/java/android/app/slice/Slice.java b/core/java/android/app/slice/Slice.java
index 475ee7a..5514868 100644
--- a/core/java/android/app/slice/Slice.java
+++ b/core/java/android/app/slice/Slice.java
@@ -41,7 +41,12 @@
  *
  * <p>They are constructed using {@link Builder} in a tree structure
  * that provides the OS some information about how the content should be displayed.
+ * @deprecated Slice framework has been deprecated, it will not receive any updates from
+ *          {@link android.os.Build.VANILLA_ICE_CREAM} and forward. If you are looking for a
+ *          framework that sends displayable data from one app to another, consider using
+ *          {@link android.app.appsearch.AppSearchManager}.
  */
+@Deprecated
 public final class Slice implements Parcelable {
 
     /**
@@ -338,7 +343,12 @@
 
     /**
      * A Builder used to construct {@link Slice}s
+     * @deprecated Slice framework has been deprecated, it will not receive any updates from
+     *          {@link android.os.Build.VANILLA_ICE_CREAM} and forward. If you are looking for a
+     *          framework that sends displayable data from one app to another, consider using
+     *          {@link android.app.appsearch.AppSearchManager}.
      */
+    @Deprecated
     public static class Builder {
 
         private final Uri mUri;
diff --git a/core/java/android/app/slice/SliceItem.java b/core/java/android/app/slice/SliceItem.java
index 2d6f4a6..27c726d 100644
--- a/core/java/android/app/slice/SliceItem.java
+++ b/core/java/android/app/slice/SliceItem.java
@@ -53,7 +53,12 @@
  * The hints that a {@link SliceItem} are a set of strings which annotate
  * the content. The hints that are guaranteed to be understood by the system
  * are defined on {@link Slice}.
+ * @deprecated Slice framework has been deprecated, it will not receive any updates from
+ *          {@link android.os.Build.VANILLA_ICE_CREAM} and forward. If you are looking for a
+ *          framework that sends displayable data from one app to another, consider using
+ *          {@link android.app.appsearch.AppSearchManager}.
  */
+@Deprecated
 public final class SliceItem implements Parcelable {
 
     private static final String TAG = "SliceItem";
diff --git a/core/java/android/app/slice/SliceManager.java b/core/java/android/app/slice/SliceManager.java
index 2e179d0..4fc2a0c 100644
--- a/core/java/android/app/slice/SliceManager.java
+++ b/core/java/android/app/slice/SliceManager.java
@@ -59,7 +59,12 @@
  * Class to handle interactions with {@link Slice}s.
  * <p>
  * The SliceManager manages permissions and pinned state for slices.
+ * @deprecated Slice framework has been deprecated, it will not receive any updates from
+ *          {@link android.os.Build.VANILLA_ICE_CREAM} and forward. If you are looking for a
+ *          framework that sends displayable data from one app to another, consider using
+ *          {@link android.app.appsearch.AppSearchManager}.
  */
+@Deprecated
 @SystemService(Context.SLICE_SERVICE)
 public class SliceManager {
 
diff --git a/core/java/android/app/slice/SliceMetrics.java b/core/java/android/app/slice/SliceMetrics.java
index 746beaf..abfe3a2f 100644
--- a/core/java/android/app/slice/SliceMetrics.java
+++ b/core/java/android/app/slice/SliceMetrics.java
@@ -31,7 +31,12 @@
  * not need to reference this class.
  *
  * @see androidx.slice.widget.SliceView
+ * @deprecated Slice framework has been deprecated, it will not receive any updates from
+ *          {@link android.os.Build.VANILLA_ICE_CREAM} and forward. If you are looking for a
+ *          framework that sends displayable data from one app to another, consider using
+ *          {@link android.app.appsearch.AppSearchManager}.
  */
+@Deprecated
 public class SliceMetrics {
 
     private static final String TAG = "SliceMetrics";
diff --git a/core/java/android/app/slice/SliceProvider.java b/core/java/android/app/slice/SliceProvider.java
index 42c3aa6..4374550 100644
--- a/core/java/android/app/slice/SliceProvider.java
+++ b/core/java/android/app/slice/SliceProvider.java
@@ -91,7 +91,12 @@
  * </pre>
  *
  * @see Slice
+ * @deprecated Slice framework has been deprecated, it will not receive any updates from
+ *          {@link android.os.Build.VANILLA_ICE_CREAM} and forward. If you are looking for a
+ *          framework that sends displayable data from one app to another, consider using
+ *          {@link android.app.appsearch.AppSearchManager}.
  */
+@Deprecated
 public abstract class SliceProvider extends ContentProvider {
     /**
      * This is the Android platform's MIME type for a URI
diff --git a/core/java/android/app/slice/SliceSpec.java b/core/java/android/app/slice/SliceSpec.java
index a332349..078f552 100644
--- a/core/java/android/app/slice/SliceSpec.java
+++ b/core/java/android/app/slice/SliceSpec.java
@@ -38,7 +38,12 @@
  *
  * @see Slice
  * @see SliceProvider#onBindSlice(Uri, Set)
+ * @deprecated Slice framework has been deprecated, it will not receive any updates from
+ *          {@link android.os.Build.VANILLA_ICE_CREAM} and forward. If you are looking for a
+ *          framework that sends displayable data from one app to another, consider using
+ *          {@link android.app.appsearch.AppSearchManager}.
  */
+@Deprecated
 public final class SliceSpec implements Parcelable {
 
     private final String mType;
diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java
index 1c36a88..a9f70c9 100644
--- a/core/java/android/hardware/Camera.java
+++ b/core/java/android/hardware/Camera.java
@@ -16,14 +16,21 @@
 
 package android.hardware;
 
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA;
+import static android.content.Context.DEVICE_ID_DEFAULT;
 import static android.system.OsConstants.EACCES;
 import static android.system.OsConstants.ENODEV;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.SdkConstant;
 import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SuppressLint;
+import android.annotation.TestApi;
 import android.app.ActivityThread;
 import android.app.AppOpsManager;
+import android.companion.virtual.VirtualDeviceManager;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.graphics.ImageFormat;
@@ -56,6 +63,7 @@
 import java.util.ArrayList;
 import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * The Camera class is used to set image capture settings, start/stop preview,
@@ -271,7 +279,24 @@
      * @return total number of accessible camera devices, or 0 if there are no
      *   cameras or an error was encountered enumerating them.
      */
-    public native static int getNumberOfCameras();
+    public static int getNumberOfCameras() {
+        return getNumberOfCameras(ActivityThread.currentApplication().getApplicationContext());
+    }
+
+    /**
+     * Returns the number of physical cameras available on this device for the given context.
+     * The return value of this method might change dynamically if the device supports external
+     * cameras and an external camera is connected or disconnected.
+     *
+     * @hide
+     */
+    @SuppressLint("UnflaggedApi") // @TestApi without associated feature.
+    @TestApi
+    public static int getNumberOfCameras(@NonNull Context context) {
+        return _getNumberOfCameras(context.getDeviceId(), getDevicePolicyFromContext(context));
+    }
+
+    private static native int _getNumberOfCameras(int deviceId, int devicePolicy);
 
     /**
      * Returns the information about a particular camera.
@@ -282,10 +307,22 @@
      *    low-level failure).
      */
     public static void getCameraInfo(int cameraId, CameraInfo cameraInfo) {
-        boolean overrideToPortrait = CameraManager.shouldOverrideToPortrait(
-                ActivityThread.currentApplication().getApplicationContext());
+        Context context = ActivityThread.currentApplication().getApplicationContext();
+        boolean overrideToPortrait = CameraManager.shouldOverrideToPortrait(context);
+        getCameraInfo(cameraId, context, overrideToPortrait, cameraInfo);
+    }
 
-        _getCameraInfo(cameraId, overrideToPortrait, cameraInfo);
+    /**
+     * Returns the information about a particular camera for the given context.
+     *
+     * @hide
+     */
+    @SuppressLint("UnflaggedApi") // @TestApi without associated feature.
+    @TestApi
+    public static void getCameraInfo(int cameraId, @NonNull Context context,
+            boolean overrideToPortrait, CameraInfo cameraInfo) {
+        _getCameraInfo(cameraId, overrideToPortrait, context.getDeviceId(),
+                getDevicePolicyFromContext(context), cameraInfo);
         IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
         IAudioService audioService = IAudioService.Stub.asInterface(b);
         try {
@@ -298,8 +335,20 @@
             Log.e(TAG, "Audio service is unavailable for queries");
         }
     }
+
     private native static void _getCameraInfo(int cameraId, boolean overrideToPortrait,
-            CameraInfo cameraInfo);
+            int deviceId, int devicePolicy, CameraInfo cameraInfo);
+
+    private static int getDevicePolicyFromContext(Context context) {
+        if (context.getDeviceId() == DEVICE_ID_DEFAULT
+                || !android.companion.virtual.flags.Flags.virtualCamera()) {
+            return DEVICE_POLICY_DEFAULT;
+        }
+
+        VirtualDeviceManager virtualDeviceManager =
+                context.getSystemService(VirtualDeviceManager.class);
+        return virtualDeviceManager.getDevicePolicy(context.getDeviceId(), POLICY_TYPE_CAMERA);
+    }
 
     /**
      * Information about a camera
@@ -359,7 +408,7 @@
          * when {@link Camera#takePicture takePicture} is called.</p>
          */
         public boolean canDisableShutterSound;
-    };
+    }
 
     /**
      * Creates a new Camera object to access a particular hardware camera. If
@@ -391,7 +440,20 @@
      * @see android.app.admin.DevicePolicyManager#getCameraDisabled(android.content.ComponentName)
      */
     public static Camera open(int cameraId) {
-        return new Camera(cameraId);
+        Context context = ActivityThread.currentApplication().getApplicationContext();
+        boolean overrideToPortrait = CameraManager.shouldOverrideToPortrait(context);
+        return open(cameraId, context, overrideToPortrait);
+    }
+
+    /**
+     * Creates a new Camera object for a given camera id for the given context.
+     *
+     * @hide
+     */
+    @SuppressLint("UnflaggedApi") // @TestApi without associated feature.
+    @TestApi
+    public static Camera open(int cameraId, @NonNull Context context, boolean overrideToPortrait) {
+        return new Camera(cameraId, context, overrideToPortrait);
     }
 
     /**
@@ -409,7 +471,7 @@
         for (int i = 0; i < numberOfCameras; i++) {
             getCameraInfo(i, cameraInfo);
             if (cameraInfo.facing == CameraInfo.CAMERA_FACING_BACK) {
-                return new Camera(i);
+                return open(i);
             }
         }
         return null;
@@ -459,10 +521,10 @@
             throw new IllegalArgumentException("Unsupported HAL version " + halVersion);
         }
 
-        return new Camera(cameraId);
+        return open(cameraId);
     }
 
-    private int cameraInit(int cameraId) {
+    private int cameraInit(int cameraId, Context context, boolean overrideToPortrait) {
         mShutterCallback = null;
         mRawImageCallback = null;
         mJpegCallback = null;
@@ -480,11 +542,10 @@
             mEventHandler = null;
         }
 
-        boolean overrideToPortrait = CameraManager.shouldOverrideToPortrait(
-                ActivityThread.currentApplication().getApplicationContext());
         boolean forceSlowJpegMode = shouldForceSlowJpegMode();
-        return native_setup(new WeakReference<Camera>(this), cameraId,
-                ActivityThread.currentOpPackageName(), overrideToPortrait, forceSlowJpegMode);
+        return native_setup(new WeakReference<>(this), cameraId,
+                ActivityThread.currentOpPackageName(), overrideToPortrait, forceSlowJpegMode,
+                context.getDeviceId(), getDevicePolicyFromContext(context));
     }
 
     private boolean shouldForceSlowJpegMode() {
@@ -501,8 +562,9 @@
     }
 
     /** used by Camera#open, Camera#open(int) */
-    Camera(int cameraId) {
-        int err = cameraInit(cameraId);
+    Camera(int cameraId, @NonNull Context context, boolean overrideToPortrait) {
+        Objects.requireNonNull(context);
+        int err = cameraInit(cameraId, context, overrideToPortrait);
         if (checkInitErrors(err)) {
             if (err == -EACCES) {
                 throw new RuntimeException("Fail to connect to camera service");
@@ -515,7 +577,6 @@
         initAppOps();
     }
 
-
     /**
      * @hide
      */
@@ -568,11 +629,10 @@
 
     @UnsupportedAppUsage
     private native int native_setup(Object cameraThis, int cameraId, String packageName,
-            boolean overrideToPortrait, boolean forceSlowJpegMode);
+            boolean overrideToPortrait, boolean forceSlowJpegMode, int deviceId, int devicePolicy);
 
     private native final void native_release();
 
-
     /**
      * Disconnects and releases the Camera object resources.
      *
@@ -672,13 +732,15 @@
         if (holder != null) {
             setPreviewSurface(holder.getSurface());
         } else {
-            setPreviewSurface((Surface)null);
+            setPreviewSurface(null);
         }
     }
 
     /**
      * @hide
      */
+    @SuppressLint("UnflaggedApi") // @TestApi without associated feature.
+    @TestApi
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
     public native final void setPreviewSurface(Surface surface) throws IOException;
 
diff --git a/core/java/android/hardware/CameraStatus.java b/core/java/android/hardware/CameraStatus.java
index fa35efb..0198421 100644
--- a/core/java/android/hardware/CameraStatus.java
+++ b/core/java/android/hardware/CameraStatus.java
@@ -16,6 +16,7 @@
 
 package android.hardware;
 
+import android.annotation.NonNull;
 import android.os.Parcel;
 import android.os.Parcelable;
 
@@ -23,7 +24,7 @@
  * Status information about a camera.
  *
  * Contains the name of the camera device, and its current status, one of the
- * ICameraServiceListener.STATUS_ values.
+ * ICameraServiceListener.STATUS_* values.
  *
  * @hide
  */
@@ -32,6 +33,7 @@
     public int status;
     public String[] unavailablePhysicalCameras;
     public String clientPackage;
+    public int deviceId;
 
     @Override
     public int describeContents() {
@@ -44,6 +46,7 @@
         out.writeInt(status);
         out.writeStringArray(unavailablePhysicalCameras);
         out.writeString(clientPackage);
+        out.writeInt(deviceId);
     }
 
     public void readFromParcel(Parcel in) {
@@ -51,21 +54,22 @@
         status = in.readInt();
         unavailablePhysicalCameras = in.readStringArray();
         clientPackage = in.readString();
+        deviceId = in.readInt();
     }
 
-    public static final @android.annotation.NonNull Parcelable.Creator<CameraStatus> CREATOR =
-            new Parcelable.Creator<CameraStatus>() {
-        @Override
-        public CameraStatus createFromParcel(Parcel in) {
-            CameraStatus status = new CameraStatus();
-            status.readFromParcel(in);
+    @NonNull
+    public static final Parcelable.Creator<CameraStatus> CREATOR =
+            new Parcelable.Creator<>() {
+                @Override
+                public CameraStatus createFromParcel(Parcel in) {
+                    CameraStatus status = new CameraStatus();
+                    status.readFromParcel(in);
+                    return status;
+                }
 
-            return status;
-        }
-
-        @Override
-        public CameraStatus[] newArray(int size) {
-            return new CameraStatus[size];
-        }
-    };
+                @Override
+                public CameraStatus[] newArray(int size) {
+                    return new CameraStatus[size];
+                }
+            };
 }
diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java
index 238c381..21c7004 100644
--- a/core/java/android/hardware/camera2/CameraCharacteristics.java
+++ b/core/java/android/hardware/camera2/CameraCharacteristics.java
@@ -19,6 +19,8 @@
 import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.companion.virtual.VirtualDeviceManager;
+import android.companion.virtual.camera.VirtualCameraConfig;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.hardware.camera2.impl.CameraMetadataNative;
 import android.hardware.camera2.impl.ExtensionKey;
@@ -5329,6 +5331,19 @@
             new Key<Integer>("android.info.sessionConfigurationQueryVersion", int.class);
 
     /**
+     * <p>Id of the device that owns this camera.</p>
+     * <p>In case of a virtual camera, this would be the id of the virtual device
+     * owning the camera. For any other camera, this key would not be present.
+     * Callers should assume {@link android.content.Context#DEVICE_ID_DEFAULT}
+     * in case this key is not present.</p>
+     * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p>
+     *  @see VirtualDeviceManager.VirtualDevice#createVirtualCamera(VirtualCameraConfig)
+     * @hide
+     */
+    public static final Key<Integer> INFO_DEVICE_ID =
+            new Key<Integer>("android.info.deviceId", int.class);
+
+    /**
      * <p>The maximum number of frames that can occur after a request
      * (different than the previous) has been submitted, and before the
      * result's state becomes synchronized.</p>
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index b43a900..8feb133 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -16,6 +16,10 @@
 
 package android.hardware.camera2;
 
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
+import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA;
+import static android.content.Context.DEVICE_ID_DEFAULT;
+
 import android.annotation.CallbackExecutor;
 import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
@@ -25,6 +29,7 @@
 import android.annotation.SystemService;
 import android.annotation.TestApi;
 import android.app.compat.CompatChanges;
+import android.companion.virtual.VirtualDeviceManager;
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.EnabledSince;
 import android.compat.annotation.Overridable;
@@ -74,7 +79,9 @@
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
@@ -105,8 +112,6 @@
     private static final int CAMERA_TYPE_BACKWARD_COMPATIBLE = 0;
     private static final int CAMERA_TYPE_ALL = 1;
 
-    private ArrayList<String> mDeviceIdList;
-
     private final Context mContext;
     private final Object mLock = new Object();
 
@@ -114,6 +119,8 @@
             "android.permission.CAMERA_OPEN_CLOSE_LISTENER";
     private final boolean mHasOpenCloseListenerPermission;
 
+    private VirtualDeviceManager mVirtualDeviceManager;
+
     /**
      * Force camera output to be rotated to portrait orientation on landscape cameras.
      * Many apps do not handle this situation and display stretched images otherwise.
@@ -240,7 +247,8 @@
      */
     @NonNull
     public String[] getCameraIdList() throws CameraAccessException {
-        return CameraManagerGlobal.get().getCameraIdList();
+        return CameraManagerGlobal.get().getCameraIdList(mContext.getDeviceId(),
+                getDevicePolicyFromContext(mContext));
     }
 
     /**
@@ -251,11 +259,13 @@
      * adopt(drop)ShellPermissionIdentity() and effectively change their permissions). This call
      * affects the camera ids returned by getCameraIdList() as well. Tests which do adopt shell
      * permission identity should not mix getCameraIdList() and getCameraListNoLazyCalls().
+     *
+     * @hide
      */
-    /** @hide */
     @TestApi
     public String[] getCameraIdListNoLazy() throws CameraAccessException {
-        return CameraManagerGlobal.get().getCameraIdListNoLazy();
+        return CameraManagerGlobal.get().getCameraIdListNoLazy(mContext.getDeviceId(),
+                getDevicePolicyFromContext(mContext));
     }
 
     /**
@@ -381,7 +391,8 @@
     public void registerAvailabilityCallback(@NonNull AvailabilityCallback callback,
             @Nullable Handler handler) {
         CameraManagerGlobal.get().registerAvailabilityCallback(callback,
-                CameraDeviceImpl.checkAndWrapHandler(handler), mHasOpenCloseListenerPermission);
+                CameraDeviceImpl.checkAndWrapHandler(handler), mHasOpenCloseListenerPermission,
+                mContext.getDeviceId(), getDevicePolicyFromContext(mContext));
     }
 
     /**
@@ -420,7 +431,8 @@
             throw new IllegalArgumentException("executor was null");
         }
         CameraManagerGlobal.get().registerAvailabilityCallback(callback, executor,
-                mHasOpenCloseListenerPermission);
+                mHasOpenCloseListenerPermission, mContext.getDeviceId(),
+                getDevicePolicyFromContext(mContext));
     }
 
     /**
@@ -459,7 +471,8 @@
      */
     public void registerTorchCallback(@NonNull TorchCallback callback, @Nullable Handler handler) {
         CameraManagerGlobal.get().registerTorchCallback(callback,
-                CameraDeviceImpl.checkAndWrapHandler(handler));
+                CameraDeviceImpl.checkAndWrapHandler(handler), mContext.getDeviceId(),
+                getDevicePolicyFromContext(mContext));
     }
 
     /**
@@ -480,7 +493,8 @@
         if (executor == null) {
             throw new IllegalArgumentException("executor was null");
         }
-        CameraManagerGlobal.get().registerTorchCallback(callback, executor);
+        CameraManagerGlobal.get().registerTorchCallback(callback, executor, mContext.getDeviceId(),
+                getDevicePolicyFromContext(mContext));
     }
 
     /**
@@ -495,6 +509,19 @@
         CameraManagerGlobal.get().unregisterTorchCallback(callback);
     }
 
+    /** @hide */
+    public int getDevicePolicyFromContext(@NonNull Context context) {
+        if (context.getDeviceId() == DEVICE_ID_DEFAULT
+                || !android.companion.virtual.flags.Flags.virtualCamera()) {
+            return DEVICE_POLICY_DEFAULT;
+        }
+
+        if (mVirtualDeviceManager == null) {
+            mVirtualDeviceManager = context.getSystemService(VirtualDeviceManager.class);
+        }
+        return mVirtualDeviceManager.getDevicePolicy(context.getDeviceId(), POLICY_TYPE_CAMERA);
+    }
+
     // TODO(b/147726300): Investigate how to support foldables/multi-display devices.
     private Size getDisplaySize() {
         Size ret = new Size(0, 0);
@@ -566,7 +593,8 @@
                 CameraMetadataNative physicalCameraInfo =
                         cameraService.getCameraCharacteristics(physicalCameraId,
                                 mContext.getApplicationInfo().targetSdkVersion,
-                                /*overrideToPortrait*/false);
+                                /*overrideToPortrait*/ false, DEVICE_ID_DEFAULT,
+                                DEVICE_POLICY_DEFAULT);
                 StreamConfiguration[] configs = physicalCameraInfo.get(
                         CameraCharacteristics.
                                 SCALER_PHYSICAL_CAMERA_MULTI_RESOLUTION_STREAM_CONFIGURATIONS);
@@ -637,7 +665,7 @@
     @NonNull
     public CameraCharacteristics getCameraCharacteristics(@NonNull String cameraId,
             boolean overrideToPortrait) throws CameraAccessException {
-        CameraCharacteristics characteristics = null;
+        CameraCharacteristics characteristics;
         if (CameraManagerGlobal.sCameraServiceDisabled) {
             throw new IllegalArgumentException("No cameras available on device");
         }
@@ -651,7 +679,8 @@
                 Size displaySize = getDisplaySize();
 
                 CameraMetadataNative info = cameraService.getCameraCharacteristics(cameraId,
-                        mContext.getApplicationInfo().targetSdkVersion, overrideToPortrait);
+                        mContext.getApplicationInfo().targetSdkVersion, overrideToPortrait,
+                        mContext.getDeviceId(), getDevicePolicyFromContext(mContext));
                 try {
                     info.setCameraId(Integer.parseInt(cameraId));
                 } catch (NumberFormatException e) {
@@ -659,7 +688,8 @@
                 }
 
                 boolean hasConcurrentStreams =
-                        CameraManagerGlobal.get().cameraIdHasConcurrentStreamsLocked(cameraId);
+                        CameraManagerGlobal.get().cameraIdHasConcurrentStreamsLocked(cameraId,
+                                mContext.getDeviceId());
                 info.setHasMandatoryConcurrentStreams(hasConcurrentStreams);
                 info.setDisplaySize(displaySize);
 
@@ -759,7 +789,6 @@
         }
 
         return getCameraDeviceSetupUnsafe(cameraId);
-
     }
 
     /**
@@ -806,7 +835,8 @@
         }
 
         if (CameraManagerGlobal.sCameraServiceDisabled
-                || !Arrays.asList(CameraManagerGlobal.get().getCameraIdList()).contains(cameraId)) {
+                || !Arrays.asList(CameraManagerGlobal.get().getCameraIdList(mContext.getDeviceId(),
+                getDevicePolicyFromContext(mContext))).contains(cameraId)) {
             throw new IllegalArgumentException(
                     "Camera ID '" + cameraId + "' not available on device.");
         }
@@ -846,7 +876,6 @@
         Map<String, CameraCharacteristics> physicalIdsToChars =
                 getPhysicalIdToCharsMap(characteristics);
         synchronized (mLock) {
-
             ICameraDeviceUser cameraUser = null;
             CameraDevice.CameraDeviceSetup cameraDeviceSetup = null;
             if (Flags.cameraDeviceSetup()
@@ -855,7 +884,7 @@
             }
 
             android.hardware.camera2.impl.CameraDeviceImpl deviceImpl =
-                    new android.hardware.camera2.impl.CameraDeviceImpl(
+                    new CameraDeviceImpl(
                         cameraId,
                         callback,
                         executor,
@@ -876,7 +905,8 @@
                 cameraUser = cameraService.connectDevice(callbacks, cameraId,
                     mContext.getOpPackageName(), mContext.getAttributionTag(), uid,
                     oomScoreOffset, mContext.getApplicationInfo().targetSdkVersion,
-                    overrideToPortrait);
+                    overrideToPortrait, mContext.getDeviceId(),
+                        getDevicePolicyFromContext(mContext));
             } catch (ServiceSpecificException e) {
                 if (e.errorCode == ICameraService.ERROR_DEPRECATED_HAL) {
                     throw new AssertionError("Should've gone down the shim path");
@@ -1002,7 +1032,6 @@
     public void openCamera(@NonNull String cameraId,
             @NonNull final CameraDevice.StateCallback callback, @Nullable Handler handler)
             throws CameraAccessException {
-
         openCameraForUid(cameraId, callback, CameraDeviceImpl.checkAndWrapHandler(handler),
                 USE_CALLING_UID);
     }
@@ -1253,7 +1282,8 @@
         if (CameraManagerGlobal.sCameraServiceDisabled) {
             throw new IllegalArgumentException("No cameras available on device");
         }
-        CameraManagerGlobal.get().setTorchMode(cameraId, enabled);
+        CameraManagerGlobal.get().setTorchMode(cameraId, enabled, mContext.getDeviceId(),
+                getDevicePolicyFromContext(mContext));
     }
 
     /**
@@ -1316,7 +1346,8 @@
         if (CameraManagerGlobal.sCameraServiceDisabled) {
             throw new IllegalArgumentException("No camera available on device");
         }
-        CameraManagerGlobal.get().turnOnTorchWithStrengthLevel(cameraId, torchStrength);
+        CameraManagerGlobal.get().turnOnTorchWithStrengthLevel(cameraId, torchStrength,
+                mContext.getDeviceId(), getDevicePolicyFromContext(mContext));
     }
 
     /**
@@ -1342,7 +1373,8 @@
         if (CameraManagerGlobal.sCameraServiceDisabled) {
             throw new IllegalArgumentException("No camera available on device.");
         }
-        return CameraManagerGlobal.get().getTorchStrengthLevel(cameraId);
+        return CameraManagerGlobal.get().getTorchStrengthLevel(cameraId, mContext.getDeviceId(),
+                getDevicePolicyFromContext(mContext));
     }
 
     /**
@@ -1407,6 +1439,9 @@
      */
     public static abstract class AvailabilityCallback {
 
+        private int mDeviceId;
+        private int mDevicePolicy;
+
         /**
          * A new camera has become available to use.
          *
@@ -1645,6 +1680,10 @@
      * @see #registerTorchCallback
      */
     public static abstract class TorchCallback {
+
+        private int mDeviceId;
+        private int mDevicePolicy;
+
         /**
          * A camera's torch mode has become unavailable to set via {@link #setTorchMode}.
          *
@@ -1867,6 +1906,10 @@
             implements IBinder.DeathRecipient {
 
         private static final String TAG = "CameraManagerGlobal";
+
+        private static final String BACK_CAMERA_ID = "0";
+        private static final String FRONT_CAMERA_ID = "1";
+
         private final boolean DEBUG = false;
 
         private final int CAMERA_SERVICE_RECONNECT_DELAY_MS = 1000;
@@ -1882,29 +1925,26 @@
 
         private final ScheduledExecutorService mScheduler = Executors.newScheduledThreadPool(1);
         // Camera ID -> Status map
-        private final ArrayMap<String, Integer> mDeviceStatus = new ArrayMap<String, Integer>();
+        private final ArrayMap<DeviceCameraInfo, Integer> mDeviceStatus = new ArrayMap<>();
         // Camera ID -> (physical camera ID -> Status map)
-        private final ArrayMap<String, ArrayList<String>> mUnavailablePhysicalDevices =
-                new ArrayMap<String, ArrayList<String>>();
+        private final ArrayMap<DeviceCameraInfo, ArrayList<String>> mUnavailablePhysicalDevices =
+                new ArrayMap<>();
         // Opened Camera ID -> apk name map
-        private final ArrayMap<String, String> mOpenedDevices = new ArrayMap<String, String>();
+        private final ArrayMap<DeviceCameraInfo, String> mOpenedDevices = new ArrayMap<>();
 
-        private final Set<Set<String>> mConcurrentCameraIdCombinations =
-                new ArraySet<Set<String>>();
+        private final Set<Set<String>> mConcurrentCameraIdCombinations = new ArraySet<>();
 
         // Registered availability callbacks and their executors
-        private final ArrayMap<AvailabilityCallback, Executor> mCallbackMap =
-            new ArrayMap<AvailabilityCallback, Executor>();
+        private final ArrayMap<AvailabilityCallback, Executor> mCallbackMap = new ArrayMap<>();
 
         // torch client binder to set the torch mode with.
-        private Binder mTorchClientBinder = new Binder();
+        private final Binder mTorchClientBinder = new Binder();
 
         // Camera ID -> Torch status map
-        private final ArrayMap<String, Integer> mTorchStatus = new ArrayMap<String, Integer>();
+        private final ArrayMap<DeviceCameraInfo, Integer> mTorchStatus = new ArrayMap<>();
 
         // Registered torch callbacks and their executors
-        private final ArrayMap<TorchCallback, Executor> mTorchCallbackMap =
-                new ArrayMap<TorchCallback, Executor>();
+        private final ArrayMap<TorchCallback, Executor> mTorchCallbackMap = new ArrayMap<>();
 
         private final Object mLock = new Object();
 
@@ -2019,25 +2059,28 @@
 
             try {
                 CameraStatus[] cameraStatuses = cameraService.addListener(this);
-                for (CameraStatus c : cameraStatuses) {
-                    onStatusChangedLocked(c.status, c.cameraId);
+                for (CameraStatus cameraStatus : cameraStatuses) {
+                    DeviceCameraInfo info = new DeviceCameraInfo(cameraStatus.cameraId,
+                            cameraStatus.deviceId);
+                    onStatusChangedLocked(cameraStatus.status, info);
 
-                    if (c.unavailablePhysicalCameras != null) {
-                        for (String unavailPhysicalCamera : c.unavailablePhysicalCameras) {
+                    if (cameraStatus.unavailablePhysicalCameras != null) {
+                        for (String unavailablePhysicalCamera :
+                                cameraStatus.unavailablePhysicalCameras) {
                             onPhysicalCameraStatusChangedLocked(
                                     ICameraServiceListener.STATUS_NOT_PRESENT,
-                                    c.cameraId, unavailPhysicalCamera);
+                                    info, unavailablePhysicalCamera);
                         }
                     }
 
-                    if (mHasOpenCloseListenerPermission &&
-                            c.status == ICameraServiceListener.STATUS_NOT_AVAILABLE &&
-                            !c.clientPackage.isEmpty()) {
-                        onCameraOpenedLocked(c.cameraId, c.clientPackage);
+                    if (mHasOpenCloseListenerPermission
+                            && cameraStatus.status == ICameraServiceListener.STATUS_NOT_AVAILABLE
+                            && !cameraStatus.clientPackage.isEmpty()) {
+                        onCameraOpenedLocked(info, cameraStatus.clientPackage);
                     }
                 }
                 mCameraService = cameraService;
-            } catch(ServiceSpecificException e) {
+            } catch (ServiceSpecificException e) {
                 // Unexpected failure
                 throw new IllegalStateException("Failed to register a camera service listener", e);
             } catch (RemoteException e) {
@@ -2118,36 +2161,32 @@
             }
         }
 
-        private String[] extractCameraIdListLocked() {
-            String[] cameraIds = null;
-            int idCount = 0;
+        private String[] extractCameraIdListLocked(int deviceId, int devicePolicy) {
+            List<String> cameraIds = new ArrayList<>();
             for (int i = 0; i < mDeviceStatus.size(); i++) {
                 int status = mDeviceStatus.valueAt(i);
+                DeviceCameraInfo info = mDeviceStatus.keyAt(i);
                 if (status == ICameraServiceListener.STATUS_NOT_PRESENT
-                        || status == ICameraServiceListener.STATUS_ENUMERATING) continue;
-                idCount++;
+                        || status == ICameraServiceListener.STATUS_ENUMERATING
+                        || shouldHideCamera(deviceId, devicePolicy, info)) {
+                    continue;
+                }
+                cameraIds.add(info.mCameraId);
             }
-            cameraIds = new String[idCount];
-            idCount = 0;
-            for (int i = 0; i < mDeviceStatus.size(); i++) {
-                int status = mDeviceStatus.valueAt(i);
-                if (status == ICameraServiceListener.STATUS_NOT_PRESENT
-                        || status == ICameraServiceListener.STATUS_ENUMERATING) continue;
-                cameraIds[idCount] = mDeviceStatus.keyAt(i);
-                idCount++;
-            }
-            return cameraIds;
+            return cameraIds.toArray(new String[0]);
         }
 
         private Set<Set<String>> extractConcurrentCameraIdListLocked() {
-            Set<Set<String>> concurrentCameraIds = new ArraySet<Set<String>>();
+            Set<Set<String>> concurrentCameraIds = new ArraySet<>();
             for (Set<String> cameraIds : mConcurrentCameraIdCombinations) {
-                Set<String> extractedCameraIds = new ArraySet<String>();
+                Set<String> extractedCameraIds = new ArraySet<>();
                 for (String cameraId : cameraIds) {
+                    // TODO(b/291736219): This to be made device-aware.
+                    DeviceCameraInfo info = new DeviceCameraInfo(cameraId, DEVICE_ID_DEFAULT);
                     // if the camera id status is NOT_PRESENT or ENUMERATING; skip the device.
                     // TODO: Would a device status NOT_PRESENT ever be in the map ? it gets removed
                     // in the callback anyway.
-                    Integer status = mDeviceStatus.get(cameraId);
+                    Integer status = mDeviceStatus.get(info);
                     if (status == null) {
                         // camera id not present
                         continue;
@@ -2194,19 +2233,39 @@
                             return s1.compareTo(s2);
                         }
                     }});
-
         }
 
-        public static boolean cameraStatusesContains(CameraStatus[] cameraStatuses, String id) {
+        private boolean shouldHideCamera(int currentDeviceId, int devicePolicy,
+                DeviceCameraInfo info) {
+            if (!android.companion.virtualdevice.flags.Flags.cameraDeviceAwareness()) {
+                // Don't hide any cameras if the device-awareness feature flag is disabled.
+                return false;
+            }
+
+            if (devicePolicy == DEVICE_POLICY_DEFAULT && info.mDeviceId == DEVICE_ID_DEFAULT) {
+                // Don't hide default-device cameras for a default-policy virtual device.
+                return false;
+            }
+
+            // External cameras should never be hidden.
+            if (!info.mCameraId.equals(FRONT_CAMERA_ID) && !info.mCameraId.equals(BACK_CAMERA_ID)) {
+                return false;
+            }
+
+            return currentDeviceId != info.mDeviceId;
+        }
+
+        private static boolean cameraStatusesContains(CameraStatus[] cameraStatuses,
+                DeviceCameraInfo info) {
             for (CameraStatus c : cameraStatuses) {
-                if (c.cameraId.equals(id)) {
+                if (c.cameraId.equals(info.mCameraId) && c.deviceId == info.mDeviceId) {
                     return true;
                 }
             }
             return false;
         }
 
-        public String[] getCameraIdListNoLazy() {
+        public String[] getCameraIdListNoLazy(int deviceId, int devicePolicy) {
             if (sCameraServiceDisabled) {
                 return new String[] {};
             }
@@ -2214,30 +2273,32 @@
             CameraStatus[] cameraStatuses;
             ICameraServiceListener.Stub testListener = new ICameraServiceListener.Stub() {
                 @Override
-                public void onStatusChanged(int status, String id) throws RemoteException {
+                public void onStatusChanged(int status, String id, int deviceId)
+                        throws RemoteException {
                 }
                 @Override
                 public void onPhysicalCameraStatusChanged(int status,
-                        String id, String physicalId) throws RemoteException {
+                        String id, String physicalId, int deviceId) throws RemoteException {
                 }
                 @Override
-                public void onTorchStatusChanged(int status, String id) throws RemoteException {
-                }
-                @Override
-                public void onTorchStrengthLevelChanged(String id, int newStrengthLevel)
+                public void onTorchStatusChanged(int status, String id, int deviceId)
                         throws RemoteException {
                 }
                 @Override
+                public void onTorchStrengthLevelChanged(String id, int newStrengthLevel,
+                        int deviceId) throws RemoteException {
+                }
+                @Override
                 public void onCameraAccessPrioritiesChanged() {
                 }
                 @Override
-                public void onCameraOpened(String id, String clientPackageId) {
+                public void onCameraOpened(String id, String clientPackageId, int deviceId) {
                 }
                 @Override
-                public void onCameraClosed(String id) {
+                public void onCameraClosed(String id, int deviceId) {
                 }};
 
-            String[] cameraIds = null;
+            String[] cameraIds;
             synchronized (mLock) {
                 connectCameraServiceLocked();
                 try {
@@ -2255,23 +2316,24 @@
                     // devices removed as well. This is the same situation.
                     cameraStatuses = mCameraService.addListener(testListener);
                     mCameraService.removeListener(testListener);
-                    for (CameraStatus c : cameraStatuses) {
-                        onStatusChangedLocked(c.status, c.cameraId);
+                    for (CameraStatus cameraStatus : cameraStatuses) {
+                        onStatusChangedLocked(cameraStatus.status,
+                                new DeviceCameraInfo(cameraStatus.cameraId, cameraStatus.deviceId));
                     }
-                    Set<String> deviceCameraIds = mDeviceStatus.keySet();
-                    ArrayList<String> deviceIdsToRemove = new ArrayList<String>();
-                    for (String deviceCameraId : deviceCameraIds) {
+                    Set<DeviceCameraInfo> deviceCameraInfos = mDeviceStatus.keySet();
+                    List<DeviceCameraInfo> deviceInfosToRemove = new ArrayList<>();
+                    for (DeviceCameraInfo info : deviceCameraInfos) {
                         // Its possible that a device id was removed without a callback notifying
                         // us. This may happen in case a process 'drops' system camera permissions
                         // (even though the permission isn't a changeable one, tests may call
                         // adoptShellPermissionIdentity() and then dropShellPermissionIdentity().
-                        if (!cameraStatusesContains(cameraStatuses, deviceCameraId)) {
-                            deviceIdsToRemove.add(deviceCameraId);
+                        if (!cameraStatusesContains(cameraStatuses, info)) {
+                            deviceInfosToRemove.add(info);
                         }
                     }
-                    for (String id : deviceIdsToRemove) {
-                        onStatusChangedLocked(ICameraServiceListener.STATUS_NOT_PRESENT, id);
-                        mTorchStatus.remove(id);
+                    for (DeviceCameraInfo info : deviceInfosToRemove) {
+                        onStatusChangedLocked(ICameraServiceListener.STATUS_NOT_PRESENT, info);
+                        mTorchStatus.remove(info);
                     }
                 } catch (ServiceSpecificException e) {
                     // Unexpected failure
@@ -2280,7 +2342,7 @@
                 } catch (RemoteException e) {
                     // Camera service is now down, leave mCameraService as null
                 }
-                cameraIds = extractCameraIdListLocked();
+                cameraIds = extractCameraIdListLocked(deviceId, devicePolicy);
             }
             sortCameraIds(cameraIds);
             return cameraIds;
@@ -2290,19 +2352,19 @@
          * Get a list of all camera IDs that are at least PRESENT; ignore devices that are
          * NOT_PRESENT or ENUMERATING, since they cannot be used by anyone.
          */
-        public String[] getCameraIdList() {
-            String[] cameraIds = null;
+        public String[] getCameraIdList(int deviceId, int devicePolicy) {
+            String[] cameraIds;
             synchronized (mLock) {
                 // Try to make sure we have an up-to-date list of camera devices.
                 connectCameraServiceLocked();
-                cameraIds = extractCameraIdListLocked();
+                cameraIds = extractCameraIdListLocked(deviceId, devicePolicy);
             }
             sortCameraIds(cameraIds);
             return cameraIds;
         }
 
         public @NonNull Set<Set<String>> getConcurrentCameraIds() {
-            Set<Set<String>> concurrentStreamingCameraIds = null;
+            Set<Set<String>> concurrentStreamingCameraIds;
             synchronized (mLock) {
                 // Try to make sure we have an up-to-date list of concurrent camera devices.
                 connectCameraServiceLocked();
@@ -2315,11 +2377,12 @@
         public boolean isConcurrentSessionConfigurationSupported(
                 @NonNull Map<String, SessionConfiguration> cameraIdsAndSessionConfigurations,
                 int targetSdkVersion) throws CameraAccessException {
-
             if (cameraIdsAndSessionConfigurations == null) {
                 throw new IllegalArgumentException("cameraIdsAndSessionConfigurations was null");
             }
 
+            // TODO(b/291736219): Check if this API needs to be made device-aware.
+
             int size = cameraIdsAndSessionConfigurations.size();
             if (size == 0) {
                 throw new IllegalArgumentException("camera id and session combination is empty");
@@ -2361,19 +2424,21 @@
             }
         }
 
-      /**
-        * Helper function to find out if a camera id is in the set of combinations returned by
-        * getConcurrentCameraIds()
-        * @param cameraId the unique identifier of the camera device to query
-        * @return Whether the camera device was found in the set of combinations returned by
-        *         getConcurrentCameraIds
-        */
-        public boolean cameraIdHasConcurrentStreamsLocked(String cameraId) {
-            if (!mDeviceStatus.containsKey(cameraId)) {
+        /**
+         * Helper function to find out if a camera id is in the set of combinations returned by
+         * getConcurrentCameraIds()
+         * @param cameraId the unique identifier of the camera device to query
+         * @param deviceId the device id of the context
+         * @return Whether the camera device was found in the set of combinations returned by
+         *         getConcurrentCameraIds
+         */
+        public boolean cameraIdHasConcurrentStreamsLocked(String cameraId, int deviceId) {
+            DeviceCameraInfo info = new DeviceCameraInfo(cameraId, deviceId);
+            if (!mDeviceStatus.containsKey(info)) {
                 // physical camera ids aren't advertised in concurrent camera id combinations.
                 if (DEBUG) {
                     Log.v(TAG, " physical camera id " + cameraId + " is hidden." +
-                            " Available logical camera ids : " + mDeviceStatus.toString());
+                            " Available logical camera ids : " + mDeviceStatus);
                 }
                 return false;
             }
@@ -2385,9 +2450,9 @@
             return false;
         }
 
-        public void setTorchMode(String cameraId, boolean enabled) throws CameraAccessException {
-            synchronized(mLock) {
-
+        public void setTorchMode(String cameraId, boolean enabled, int deviceId, int devicePolicy)
+                throws CameraAccessException {
+            synchronized (mLock) {
                 if (cameraId == null) {
                     throw new IllegalArgumentException("cameraId was null");
                 }
@@ -2399,7 +2464,8 @@
                 }
 
                 try {
-                    cameraService.setTorchMode(cameraId, enabled, mTorchClientBinder);
+                    cameraService.setTorchMode(cameraId, enabled, mTorchClientBinder, deviceId,
+                            devicePolicy);
                 } catch(ServiceSpecificException e) {
                     throw ExceptionUtils.throwAsPublicException(e);
                 } catch (RemoteException e) {
@@ -2409,10 +2475,10 @@
             }
         }
 
-        public void turnOnTorchWithStrengthLevel(String cameraId, int torchStrength) throws
-                CameraAccessException {
-            synchronized(mLock) {
-
+        public void turnOnTorchWithStrengthLevel(String cameraId, int torchStrength, int deviceId,
+                int devicePolicy)
+                throws CameraAccessException {
+            synchronized (mLock) {
                 if (cameraId == null) {
                     throw new IllegalArgumentException("cameraId was null");
                 }
@@ -2425,7 +2491,7 @@
 
                 try {
                     cameraService.turnOnTorchWithStrengthLevel(cameraId, torchStrength,
-                            mTorchClientBinder);
+                            mTorchClientBinder, deviceId, devicePolicy);
                 } catch(ServiceSpecificException e) {
                     throw ExceptionUtils.throwAsPublicException(e);
                 } catch (RemoteException e) {
@@ -2435,9 +2501,10 @@
             }
         }
 
-        public int getTorchStrengthLevel(String cameraId) throws CameraAccessException {
-            int torchStrength = 0;
-            synchronized(mLock) {
+        public int getTorchStrengthLevel(String cameraId, int deviceId, int devicePolicy)
+                throws CameraAccessException {
+            int torchStrength;
+            synchronized (mLock) {
                 if (cameraId == null) {
                     throw new IllegalArgumentException("cameraId was null");
                 }
@@ -2449,7 +2516,8 @@
                 }
 
                 try {
-                    torchStrength = cameraService.getTorchStrengthLevel(cameraId);
+                    torchStrength = cameraService.getTorchStrengthLevel(cameraId, deviceId,
+                            devicePolicy);
                 } catch(ServiceSpecificException e) {
                     throw ExceptionUtils.throwAsPublicException(e);
                 } catch (RemoteException e) {
@@ -2506,13 +2574,7 @@
                 final Executor executor) {
             final long ident = Binder.clearCallingIdentity();
             try {
-                executor.execute(
-                    new Runnable() {
-                        @Override
-                        public void run() {
-                            callback.onCameraAccessPrioritiesChanged();
-                        }
-                    });
+                executor.execute(callback::onCameraAccessPrioritiesChanged);
             } finally {
                 Binder.restoreCallingIdentity(ident);
             }
@@ -2522,13 +2584,7 @@
                 final Executor executor, final String id, final String packageId) {
             final long ident = Binder.clearCallingIdentity();
             try {
-                executor.execute(
-                    new Runnable() {
-                        @Override
-                        public void run() {
-                            callback.onCameraOpened(id, packageId);
-                        }
-                    });
+                executor.execute(() -> callback.onCameraOpened(id, packageId));
             } finally {
                 Binder.restoreCallingIdentity(ident);
             }
@@ -2538,13 +2594,7 @@
                 final Executor executor, final String id) {
             final long ident = Binder.clearCallingIdentity();
             try {
-                executor.execute(
-                    new Runnable() {
-                        @Override
-                        public void run() {
-                            callback.onCameraClosed(id);
-                        }
-                    });
+                executor.execute(() -> callback.onCameraClosed(id));
             } finally {
                 Binder.restoreCallingIdentity(ident);
             }
@@ -2556,16 +2606,13 @@
                 final long ident = Binder.clearCallingIdentity();
                 try {
                     executor.execute(
-                        new Runnable() {
-                            @Override
-                            public void run() {
+                            () -> {
                                 if (physicalId == null) {
                                     callback.onCameraAvailable(id);
                                 } else {
                                     callback.onPhysicalCameraAvailable(id, physicalId);
                                 }
-                            }
-                        });
+                            });
                 } finally {
                     Binder.restoreCallingIdentity(ident);
                 }
@@ -2573,16 +2620,13 @@
                 final long ident = Binder.clearCallingIdentity();
                 try {
                     executor.execute(
-                        new Runnable() {
-                            @Override
-                            public void run() {
+                            () -> {
                                 if (physicalId == null) {
                                     callback.onCameraUnavailable(id);
                                 } else {
                                     callback.onPhysicalCameraUnavailable(id, physicalId);
                                 }
-                            }
-                        });
+                            });
                 } finally {
                     Binder.restoreCallingIdentity(ident);
                 }
@@ -2594,28 +2638,24 @@
             switch(status) {
                 case ICameraServiceListener.TORCH_STATUS_AVAILABLE_ON:
                 case ICameraServiceListener.TORCH_STATUS_AVAILABLE_OFF: {
-                        final long ident = Binder.clearCallingIdentity();
-                        try {
-                            executor.execute(() -> {
-                                callback.onTorchModeChanged(id, status ==
-                                        ICameraServiceListener.TORCH_STATUS_AVAILABLE_ON);
-                            });
-                        } finally {
-                            Binder.restoreCallingIdentity(ident);
-                        }
+                    final long ident = Binder.clearCallingIdentity();
+                    try {
+                        executor.execute(() -> callback.onTorchModeChanged(id, status
+                                == ICameraServiceListener.TORCH_STATUS_AVAILABLE_ON));
+                    } finally {
+                        Binder.restoreCallingIdentity(ident);
                     }
                     break;
+                }
                 default: {
-                        final long ident = Binder.clearCallingIdentity();
-                        try {
-                            executor.execute(() -> {
-                                callback.onTorchModeUnavailable(id);
-                            });
-                        } finally {
-                            Binder.restoreCallingIdentity(ident);
-                        }
+                    final long ident = Binder.clearCallingIdentity();
+                    try {
+                        executor.execute(() -> callback.onTorchModeUnavailable(id));
+                    } finally {
+                        Binder.restoreCallingIdentity(ident);
                     }
                     break;
+                }
             }
         }
 
@@ -2623,9 +2663,7 @@
                  final Executor executor, final String id, final int newStrengthLevel) {
             final long ident = Binder.clearCallingIdentity();
             try {
-                executor.execute(() -> {
-                    callback.onTorchStrengthLevelChanged(id, newStrengthLevel);
-                });
+                executor.execute(() -> callback.onTorchStrengthLevelChanged(id, newStrengthLevel));
             } finally {
                 Binder.restoreCallingIdentity(ident);
             }
@@ -2637,48 +2675,57 @@
          */
         private void updateCallbackLocked(AvailabilityCallback callback, Executor executor) {
             for (int i = 0; i < mDeviceStatus.size(); i++) {
-                String id = mDeviceStatus.keyAt(i);
+                DeviceCameraInfo info = mDeviceStatus.keyAt(i);
+                if (shouldHideCamera(callback.mDeviceId, callback.mDevicePolicy, info)) {
+                    continue;
+                }
+
                 Integer status = mDeviceStatus.valueAt(i);
-                postSingleUpdate(callback, executor, id, null /*physicalId*/, status);
+                postSingleUpdate(callback, executor, info.mCameraId, null /* physicalId */, status);
 
                 // Send the NOT_PRESENT state for unavailable physical cameras
                 if ((isAvailable(status) || physicalCallbacksAreEnabledForUnavailableCamera())
-                        && mUnavailablePhysicalDevices.containsKey(id)) {
-                    ArrayList<String> unavailableIds = mUnavailablePhysicalDevices.get(id);
+                        && mUnavailablePhysicalDevices.containsKey(info)) {
+                    List<String> unavailableIds = mUnavailablePhysicalDevices.get(info);
                     for (String unavailableId : unavailableIds) {
-                        postSingleUpdate(callback, executor, id, unavailableId,
+                        postSingleUpdate(callback, executor, info.mCameraId, unavailableId,
                                 ICameraServiceListener.STATUS_NOT_PRESENT);
                     }
                 }
-
             }
+
             for (int i = 0; i < mOpenedDevices.size(); i++) {
-                String id = mOpenedDevices.keyAt(i);
+                DeviceCameraInfo info = mOpenedDevices.keyAt(i);
+                if (shouldHideCamera(callback.mDeviceId, callback.mDevicePolicy, info)) {
+                    continue;
+                }
+
                 String clientPackageId = mOpenedDevices.valueAt(i);
-                postSingleCameraOpenedUpdate(callback, executor, id, clientPackageId);
+                postSingleCameraOpenedUpdate(callback, executor, info.mCameraId, clientPackageId);
             }
         }
 
-        private void onStatusChangedLocked(int status, String id) {
+        private void onStatusChangedLocked(int status, DeviceCameraInfo info) {
             if (DEBUG) {
                 Log.v(TAG,
-                        String.format("Camera id %s has status changed to 0x%x", id, status));
+                        String.format("Camera id %s has status changed to 0x%x for device %d",
+                                info.mCameraId, status, info.mDeviceId));
             }
 
             if (!validStatus(status)) {
-                Log.e(TAG, String.format("Ignoring invalid device %s status 0x%x", id,
-                                status));
+                Log.e(TAG, String.format("Ignoring invalid camera %s status 0x%x for device %d",
+                        info.mCameraId, status, info.mDeviceId));
                 return;
             }
 
             Integer oldStatus;
             if (status == ICameraServiceListener.STATUS_NOT_PRESENT) {
-                oldStatus = mDeviceStatus.remove(id);
-                mUnavailablePhysicalDevices.remove(id);
+                oldStatus = mDeviceStatus.remove(info);
+                mUnavailablePhysicalDevices.remove(info);
             } else {
-                oldStatus = mDeviceStatus.put(id, status);
+                oldStatus = mDeviceStatus.put(info, status);
                 if (oldStatus == null) {
-                    mUnavailablePhysicalDevices.put(id, new ArrayList<String>());
+                    mUnavailablePhysicalDevices.put(info, new ArrayList<>());
                 }
             }
 
@@ -2718,45 +2765,50 @@
 
             final int callbackCount = mCallbackMap.size();
             for (int i = 0; i < callbackCount; i++) {
-                Executor executor = mCallbackMap.valueAt(i);
                 final AvailabilityCallback callback = mCallbackMap.keyAt(i);
+                if (shouldHideCamera(callback.mDeviceId, callback.mDevicePolicy, info)) {
+                    continue;
+                }
 
-                postSingleUpdate(callback, executor, id, null /*physicalId*/, status);
+                final Executor executor = mCallbackMap.valueAt(i);
+                postSingleUpdate(callback, executor, info.mCameraId, null /* physicalId */, status);
 
                 // Send the NOT_PRESENT state for unavailable physical cameras
-                if (isAvailable(status) && mUnavailablePhysicalDevices.containsKey(id)) {
-                    ArrayList<String> unavailableIds = mUnavailablePhysicalDevices.get(id);
+                if (isAvailable(status) && mUnavailablePhysicalDevices.containsKey(info)) {
+                    List<String> unavailableIds = mUnavailablePhysicalDevices.get(info);
                     for (String unavailableId : unavailableIds) {
-                        postSingleUpdate(callback, executor, id, unavailableId,
+                        postSingleUpdate(callback, executor, info.mCameraId, unavailableId,
                                 ICameraServiceListener.STATUS_NOT_PRESENT);
                     }
                 }
             }
         } // onStatusChangedLocked
 
-        private void onPhysicalCameraStatusChangedLocked(int status,
-                String id, String physicalId) {
+        private void onPhysicalCameraStatusChangedLocked(int status, DeviceCameraInfo info,
+                String physicalId) {
             if (DEBUG) {
                 Log.v(TAG,
-                        String.format("Camera id %s physical camera id %s has status "
-                        + "changed to 0x%x", id, physicalId, status));
+                        String.format("Camera id %s physical camera id %s has status changed "
+                                + "to 0x%x for device %d", info.mCameraId, physicalId, status,
+                                info.mDeviceId));
             }
 
             if (!validStatus(status)) {
                 Log.e(TAG, String.format(
-                        "Ignoring invalid device %s physical device %s status 0x%x", id,
-                        physicalId, status));
+                        "Ignoring invalid device %s physical device %s status 0x%x for device %d",
+                        info.mCameraId, physicalId, status, info.mDeviceId));
                 return;
             }
 
             //TODO: Do we need to treat this as error?
-            if (!mDeviceStatus.containsKey(id) || !mUnavailablePhysicalDevices.containsKey(id)) {
+            if (!mDeviceStatus.containsKey(info)
+                    || !mUnavailablePhysicalDevices.containsKey(info)) {
                 Log.e(TAG, String.format("Camera %s is not present. Ignore physical camera "
-                        + "status change", id));
+                        + "status change", info.mCameraId));
                 return;
             }
 
-            ArrayList<String> unavailablePhysicalDevices = mUnavailablePhysicalDevices.get(id);
+            List<String> unavailablePhysicalDevices = mUnavailablePhysicalDevices.get(info);
             if (!isAvailable(status)
                     && !unavailablePhysicalDevices.contains(physicalId)) {
                 unavailablePhysicalDevices.add(physicalId);
@@ -2777,42 +2829,51 @@
             }
 
             if (!physicalCallbacksAreEnabledForUnavailableCamera()
-                    && !isAvailable(mDeviceStatus.get(id))) {
+                    && !isAvailable(mDeviceStatus.get(info))) {
                 Log.i(TAG, String.format("Camera %s is not available. Ignore physical camera "
-                        + "status change callback(s)", id));
+                        + "status change callback(s)", info.mCameraId));
                 return;
             }
 
             final int callbackCount = mCallbackMap.size();
             for (int i = 0; i < callbackCount; i++) {
-                Executor executor = mCallbackMap.valueAt(i);
                 final AvailabilityCallback callback = mCallbackMap.keyAt(i);
+                if (shouldHideCamera(callback.mDeviceId, callback.mDevicePolicy, info)) {
+                    continue;
+                }
 
-                postSingleUpdate(callback, executor, id, physicalId, status);
+                final Executor executor = mCallbackMap.valueAt(i);
+                postSingleUpdate(callback, executor, info.mCameraId, physicalId, status);
             }
         } // onPhysicalCameraStatusChangedLocked
 
         private void updateTorchCallbackLocked(TorchCallback callback, Executor executor) {
             for (int i = 0; i < mTorchStatus.size(); i++) {
-                String id = mTorchStatus.keyAt(i);
+                DeviceCameraInfo info = mTorchStatus.keyAt(i);
+                if (shouldHideCamera(callback.mDeviceId, callback.mDevicePolicy, info)) {
+                    continue;
+                }
+
                 Integer status = mTorchStatus.valueAt(i);
-                postSingleTorchUpdate(callback, executor, id, status);
+                postSingleTorchUpdate(callback, executor, info.mCameraId, status);
             }
         }
 
-        private void onTorchStatusChangedLocked(int status, String id) {
+        private void onTorchStatusChangedLocked(int status, DeviceCameraInfo info) {
             if (DEBUG) {
-                Log.v(TAG,
-                        String.format("Camera id %s has torch status changed to 0x%x", id, status));
+                Log.v(TAG, String.format(
+                        "Camera id %s has torch status changed to 0x%x for device %d",
+                        info.mCameraId, status, info.mDeviceId));
             }
 
             if (!validTorchStatus(status)) {
-                Log.e(TAG, String.format("Ignoring invalid device %s torch status 0x%x", id,
-                                status));
+                Log.e(TAG, String.format(
+                        "Ignoring invalid camera %s torch status 0x%x for device %d",
+                        info.mCameraId, status, info.mDeviceId));
                 return;
             }
 
-            Integer oldStatus = mTorchStatus.put(id, status);
+            Integer oldStatus = mTorchStatus.put(info, status);
             if (oldStatus != null && oldStatus == status) {
                 if (DEBUG) {
                     Log.v(TAG, String.format(
@@ -2824,25 +2885,34 @@
 
             final int callbackCount = mTorchCallbackMap.size();
             for (int i = 0; i < callbackCount; i++) {
-                final Executor executor = mTorchCallbackMap.valueAt(i);
                 final TorchCallback callback = mTorchCallbackMap.keyAt(i);
-                postSingleTorchUpdate(callback, executor, id, status);
+                if (shouldHideCamera(callback.mDeviceId, callback.mDevicePolicy, info)) {
+                    continue;
+                }
+
+                final Executor executor = mTorchCallbackMap.valueAt(i);
+                postSingleTorchUpdate(callback, executor, info.mCameraId, status);
             }
         } // onTorchStatusChangedLocked
 
-        private void onTorchStrengthLevelChangedLocked(String cameraId, int newStrengthLevel) {
+        private void onTorchStrengthLevelChangedLocked(DeviceCameraInfo info,
+                int newStrengthLevel) {
             if (DEBUG) {
-
-                Log.v(TAG,
-                        String.format("Camera id %s has torch strength level changed to %d",
-                            cameraId, newStrengthLevel));
+                Log.v(TAG, String.format(
+                        "Camera id %s has torch strength level changed to %d for device %d",
+                        info.mCameraId, newStrengthLevel, info.mDeviceId));
             }
 
             final int callbackCount = mTorchCallbackMap.size();
             for (int i = 0; i < callbackCount; i++) {
-                final Executor executor = mTorchCallbackMap.valueAt(i);
                 final TorchCallback callback = mTorchCallbackMap.keyAt(i);
-                postSingleTorchStrengthLevelUpdate(callback, executor, cameraId, newStrengthLevel);
+                if (shouldHideCamera(callback.mDeviceId, callback.mDevicePolicy, info)) {
+                    continue;
+                }
+
+                final Executor executor = mTorchCallbackMap.valueAt(i);
+                postSingleTorchStrengthLevelUpdate(callback, executor, info.mCameraId,
+                        newStrengthLevel);
             }
         } // onTorchStrengthLevelChanged
 
@@ -2856,13 +2926,16 @@
          *                                       onCameraOpened/onCameraClosed callback
          */
         public void registerAvailabilityCallback(AvailabilityCallback callback, Executor executor,
-                boolean hasOpenCloseListenerPermission) {
+                boolean hasOpenCloseListenerPermission, int deviceId, int devicePolicy) {
             synchronized (mLock) {
                 // In practice, this permission doesn't change. So we don't need one flag for each
                 // callback object.
                 mHasOpenCloseListenerPermission = hasOpenCloseListenerPermission;
                 connectCameraServiceLocked();
 
+                callback.mDeviceId = deviceId;
+                callback.mDevicePolicy = devicePolicy;
+
                 Executor oldExecutor = mCallbackMap.put(callback, executor);
                 // For new callbacks, provide initial availability information
                 if (oldExecutor == null) {
@@ -2888,10 +2961,14 @@
             }
         }
 
-        public void registerTorchCallback(TorchCallback callback, Executor executor) {
+        public void registerTorchCallback(TorchCallback callback, Executor executor, int deviceId,
+                int devicePolicy) {
             synchronized(mLock) {
                 connectCameraServiceLocked();
 
+                callback.mDeviceId = deviceId;
+                callback.mDevicePolicy = devicePolicy;
+
                 Executor oldExecutor = mTorchCallbackMap.put(callback, executor);
                 // For new callbacks, provide initial torch information
                 if (oldExecutor == null) {
@@ -2915,32 +2992,36 @@
          * Callback from camera service notifying the process about camera availability changes
          */
         @Override
-        public void onStatusChanged(int status, String cameraId) throws RemoteException {
+        public void onStatusChanged(int status, String cameraId, int deviceId)
+                throws RemoteException {
             synchronized(mLock) {
-                onStatusChangedLocked(status, cameraId);
+                onStatusChangedLocked(status, new DeviceCameraInfo(cameraId, deviceId));
             }
         }
 
         @Override
         public void onPhysicalCameraStatusChanged(int status, String cameraId,
-                String physicalCameraId) throws RemoteException {
+                String physicalCameraId, int deviceId) throws RemoteException {
             synchronized (mLock) {
-                onPhysicalCameraStatusChangedLocked(status, cameraId, physicalCameraId);
+                onPhysicalCameraStatusChangedLocked(status,
+                        new DeviceCameraInfo(cameraId, deviceId), physicalCameraId);
             }
         }
 
         @Override
-        public void onTorchStatusChanged(int status, String cameraId) throws RemoteException {
-            synchronized (mLock) {
-                onTorchStatusChangedLocked(status, cameraId);
-            }
-        }
-
-        @Override
-        public void onTorchStrengthLevelChanged(String cameraId, int newStrengthLevel)
+        public void onTorchStatusChanged(int status, String cameraId, int deviceId)
                 throws RemoteException {
             synchronized (mLock) {
-                onTorchStrengthLevelChangedLocked(cameraId, newStrengthLevel);
+                onTorchStatusChangedLocked(status, new DeviceCameraInfo(cameraId, deviceId));
+            }
+        }
+
+        @Override
+        public void onTorchStrengthLevelChanged(String cameraId, int newStrengthLevel, int deviceId)
+                throws RemoteException {
+            synchronized (mLock) {
+                onTorchStrengthLevelChangedLocked(new DeviceCameraInfo(cameraId, deviceId),
+                        newStrengthLevel);
             }
         }
 
@@ -2958,14 +3039,14 @@
         }
 
         @Override
-        public void onCameraOpened(String cameraId, String clientPackageId) {
+        public void onCameraOpened(String cameraId, String clientPackageId, int deviceId) {
             synchronized (mLock) {
-                onCameraOpenedLocked(cameraId, clientPackageId);
+                onCameraOpenedLocked(new DeviceCameraInfo(cameraId, deviceId), clientPackageId);
             }
         }
 
-        private void onCameraOpenedLocked(String cameraId, String clientPackageId) {
-            String oldApk = mOpenedDevices.put(cameraId, clientPackageId);
+        private void onCameraOpenedLocked(DeviceCameraInfo info, String clientPackageId) {
+            String oldApk = mOpenedDevices.put(info, clientPackageId);
 
             if (oldApk != null) {
                 if (oldApk.equals(clientPackageId)) {
@@ -2984,29 +3065,35 @@
 
             final int callbackCount = mCallbackMap.size();
             for (int i = 0; i < callbackCount; i++) {
-                Executor executor = mCallbackMap.valueAt(i);
                 final AvailabilityCallback callback = mCallbackMap.keyAt(i);
+                if (shouldHideCamera(callback.mDeviceId, callback.mDevicePolicy, info)) {
+                    continue;
+                }
 
-                postSingleCameraOpenedUpdate(callback, executor, cameraId, clientPackageId);
+                final Executor executor = mCallbackMap.valueAt(i);
+                postSingleCameraOpenedUpdate(callback, executor, info.mCameraId, clientPackageId);
             }
         }
 
         @Override
-        public void onCameraClosed(String cameraId) {
+        public void onCameraClosed(String cameraId, int deviceId) {
             synchronized (mLock) {
-                onCameraClosedLocked(cameraId);
+                onCameraClosedLocked(new DeviceCameraInfo(cameraId, deviceId));
             }
         }
 
-        private void onCameraClosedLocked(String cameraId) {
-            mOpenedDevices.remove(cameraId);
+        private void onCameraClosedLocked(DeviceCameraInfo info) {
+            mOpenedDevices.remove(info);
 
             final int callbackCount = mCallbackMap.size();
             for (int i = 0; i < callbackCount; i++) {
-                Executor executor = mCallbackMap.valueAt(i);
                 final AvailabilityCallback callback = mCallbackMap.keyAt(i);
+                if (shouldHideCamera(callback.mDeviceId, callback.mDevicePolicy, info)) {
+                    continue;
+                }
 
-                postSingleCameraClosedUpdate(callback, executor, cameraId);
+                final Executor executor = mCallbackMap.valueAt(i);
+                postSingleCameraClosedUpdate(callback, executor, info.mCameraId);
             }
         }
 
@@ -3062,17 +3149,18 @@
                 // Iterate from the end to the beginning because onStatusChangedLocked removes
                 // entries from the ArrayMap.
                 for (int i = mDeviceStatus.size() - 1; i >= 0; i--) {
-                    String cameraId = mDeviceStatus.keyAt(i);
-                    onStatusChangedLocked(ICameraServiceListener.STATUS_NOT_PRESENT, cameraId);
+                    DeviceCameraInfo info = mDeviceStatus.keyAt(i);
+                    onStatusChangedLocked(ICameraServiceListener.STATUS_NOT_PRESENT, info);
 
                     if (mHasOpenCloseListenerPermission) {
-                        onCameraClosedLocked(cameraId);
+                        onCameraClosedLocked(info);
                     }
                 }
+
                 for (int i = 0; i < mTorchStatus.size(); i++) {
-                    String cameraId = mTorchStatus.keyAt(i);
+                    DeviceCameraInfo info = mTorchStatus.keyAt(i);
                     onTorchStatusChangedLocked(ICameraServiceListener.TORCH_STATUS_NOT_AVAILABLE,
-                            cameraId);
+                            info);
                 }
 
                 mConcurrentCameraIdCombinations.clear();
@@ -3081,6 +3169,31 @@
             }
         }
 
-    } // CameraManagerGlobal
+        private static final class DeviceCameraInfo {
+            private final String mCameraId;
+            private final int mDeviceId;
 
+            DeviceCameraInfo(String cameraId, int deviceId) {
+                mCameraId = cameraId;
+                mDeviceId = deviceId;
+            }
+
+            @Override
+            public boolean equals(Object o) {
+                if (this == o) {
+                    return true;
+                }
+                if (o == null || getClass() != o.getClass()) {
+                    return false;
+                }
+                DeviceCameraInfo that = (DeviceCameraInfo) o;
+                return mDeviceId == that.mDeviceId && Objects.equals(mCameraId, that.mCameraId);
+            }
+
+            @Override
+            public int hashCode() {
+                return Objects.hash(mCameraId, mDeviceId);
+            }
+        }
+    } // CameraManagerGlobal
 } // CameraManager
diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceSetupImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceSetupImpl.java
index 9e01438..24ac0b5 100644
--- a/core/java/android/hardware/camera2/impl/CameraDeviceSetupImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraDeviceSetupImpl.java
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 package android.hardware.camera2.impl;
 
 import android.annotation.CallbackExecutor;
@@ -71,7 +72,8 @@
 
             try {
                 CameraMetadataNative defaultRequest = cameraService.createDefaultRequest(mCameraId,
-                        templateType);
+                        templateType, mContext.getDeviceId(),
+                        mCameraManager.getDevicePolicyFromContext(mContext));
                 CameraDeviceImpl.disableZslIfNeeded(defaultRequest, mTargetSdkVersion,
                         templateType);
 
@@ -103,7 +105,8 @@
 
             try {
                 return cameraService.isSessionConfigurationWithParametersSupported(
-                        mCameraId, config);
+                        mCameraId, config, mContext.getDeviceId(),
+                        mCameraManager.getDevicePolicyFromContext(mContext));
             } catch (ServiceSpecificException e) {
                 throw ExceptionUtils.throwAsPublicException(e);
             } catch (RemoteException e) {
@@ -131,7 +134,9 @@
             try {
                 CameraMetadataNative metadataNative = cameraService.getSessionCharacteristics(
                         mCameraId, mTargetSdkVersion,
-                        CameraManager.shouldOverrideToPortrait(mContext), sessionConfig);
+                        CameraManager.shouldOverrideToPortrait(mContext), sessionConfig,
+                        mContext.getDeviceId(),
+                        mCameraManager.getDevicePolicyFromContext(mContext));
 
                 return new CameraCharacteristics(metadataNative);
             } catch (ServiceSpecificException e) {
diff --git a/core/java/android/hardware/devicestate/DeviceState.java b/core/java/android/hardware/devicestate/DeviceState.java
index 689e343..64fc4c2 100644
--- a/core/java/android/hardware/devicestate/DeviceState.java
+++ b/core/java/android/hardware/devicestate/DeviceState.java
@@ -333,14 +333,12 @@
         private final ArraySet<@PhysicalDeviceStateProperties Integer> mPhysicalProperties;
 
         private Configuration(int identifier, @NonNull String name,
-                @NonNull Set<@SystemDeviceStateProperties Integer> systemProperties,
-                @NonNull Set<@PhysicalDeviceStateProperties Integer> physicalProperties) {
+                @NonNull ArraySet<@SystemDeviceStateProperties Integer> systemProperties,
+                @NonNull ArraySet<@PhysicalDeviceStateProperties Integer> physicalProperties) {
             mIdentifier = identifier;
             mName = name;
-            mSystemProperties = new ArraySet<@SystemDeviceStateProperties Integer>(
-                    systemProperties);
-            mPhysicalProperties = new ArraySet<@PhysicalDeviceStateProperties Integer>(
-                    physicalProperties);
+            mSystemProperties = systemProperties;
+            mPhysicalProperties = physicalProperties;
         }
 
         /** Returns the unique identifier for the device state. */
@@ -479,8 +477,8 @@
              */
             @NonNull
             public DeviceState.Configuration build() {
-                return new DeviceState.Configuration(mIdentifier, mName, mSystemProperties,
-                        mPhysicalProperties);
+                return new DeviceState.Configuration(mIdentifier, mName,
+                        new ArraySet<>(mSystemProperties), new ArraySet<>(mPhysicalProperties));
             }
         }
     }
diff --git a/core/java/android/hardware/devicestate/DeviceStateInfo.java b/core/java/android/hardware/devicestate/DeviceStateInfo.java
index c319c89..28561ec 100644
--- a/core/java/android/hardware/devicestate/DeviceStateInfo.java
+++ b/core/java/android/hardware/devicestate/DeviceStateInfo.java
@@ -25,7 +25,6 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
-import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.Executor;
 
@@ -77,9 +76,11 @@
      * NOTE: Unlike {@link #DeviceStateInfo(DeviceStateInfo)}, this constructor does not copy the
      * supplied parameters.
      */
-    public DeviceStateInfo(@NonNull List<DeviceState> supportedStates, DeviceState baseState,
+    // Using the specific types to avoid virtual method calls in binder transactions
+    @SuppressWarnings("NonApiType")
+    public DeviceStateInfo(@NonNull ArrayList<DeviceState> supportedStates, DeviceState baseState,
             DeviceState state) {
-        this.supportedStates = new ArrayList<>(supportedStates);
+        this.supportedStates = supportedStates;
         this.baseState = baseState;
         this.currentState = state;
     }
@@ -89,13 +90,13 @@
      * the fields of the returned instance.
      */
     public DeviceStateInfo(@NonNull DeviceStateInfo info) {
-        this(List.copyOf(info.supportedStates), info.baseState, info.currentState);
+        this(new ArrayList<>(info.supportedStates), info.baseState, info.currentState);
     }
 
     @Override
     public boolean equals(@Nullable Object other) {
         if (this == other) return true;
-        if (other == null || getClass() != other.getClass()) return false;
+        if (other == null || (getClass() != other.getClass())) return false;
         DeviceStateInfo that = (DeviceStateInfo) other;
         return baseState.equals(that.baseState)
                 &&  currentState.equals(that.currentState)
diff --git a/core/java/android/service/notification/SystemZenRules.java b/core/java/android/service/notification/SystemZenRules.java
new file mode 100644
index 0000000..302efb3
--- /dev/null
+++ b/core/java/android/service/notification/SystemZenRules.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2024 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.service.notification;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.AutomaticZenRule;
+import android.app.Flags;
+import android.content.Context;
+import android.service.notification.ZenModeConfig.EventInfo;
+import android.service.notification.ZenModeConfig.ScheduleInfo;
+import android.service.notification.ZenModeConfig.ZenRule;
+import android.text.format.DateFormat;
+import android.util.Log;
+
+import com.android.internal.R;
+
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Locale;
+import java.util.Objects;
+
+/**
+ * Helper methods for schedule-type (system-owned) rules.
+ * @hide
+ */
+public final class SystemZenRules {
+
+    private static final String TAG = "SystemZenRules";
+
+    public static final String PACKAGE_ANDROID = "android";
+
+    /** Updates existing system-owned rules to use the new Modes fields (type, etc). */
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    public static void maybeUpgradeRules(Context context, ZenModeConfig config) {
+        for (ZenRule rule : config.automaticRules.values()) {
+            if (isSystemOwnedRule(rule) && rule.type == AutomaticZenRule.TYPE_UNKNOWN) {
+                upgradeSystemProviderRule(context, rule);
+            }
+        }
+    }
+
+    /**
+     * Returns whether the rule corresponds to a system ConditionProviderService (i.e. it is owned
+     * by the "android" package).
+     */
+    public static boolean isSystemOwnedRule(ZenRule rule) {
+        return PACKAGE_ANDROID.equals(rule.pkg);
+    }
+
+    @FlaggedApi(Flags.FLAG_MODES_API)
+    private static void upgradeSystemProviderRule(Context context, ZenRule rule) {
+        ScheduleInfo scheduleInfo = ZenModeConfig.tryParseScheduleConditionId(rule.conditionId);
+        if (scheduleInfo != null) {
+            rule.type = AutomaticZenRule.TYPE_SCHEDULE_TIME;
+            rule.triggerDescription = getTriggerDescriptionForScheduleTime(context, scheduleInfo);
+            return;
+        }
+        EventInfo eventInfo = ZenModeConfig.tryParseEventConditionId(rule.conditionId);
+        if (eventInfo != null) {
+            rule.type = AutomaticZenRule.TYPE_SCHEDULE_CALENDAR;
+            rule.triggerDescription = getTriggerDescriptionForScheduleEvent(context, eventInfo);
+            return;
+        }
+        Log.wtf(TAG, "Couldn't determine type of system-owned ZenRule " + rule);
+    }
+
+    /**
+     * Updates the {@link ZenRule#triggerDescription} of the system-owned rule based on the schedule
+     * or event condition encoded in its {@link ZenRule#conditionId}.
+     *
+     * @return {@code true} if the trigger description was updated.
+     */
+    public static boolean updateTriggerDescription(Context context, ZenRule rule) {
+        ScheduleInfo scheduleInfo = ZenModeConfig.tryParseScheduleConditionId(rule.conditionId);
+        if (scheduleInfo != null) {
+            return updateTriggerDescription(rule,
+                    getTriggerDescriptionForScheduleTime(context, scheduleInfo));
+        }
+        EventInfo eventInfo = ZenModeConfig.tryParseEventConditionId(rule.conditionId);
+        if (eventInfo != null) {
+            return updateTriggerDescription(rule,
+                    getTriggerDescriptionForScheduleEvent(context, eventInfo));
+        }
+        Log.wtf(TAG, "Couldn't determine type of system-owned ZenRule " + rule);
+        return false;
+    }
+
+    private static boolean updateTriggerDescription(ZenRule rule, String triggerDescription) {
+        if (!Objects.equals(rule.triggerDescription, triggerDescription)) {
+            rule.triggerDescription = triggerDescription;
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Returns a suitable trigger description for a time-schedule rule (e.g. "Mon-Fri, 8:00-10:00"),
+     * using the Context's current locale.
+     */
+    @Nullable
+    public static String getTriggerDescriptionForScheduleTime(Context context,
+            @NonNull ScheduleInfo schedule) {
+        final StringBuilder sb = new StringBuilder();
+        String daysSummary = getShortDaysSummary(context, schedule);
+        if (daysSummary == null) {
+            // no use outputting times without dates
+            return null;
+        }
+        sb.append(daysSummary);
+        sb.append(context.getString(R.string.zen_mode_trigger_summary_divider_text));
+        sb.append(context.getString(
+                R.string.zen_mode_trigger_summary_range_symbol_combination,
+                timeString(context, schedule.startHour, schedule.startMinute),
+                timeString(context, schedule.endHour, schedule.endMinute)));
+
+        return sb.toString();
+    }
+
+    /**
+     * Returns an ordered summarized list of the days on which this schedule applies, with
+     * adjacent days grouped together ("Sun-Wed" instead of "Sun,Mon,Tue,Wed").
+     */
+    @Nullable
+    private static String getShortDaysSummary(Context context, @NonNull ScheduleInfo schedule) {
+        // Compute a list of days with contiguous days grouped together, for example: "Sun-Thu" or
+        // "Sun-Mon,Wed,Fri"
+        final int[] days = schedule.days;
+        if (days != null && days.length > 0) {
+            final StringBuilder sb = new StringBuilder();
+            final Calendar cStart = Calendar.getInstance(getLocale(context));
+            final Calendar cEnd = Calendar.getInstance(getLocale(context));
+            int[] daysOfWeek = getDaysOfWeekForLocale(cStart);
+            // the i for loop goes through days in order as determined by locale. as we walk through
+            // the days of the week, keep track of "start" and "last seen"  as indicators for
+            // what's contiguous, and initialize them to something not near actual indices
+            int startDay = Integer.MIN_VALUE;
+            int lastSeenDay = Integer.MIN_VALUE;
+            for (int i = 0; i < daysOfWeek.length; i++) {
+                final int day = daysOfWeek[i];
+
+                // by default, output if this day is *not* included in the schedule, and thus
+                // ends a previously existing block. if this day is included in the schedule
+                // after all (as will be determined in the inner for loop), then output will be set
+                // to false.
+                boolean output = (i == lastSeenDay + 1);
+                for (int j = 0; j < days.length; j++) {
+                    if (day == days[j]) {
+                        // match for this day in the schedule (indicated by counter i)
+                        if (i == lastSeenDay + 1) {
+                            // contiguous to the block we're walking through right now, record it
+                            // (specifically, i, the day index) and move on to the next day
+                            lastSeenDay = i;
+                            output = false;
+                        } else {
+                            // it's a match, but not 1 past the last match, we are starting a new
+                            // block
+                            startDay = i;
+                            lastSeenDay = i;
+                        }
+
+                        // if there is a match on the last day, also make sure to output at the end
+                        // of this loop, and mark the day as the last day we'll have seen in the
+                        // scheduled days.
+                        if (i == daysOfWeek.length - 1) {
+                            output = true;
+                        }
+                        break;
+                    }
+                }
+
+                // output in either of 2 cases: this day is not a match, so has ended any previous
+                // block, or this day *is* a match but is the last day of the week, so we need to
+                // summarize
+                if (output) {
+                    // either describe just the single day if startDay == lastSeenDay, or
+                    // output "startDay - lastSeenDay" as a group
+                    if (sb.length() > 0) {
+                        sb.append(
+                                context.getString(R.string.zen_mode_trigger_summary_divider_text));
+                    }
+
+                    SimpleDateFormat dayFormat = new SimpleDateFormat("EEE", getLocale(context));
+                    if (startDay == lastSeenDay) {
+                        // last group was only one day
+                        cStart.set(Calendar.DAY_OF_WEEK, daysOfWeek[startDay]);
+                        sb.append(dayFormat.format(cStart.getTime()));
+                    } else {
+                        // last group was a contiguous group of days, so group them together
+                        cStart.set(Calendar.DAY_OF_WEEK, daysOfWeek[startDay]);
+                        cEnd.set(Calendar.DAY_OF_WEEK, daysOfWeek[lastSeenDay]);
+                        sb.append(context.getString(
+                                R.string.zen_mode_trigger_summary_range_symbol_combination,
+                                dayFormat.format(cStart.getTime()),
+                                dayFormat.format(cEnd.getTime())));
+                    }
+                }
+            }
+
+            if (sb.length() > 0) {
+                return sb.toString();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Convenience method for representing the specified time in string format.
+     */
+    private static String timeString(Context context, int hour, int minute) {
+        final Calendar c = Calendar.getInstance(getLocale(context));
+        c.set(Calendar.HOUR_OF_DAY, hour);
+        c.set(Calendar.MINUTE, minute);
+        return DateFormat.getTimeFormat(context).format(c.getTime());
+    }
+
+    private static int[] getDaysOfWeekForLocale(Calendar c) {
+        int[] daysOfWeek = new int[7];
+        int currentDay = c.getFirstDayOfWeek();
+        for (int i = 0; i < daysOfWeek.length; i++) {
+            if (currentDay > 7) currentDay = 1;
+            daysOfWeek[i] = currentDay;
+            currentDay++;
+        }
+        return daysOfWeek;
+    }
+
+    private static Locale getLocale(Context context) {
+        return context.getResources().getConfiguration().getLocales().get(0);
+    }
+
+    /**
+     * Returns a suitable trigger description for a calendar-schedule rule (either the name of the
+     * calendar, or a message indicating all calendars are included).
+     */
+    public static String getTriggerDescriptionForScheduleEvent(Context context,
+            @NonNull EventInfo event) {
+        if (event.calName != null) {
+            return event.calName;
+        } else {
+            return context.getResources().getString(
+                    R.string.zen_mode_trigger_event_calendar_any);
+        }
+    }
+
+    private SystemZenRules() {}
+}
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index 1d6dd1e..610a317 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -1773,7 +1773,12 @@
         return true;
     }
 
+    /**
+     * Returns the {@link ScheduleInfo} encoded in the condition id, or {@code null} if it could not
+     * be decoded.
+     */
     @UnsupportedAppUsage
+    @Nullable
     public static ScheduleInfo tryParseScheduleConditionId(Uri conditionId) {
         final boolean isSchedule =  conditionId != null
                 && Condition.SCHEME.equals(conditionId.getScheme())
@@ -1882,6 +1887,11 @@
         return tryParseEventConditionId(conditionId) != null;
     }
 
+    /**
+     * Returns the {@link EventInfo} encoded in the condition id, or {@code null} if it could not be
+     * decoded.
+     */
+    @Nullable
     public static EventInfo tryParseEventConditionId(Uri conditionId) {
         final boolean isEvent = conditionId != null
                 && Condition.SCHEME.equals(conditionId.getScheme())
diff --git a/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl b/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl
index 908ab5f..a708718 100644
--- a/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl
+++ b/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl
@@ -47,4 +47,5 @@
     void registerRemoteServices(in IRemoteProcessingService remoteProcessingService);
     void notifyInferenceServiceConnected();
     void notifyInferenceServiceDisconnected();
+    void ready();
 }
\ No newline at end of file
diff --git a/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java b/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java
index 86320b8..5dc540a 100644
--- a/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java
+++ b/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java
@@ -24,6 +24,7 @@
 import android.annotation.Nullable;
 import android.annotation.SdkConstant;
 import android.annotation.SystemApi;
+import android.annotation.TestApi;
 import android.app.Service;
 import android.app.ondeviceintelligence.DownloadCallback;
 import android.app.ondeviceintelligence.Feature;
@@ -111,6 +112,11 @@
             return new IOnDeviceIntelligenceService.Stub() {
                 /** {@inheritDoc} */
                 @Override
+                public void ready() {
+                    OnDeviceIntelligenceService.this.onReady();
+                }
+
+                @Override
                 public void getVersion(RemoteCallback remoteCallback) {
                     Objects.requireNonNull(remoteCallback);
                     OnDeviceIntelligenceService.this.onGetVersion(l -> {
@@ -208,6 +214,16 @@
         return null;
     }
 
+    /**
+     * Using this signal to assertively a signal each time service binds successfully, used only in
+     * tests to get a signal that service instance is ready. This is needed because we cannot rely
+     * on {@link #onCreate} or {@link #onBind} to be invoke on each binding.
+     *
+     * @hide
+     */
+    @TestApi
+    public void onReady() {}
+
 
     /**
      * Invoked when a new instance of the remote inference service is created.
diff --git a/core/java/android/view/TextureView.java b/core/java/android/view/TextureView.java
index c66abe8..5466bf5 100644
--- a/core/java/android/view/TextureView.java
+++ b/core/java/android/view/TextureView.java
@@ -889,11 +889,11 @@
      * @hide
      */
     @Override
-    protected int calculateFrameRateCategory(int width, int height) {
+    protected int calculateFrameRateCategory() {
         if (mMinusTwoFrameIntervalMillis > 15 && mMinusOneFrameIntervalMillis > 15) {
             return FRAME_RATE_CATEGORY_NORMAL;
         }
-        return super.calculateFrameRateCategory(width, height);
+        return super.calculateFrameRateCategory();
     }
 
     @UnsupportedAppUsage
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 736e815..b6df1bb 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -5695,17 +5695,10 @@
     private ViewTranslationResponse mViewTranslationResponse;
 
     /**
-     * Threshold size for something to be considered a small area update (in DP).
-     * This is the dimension for both width and height.
+     * The multiplier for mAttachInfo.mSmallSizePixels to consider a View to be small
+     * if both dimensions are smaller than this.
      */
-    private static final float FRAME_RATE_SMALL_SIZE_THRESHOLD = 40f;
-
-    /**
-     * Threshold size for something to be considered a small area update (in DP) if
-     * it is narrow. This is for either width OR height. For example, a narrow progress
-     * bar could be considered a small area.
-     */
-    private static final float FRAME_RATE_NARROW_THRESHOLD = 10f;
+    private static final int FRAME_RATE_SQUARE_SMALL_SIZE_MULTIPLIER = 4;
 
     private static final long INFREQUENT_UPDATE_INTERVAL_MILLIS = 100;
     private static final int INFREQUENT_UPDATE_COUNTS = 2;
@@ -5740,6 +5733,8 @@
     @FlaggedApi(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
     public static final float REQUESTED_FRAME_RATE_CATEGORY_HIGH = -4;
 
+    private int mSizeBasedFrameRateCategoryAndReason;
+
     /**
      * Simple constructor to use when creating a view from code.
      *
@@ -23561,6 +23556,9 @@
             return renderNode;
         }
 
+        mLastFrameX = mLeft + mRenderNode.getTranslationX();
+        mLastFrameY = mTop + mRenderNode.getTranslationY();
+
         if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0
                 || !renderNode.hasDisplayList()
                 || (mRecreateDisplayList)) {
@@ -24724,8 +24722,6 @@
         mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
 
         mFrameContentVelocity = -1;
-        mLastFrameX = mLeft + mRenderNode.getTranslationX();
-        mLastFrameY = mTop + mRenderNode.getTranslationY();
 
         /*
          * Draw traversal performs several drawing steps which must be executed
@@ -25474,6 +25470,21 @@
     }
 
     private void sizeChange(int newWidth, int newHeight, int oldWidth, int oldHeight) {
+        if (mAttachInfo != null) {
+            int narrowSize = mAttachInfo.mSmallSizePixels;
+            int smallSize = narrowSize * FRAME_RATE_SQUARE_SMALL_SIZE_MULTIPLIER;
+            if (newWidth <= narrowSize || newHeight <= narrowSize
+                    || (newWidth <= smallSize && newHeight <= smallSize)) {
+                int category = toolkitFrameRateBySizeReadOnly()
+                        ? FRAME_RATE_CATEGORY_LOW : FRAME_RATE_CATEGORY_NORMAL;
+                mSizeBasedFrameRateCategoryAndReason = category | FRAME_RATE_CATEGORY_REASON_SMALL;
+            } else {
+                int category = toolkitFrameRateDefaultNormalReadOnly()
+                        ? FRAME_RATE_CATEGORY_NORMAL : FRAME_RATE_CATEGORY_HIGH;
+                mSizeBasedFrameRateCategoryAndReason = category | FRAME_RATE_CATEGORY_REASON_LARGE;
+            }
+        }
+
         onSizeChanged(newWidth, newHeight, oldWidth, oldHeight);
         if (mOverlay != null) {
             mOverlay.getOverlayView().setRight(newWidth);
@@ -32040,6 +32051,13 @@
         int mSensitiveViewsCount;
 
         /**
+         * The size used for a View to be considered small for the purposes of using
+         * low refresh rate by default. This is the size in one direction, so a long, thin
+         * item like a progress bar can be compared to this.
+         */
+        final int mSmallSizePixels;
+
+        /**
          * Creates a new set of attachment information with the specified
          * events handler and thread.
          *
@@ -32056,6 +32074,7 @@
             mHandler = handler;
             mRootCallbacks = effectPlayer;
             mTreeObserver = new ViewTreeObserver(context);
+            mSmallSizePixels = (int) (context.getResources().getDisplayMetrics().density * 10);
         }
 
         void increaseSensitiveViewsCount() {
@@ -33784,28 +33803,10 @@
      *
      * @hide
      */
-    protected int calculateFrameRateCategory(int width, int height) {
+    protected int calculateFrameRateCategory() {
         if (mMinusTwoFrameIntervalMillis + mMinusOneFrameIntervalMillis
                 < INFREQUENT_UPDATE_INTERVAL_MILLIS) {
-            DisplayMetrics displayMetrics = mResources.getDisplayMetrics();
-            float density = displayMetrics.density;
-            if (density == 0f) {
-                density = 1f;
-            }
-            float widthDp = width / density;
-            float heightDp = height / density;
-            if (widthDp <= FRAME_RATE_NARROW_THRESHOLD
-                    || heightDp <= FRAME_RATE_NARROW_THRESHOLD
-                    || (widthDp <= FRAME_RATE_SMALL_SIZE_THRESHOLD
-                    && heightDp <= FRAME_RATE_SMALL_SIZE_THRESHOLD)) {
-                int category = toolkitFrameRateBySizeReadOnly()
-                        ? FRAME_RATE_CATEGORY_LOW : FRAME_RATE_CATEGORY_NORMAL;
-                return category | FRAME_RATE_CATEGORY_REASON_SMALL;
-            } else {
-                int category = toolkitFrameRateDefaultNormalReadOnly()
-                        ? FRAME_RATE_CATEGORY_NORMAL : FRAME_RATE_CATEGORY_HIGH;
-                return category | FRAME_RATE_CATEGORY_REASON_LARGE;
-            }
+            return mSizeBasedFrameRateCategoryAndReason;
         }
 
         if (mInfrequentUpdateCount == INFREQUENT_UPDATE_COUNTS) {
@@ -33823,7 +33824,14 @@
             if (viewVelocityApi()) {
                 float velocity = mFrameContentVelocity;
                 if (velocity < 0f) {
-                    velocity = calculateVelocity();
+                    // This current calculation is very simple. If something on the screen moved,
+                    // then it votes for the highest velocity. If it doesn't move, then return 0.
+                    RenderNode renderNode = mRenderNode;
+                    float x = mLeft + renderNode.getTranslationX();
+                    float y = mTop + renderNode.getTranslationY();
+
+                    velocity = (!Float.isNaN(mLastFrameX) && (x != mLastFrameX || y != mLastFrameY))
+                            ? 100_000f : 0f;
                 }
                 if (velocity > 0f) {
                     float frameRate = convertVelocityToFrameRate(velocity);
@@ -33831,43 +33839,55 @@
                     return;
                 }
             }
-            if (sToolkitMetricsForFrameRateDecisionFlagValue) {
-                float sizePercentage = getSizePercentage();
-                viewRootImpl.recordViewPercentage(sizePercentage);
-            }
-            int frameRateCategory;
-            if (Float.isNaN(mPreferredFrameRate)) {
-                frameRateCategory = calculateFrameRateCategory(width, height);
-            } else if (mPreferredFrameRate < 0) {
-                if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE) {
-                    frameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE
-                            | FRAME_RATE_CATEGORY_REASON_REQUESTED;
-                } else if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_LOW) {
-                    frameRateCategory = FRAME_RATE_CATEGORY_LOW
-                            | FRAME_RATE_CATEGORY_REASON_REQUESTED;
-                } else if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_NORMAL) {
-                    frameRateCategory = FRAME_RATE_CATEGORY_NORMAL
-                            | FRAME_RATE_CATEGORY_REASON_REQUESTED;
-                } else if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_HIGH) {
-                    frameRateCategory = FRAME_RATE_CATEGORY_HIGH
-                            | FRAME_RATE_CATEGORY_REASON_REQUESTED;
-                } else {
-                    // invalid frame rate, use default
-                    int category = toolkitFrameRateDefaultNormalReadOnly()
-                            ? FRAME_RATE_CATEGORY_NORMAL : FRAME_RATE_CATEGORY_HIGH;
-                    frameRateCategory = category
-                            | FRAME_RATE_CATEGORY_REASON_INVALID;
+            if (!willNotDraw()) {
+                if (sToolkitMetricsForFrameRateDecisionFlagValue) {
+                    float sizePercentage = getSizePercentage();
+                    viewRootImpl.recordViewPercentage(sizePercentage);
                 }
-            } else {
-                viewRootImpl.votePreferredFrameRate(mPreferredFrameRate,
-                        mFrameRateCompatibility);
-                return;
-            }
 
-            int category = frameRateCategory & ~FRAME_RATE_CATEGORY_REASON_MASK;
-            int reason = frameRateCategory & FRAME_RATE_CATEGORY_REASON_MASK;
-            viewRootImpl.votePreferredFrameRateCategory(category, reason, this);
-            mLastFrameRateCategory = frameRateCategory;
+                int frameRateCategory;
+                if (Float.isNaN(mPreferredFrameRate)) {
+                    if (mMinusTwoFrameIntervalMillis + mMinusOneFrameIntervalMillis
+                            < INFREQUENT_UPDATE_INTERVAL_MILLIS && mAttachInfo != null) {
+                        frameRateCategory = mSizeBasedFrameRateCategoryAndReason;
+                    } else if (mInfrequentUpdateCount == INFREQUENT_UPDATE_COUNTS) {
+                        frameRateCategory =
+                                FRAME_RATE_CATEGORY_NORMAL
+                                        | FRAME_RATE_CATEGORY_REASON_INTERMITTENT;
+                    } else {
+                        frameRateCategory = mLastFrameRateCategory;
+                    }
+                } else if (mPreferredFrameRate < 0) {
+                    if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE) {
+                        frameRateCategory = FRAME_RATE_CATEGORY_NO_PREFERENCE
+                                | FRAME_RATE_CATEGORY_REASON_REQUESTED;
+                    } else if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_LOW) {
+                        frameRateCategory = FRAME_RATE_CATEGORY_LOW
+                                | FRAME_RATE_CATEGORY_REASON_REQUESTED;
+                    } else if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_NORMAL) {
+                        frameRateCategory = FRAME_RATE_CATEGORY_NORMAL
+                                | FRAME_RATE_CATEGORY_REASON_REQUESTED;
+                    } else if (mPreferredFrameRate == REQUESTED_FRAME_RATE_CATEGORY_HIGH) {
+                        frameRateCategory = FRAME_RATE_CATEGORY_HIGH
+                                | FRAME_RATE_CATEGORY_REASON_REQUESTED;
+                    } else {
+                        // invalid frame rate, use default
+                        int category = toolkitFrameRateDefaultNormalReadOnly()
+                                ? FRAME_RATE_CATEGORY_NORMAL : FRAME_RATE_CATEGORY_HIGH;
+                        frameRateCategory = category
+                                | FRAME_RATE_CATEGORY_REASON_INVALID;
+                    }
+                } else {
+                    viewRootImpl.votePreferredFrameRate(mPreferredFrameRate,
+                            mFrameRateCompatibility);
+                    return;
+                }
+
+                int category = frameRateCategory & ~FRAME_RATE_CATEGORY_REASON_MASK;
+                int reason = frameRateCategory & FRAME_RATE_CATEGORY_REASON_MASK;
+                viewRootImpl.votePreferredFrameRateCategory(category, reason, this);
+                mLastFrameRateCategory = frameRateCategory;
+            }
         }
     }
 
@@ -33878,16 +33898,6 @@
         return Math.min(140f, 60f + (10f * (float) Math.floor(velocityDps / 300f)));
     }
 
-    private float calculateVelocity() {
-        // This current calculation is very simple. If something on the screen moved, then
-        // it votes for the highest velocity. If it doesn't move, then return 0.
-        float x = mLeft + mRenderNode.getTranslationX();
-        float y = mTop + mRenderNode.getTranslationY();
-
-        return (!Float.isNaN(mLastFrameX) && (x != mLastFrameX || y != mLastFrameY))
-                ? 100_000f : 0f;
-    }
-
     /**
      * Set the current velocity of the View, we only track positive value.
      * We will use the velocity information to adjust the frame rate when applicable.
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index eb9be18..dd548c6 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -12626,12 +12626,11 @@
         }
         mHasInvalidation = true;
         checkIdleness();
-        if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)
-                && mPreferredFrameRateCategory != oldCategory
+        if (mPreferredFrameRateCategory != oldCategory
                 && mPreferredFrameRateCategory == frameRateCategory
         ) {
             mFrameRateCategoryChangeReason = reason;
-            mFrameRateCategoryView = view.getClass().getSimpleName();
+            mFrameRateCategoryView = view == null ? "null" : view.getClass().getSimpleName();
         }
     }
 
@@ -12791,7 +12790,6 @@
         // uncomment this when we are ready for enabling dVRR
         // return sToolkitSetFrameRateReadOnlyFlagValue && isFrameRatePowerSavingsBalanced();
         return false;
-
     }
 
     private void checkIdleness() {
diff --git a/core/java/android/webkit/WebViewZygote.java b/core/java/android/webkit/WebViewZygote.java
index e14ae72..c7900e4 100644
--- a/core/java/android/webkit/WebViewZygote.java
+++ b/core/java/android/webkit/WebViewZygote.java
@@ -104,7 +104,8 @@
             sPackage = packageInfo;
 
             // If multi-process is not enabled, then do not start the zygote service.
-            if (!sMultiprocessEnabled) {
+            // Only check sMultiprocessEnabled if updateServiceV2 is not enabled.
+            if (!updateServiceV2() && !sMultiprocessEnabled) {
                 return;
             }
 
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index f54ef38..ab6b512 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -16,6 +16,8 @@
 
 package android.widget;
 
+import static android.view.flags.Flags.viewVelocityApi;
+
 import android.annotation.ColorInt;
 import android.annotation.DrawableRes;
 import android.annotation.NonNull;
@@ -5098,6 +5100,11 @@
                 boolean more = scroller.computeScrollOffset();
                 final int y = scroller.getCurrY();
 
+                // For variable refresh rate project to track the current velocity of this View
+                if (viewVelocityApi()) {
+                    setFrameContentVelocity(Math.abs(mScroller.getCurrVelocity()));
+                }
+
                 // Flip sign to convert finger direction to list items direction
                 // (e.g. finger moving down means list is moving towards the top)
                 int delta = consumeFlingInStretch(mLastFlingY - y);
@@ -5192,6 +5199,10 @@
                         invalidate();
                         postOnAnimation(this);
                     }
+                    // For variable refresh rate project to track the current velocity of this View
+                    if (viewVelocityApi()) {
+                        setFrameContentVelocity(Math.abs(mScroller.getCurrVelocity()));
+                    }
                 } else {
                     endFling();
                 }
diff --git a/core/java/com/android/internal/app/SuspendedAppActivity.java b/core/java/com/android/internal/app/SuspendedAppActivity.java
index 751368f..6620156 100644
--- a/core/java/com/android/internal/app/SuspendedAppActivity.java
+++ b/core/java/com/android/internal/app/SuspendedAppActivity.java
@@ -373,7 +373,7 @@
                 .putExtra(Intent.EXTRA_USER_ID, userId)
                 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
                         | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
-        if (crossUserSuspensionEnabled()) {
+        if (crossUserSuspensionEnabled() && suspendingPackage != null) {
             intent.putExtra(EXTRA_SUSPENDING_USER, suspendingPackage.userId);
         }
         return intent;
diff --git a/core/java/com/android/internal/widget/remotecompose/core/CompanionOperation.java b/core/java/com/android/internal/widget/remotecompose/core/CompanionOperation.java
index ce8ca0d..2d36536 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/CompanionOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/CompanionOperation.java
@@ -21,6 +21,11 @@
  * Interface for the companion operations
  */
 public interface CompanionOperation {
+    /**
+     * Read, create and add instance to operations
+     * @param buffer data to read to create operation
+     * @param operations command is to be added
+     */
     void read(WireBuffer buffer, List<Operation> operations);
 
     // Debugging / Documentation utility functions
diff --git a/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java b/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java
index 0e4c743..55f2dee 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java
@@ -331,6 +331,7 @@
     public void initFromBuffer(RemoteComposeBuffer buffer) {
         mOperations = new ArrayList<Operation>();
         buffer.inflateFromBuffer(mOperations);
+        mBuffer = buffer;
     }
 
     /**
diff --git a/core/java/com/android/internal/widget/remotecompose/core/Operations.java b/core/java/com/android/internal/widget/remotecompose/core/Operations.java
index b8bb1f0..54b277a 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/Operations.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/Operations.java
@@ -17,8 +17,29 @@
 
 import com.android.internal.widget.remotecompose.core.operations.BitmapData;
 import com.android.internal.widget.remotecompose.core.operations.ClickArea;
+import com.android.internal.widget.remotecompose.core.operations.ClipPath;
+import com.android.internal.widget.remotecompose.core.operations.ClipRect;
+import com.android.internal.widget.remotecompose.core.operations.DrawArc;
+import com.android.internal.widget.remotecompose.core.operations.DrawBitmap;
 import com.android.internal.widget.remotecompose.core.operations.DrawBitmapInt;
+import com.android.internal.widget.remotecompose.core.operations.DrawCircle;
+import com.android.internal.widget.remotecompose.core.operations.DrawLine;
+import com.android.internal.widget.remotecompose.core.operations.DrawOval;
+import com.android.internal.widget.remotecompose.core.operations.DrawPath;
+import com.android.internal.widget.remotecompose.core.operations.DrawRect;
+import com.android.internal.widget.remotecompose.core.operations.DrawRoundRect;
+import com.android.internal.widget.remotecompose.core.operations.DrawTextOnPath;
+import com.android.internal.widget.remotecompose.core.operations.DrawTextRun;
+import com.android.internal.widget.remotecompose.core.operations.DrawTweenPath;
 import com.android.internal.widget.remotecompose.core.operations.Header;
+import com.android.internal.widget.remotecompose.core.operations.MatrixRestore;
+import com.android.internal.widget.remotecompose.core.operations.MatrixRotate;
+import com.android.internal.widget.remotecompose.core.operations.MatrixSave;
+import com.android.internal.widget.remotecompose.core.operations.MatrixScale;
+import com.android.internal.widget.remotecompose.core.operations.MatrixSkew;
+import com.android.internal.widget.remotecompose.core.operations.MatrixTranslate;
+import com.android.internal.widget.remotecompose.core.operations.PaintData;
+import com.android.internal.widget.remotecompose.core.operations.PathData;
 import com.android.internal.widget.remotecompose.core.operations.RootContentBehavior;
 import com.android.internal.widget.remotecompose.core.operations.RootContentDescription;
 import com.android.internal.widget.remotecompose.core.operations.TextData;
@@ -48,7 +69,30 @@
     public static final int DATA_BITMAP = 101;
     public static final int DATA_TEXT = 102;
 
+/////////////////////////////=====================
+    public static final int CLIP_PATH = 38;
+    public static final int CLIP_RECT = 39;
+    public static final int PAINT_VALUES = 40;
+    public static final int DRAW_RECT = 42;
+    public static final int DRAW_TEXT_RUN = 43;
+    public static final int DRAW_CIRCLE = 46;
+    public static final int DRAW_LINE = 47;
+    public static final int DRAW_ROUND_RECT = 51;
+    public static final int DRAW_ARC = 52;
+    public static final int DRAW_TEXT_ON_PATH = 53;
+    public static final int DRAW_OVAL = 56;
+    public static final int DATA_PATH = 123;
+    public static final int DRAW_PATH = 124;
+    public static final int DRAW_TWEEN_PATH = 125;
+    public static final int MATRIX_SCALE = 126;
+    public static final int MATRIX_TRANSLATE = 127;
+    public static final int MATRIX_SKEW = 128;
+    public static final int MATRIX_ROTATE = 129;
+    public static final int MATRIX_SAVE = 130;
+    public static final int MATRIX_RESTORE = 131;
+    public static final int MATRIX_SET = 132;
 
+    /////////////////////////////////////////======================
     public static IntMap<CompanionOperation> map = new IntMap<>();
 
     static {
@@ -60,6 +104,29 @@
         map.put(CLICK_AREA, ClickArea.COMPANION);
         map.put(ROOT_CONTENT_BEHAVIOR, RootContentBehavior.COMPANION);
         map.put(ROOT_CONTENT_DESCRIPTION, RootContentDescription.COMPANION);
+
+        map.put(DRAW_ARC, DrawArc.COMPANION);
+        map.put(DRAW_BITMAP, DrawBitmap.COMPANION);
+        map.put(DRAW_CIRCLE, DrawCircle.COMPANION);
+        map.put(DRAW_LINE, DrawLine.COMPANION);
+        map.put(DRAW_OVAL, DrawOval.COMPANION);
+        map.put(DRAW_PATH, DrawPath.COMPANION);
+        map.put(DRAW_RECT, DrawRect.COMPANION);
+        map.put(DRAW_ROUND_RECT, DrawRoundRect.COMPANION);
+        map.put(DRAW_TEXT_ON_PATH, DrawTextOnPath.COMPANION);
+        map.put(DRAW_TEXT_RUN, DrawTextRun.COMPANION);
+        map.put(DRAW_TWEEN_PATH, DrawTweenPath.COMPANION);
+        map.put(DATA_PATH, PathData.COMPANION);
+        map.put(PAINT_VALUES, PaintData.COMPANION);
+        map.put(MATRIX_RESTORE, MatrixRestore.COMPANION);
+        map.put(MATRIX_ROTATE, MatrixRotate.COMPANION);
+        map.put(MATRIX_SAVE, MatrixSave.COMPANION);
+        map.put(MATRIX_SCALE, MatrixScale.COMPANION);
+        map.put(MATRIX_SKEW, MatrixSkew.COMPANION);
+        map.put(MATRIX_TRANSLATE, MatrixTranslate.COMPANION);
+        map.put(CLIP_PATH, ClipPath.COMPANION);
+        map.put(CLIP_RECT, ClipRect.COMPANION);
+
     }
 
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/PaintContext.java b/core/java/com/android/internal/widget/remotecompose/core/PaintContext.java
index 6999cde..eece8ad52 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/PaintContext.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/PaintContext.java
@@ -15,6 +15,8 @@
  */
 package com.android.internal.widget.remotecompose.core;
 
+import com.android.internal.widget.remotecompose.core.operations.paint.PaintBundle;
+
 /**
  * Specify an abstract paint context used by RemoteCompose commands to draw
  */
@@ -30,11 +32,74 @@
     }
 
     public abstract void drawBitmap(int imageId,
-                             int srcLeft, int srcTop, int srcRight, int srcBottom,
-                             int dstLeft, int dstTop, int dstRight, int dstBottom,
-                             int cdId);
+                                    int srcLeft, int srcTop, int srcRight, int srcBottom,
+                                    int dstLeft, int dstTop, int dstRight, int dstBottom,
+                                    int cdId);
 
     public abstract void scale(float scaleX, float scaleY);
+
     public abstract void translate(float translateX, float translateY);
+
+    public abstract void drawArc(float left,
+                                 float top,
+                                 float right,
+                                 float bottom,
+                                 float startAngle,
+                                 float sweepAngle);
+
+    public abstract void drawBitmap(int id, float left, float top, float right, float bottom);
+
+    public abstract void drawCircle(float centerX, float centerY, float radius);
+
+    public abstract void drawLine(float x1, float y1, float x2, float y2);
+
+    public abstract void drawOval(float left, float top, float right, float bottom);
+
+    public abstract void drawPath(int id, float start, float end);
+
+    public abstract void drawRect(float left, float top, float right, float bottom);
+
+    public abstract void drawRoundRect(float left,
+                                       float top,
+                                       float right,
+                                       float bottom,
+                                       float radiusX,
+                                       float radiusY);
+
+    public abstract void drawTextOnPath(int textId, int pathId, float hOffset, float vOffset);
+
+    public abstract void drawTextRun(int textID,
+                                     int start,
+                                     int end,
+                                     int contextStart,
+                                     int contextEnd,
+                                     float x,
+                                     float y,
+                                     boolean rtl);
+
+    public abstract void drawTweenPath(int path1Id,
+                                       int path2Id,
+                                       float tween,
+                                       float start,
+                                       float stop);
+
+    public abstract void applyPaint(PaintBundle mPaintData);
+
+    public abstract void mtrixScale(float scaleX, float scaleY, float centerX, float centerY);
+
+    public abstract void matrixTranslate(float translateX, float translateY);
+
+    public abstract void matrixSkew(float skewX, float skewY);
+
+    public abstract void matrixRotate(float rotate, float pivotX, float pivotY);
+
+    public abstract void matrixSave();
+
+    public abstract void matrixRestore();
+
+    public abstract void clipRect(float left, float top, float right, float bottom);
+
+    public abstract void clipPath(int pathId, int regionOp);
+
 }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/Platform.java b/core/java/com/android/internal/widget/remotecompose/core/Platform.java
index abda0c0..903dab4 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/Platform.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/Platform.java
@@ -20,5 +20,8 @@
  */
 public interface Platform {
     byte[] imageToByteArray(Object image);
+    int getImageWidth(Object image);
+    int getImageHeight(Object image);
+    float[] pathToFloatArray(Object image);
 }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java
index c34730f..c2e8131 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java
@@ -17,12 +17,34 @@
 
 import com.android.internal.widget.remotecompose.core.operations.BitmapData;
 import com.android.internal.widget.remotecompose.core.operations.ClickArea;
+import com.android.internal.widget.remotecompose.core.operations.ClipPath;
+import com.android.internal.widget.remotecompose.core.operations.ClipRect;
+import com.android.internal.widget.remotecompose.core.operations.DrawArc;
+import com.android.internal.widget.remotecompose.core.operations.DrawBitmap;
 import com.android.internal.widget.remotecompose.core.operations.DrawBitmapInt;
+import com.android.internal.widget.remotecompose.core.operations.DrawCircle;
+import com.android.internal.widget.remotecompose.core.operations.DrawLine;
+import com.android.internal.widget.remotecompose.core.operations.DrawOval;
+import com.android.internal.widget.remotecompose.core.operations.DrawPath;
+import com.android.internal.widget.remotecompose.core.operations.DrawRect;
+import com.android.internal.widget.remotecompose.core.operations.DrawRoundRect;
+import com.android.internal.widget.remotecompose.core.operations.DrawTextOnPath;
+import com.android.internal.widget.remotecompose.core.operations.DrawTextRun;
+import com.android.internal.widget.remotecompose.core.operations.DrawTweenPath;
 import com.android.internal.widget.remotecompose.core.operations.Header;
+import com.android.internal.widget.remotecompose.core.operations.MatrixRestore;
+import com.android.internal.widget.remotecompose.core.operations.MatrixRotate;
+import com.android.internal.widget.remotecompose.core.operations.MatrixSave;
+import com.android.internal.widget.remotecompose.core.operations.MatrixScale;
+import com.android.internal.widget.remotecompose.core.operations.MatrixSkew;
+import com.android.internal.widget.remotecompose.core.operations.MatrixTranslate;
+import com.android.internal.widget.remotecompose.core.operations.PaintData;
+import com.android.internal.widget.remotecompose.core.operations.PathData;
 import com.android.internal.widget.remotecompose.core.operations.RootContentBehavior;
 import com.android.internal.widget.remotecompose.core.operations.RootContentDescription;
 import com.android.internal.widget.remotecompose.core.operations.TextData;
 import com.android.internal.widget.remotecompose.core.operations.Theme;
+import com.android.internal.widget.remotecompose.core.operations.paint.PaintBundle;
 
 import java.io.File;
 import java.io.FileInputStream;
@@ -82,10 +104,10 @@
     /**
      * Insert a header
      *
-     * @param width        the width of the document in pixels
-     * @param height       the height of the document in pixels
+     * @param width              the width of the document in pixels
+     * @param height             the height of the document in pixels
      * @param contentDescription content description of the document
-     * @param capabilities bitmask indicating needed capabilities (unused for now)
+     * @param capabilities       bitmask indicating needed capabilities (unused for now)
      */
     public void header(int width, int height, String contentDescription, long capabilities) {
         Header.COMPANION.apply(mBuffer, width, height, capabilities);
@@ -99,8 +121,8 @@
     /**
      * Insert a header
      *
-     * @param width  the width of the document in pixels
-     * @param height the height of the document in pixels
+     * @param width              the width of the document in pixels
+     * @param height             the height of the document in pixels
      * @param contentDescription content description of the document
      */
     public void header(int width, int height, String contentDescription) {
@@ -111,7 +133,7 @@
      * Insert a bitmap
      *
      * @param image       an opaque image that we'll add to the buffer
-     * @param imageWidth the width of the image
+     * @param imageWidth  the width of the image
      * @param imageHeight the height of the image
      * @param srcLeft     left coordinate of the source area
      * @param srcTop      top coordinate of the source area
@@ -161,13 +183,13 @@
     /**
      * Add a click area to the document
      *
-     * @param id       the id of the click area, reported in the click listener callback
+     * @param id                 the id of the click area, reported in the click listener callback
      * @param contentDescription the content description of that click area (accessibility)
-     * @param left     left coordinate of the area bounds
-     * @param top      top coordinate of the area bounds
-     * @param right    right coordinate of the area bounds
-     * @param bottom   bottom coordinate of the area bounds
-     * @param metadata associated metadata, user-provided
+     * @param left               left coordinate of the area bounds
+     * @param top                top coordinate of the area bounds
+     * @param right              right coordinate of the area bounds
+     * @param bottom             bottom coordinate of the area bounds
+     * @param metadata           associated metadata, user-provided
      */
     public void addClickArea(
             int id,
@@ -193,35 +215,294 @@
     /**
      * Sets the way the player handles the content
      *
-     * @param scroll set the horizontal behavior (NONE|SCROLL_HORIZONTAL|SCROLL_VERTICAL)
+     * @param scroll    set the horizontal behavior (NONE|SCROLL_HORIZONTAL|SCROLL_VERTICAL)
      * @param alignment set the alignment of the content (TOP|CENTER|BOTTOM|START|END)
-     * @param sizing set the type of sizing for the content (NONE|SIZING_LAYOUT|SIZING_SCALE)
-     * @param mode set the mode of sizing, either LAYOUT modes or SCALE modes
-     *             the LAYOUT modes are:
-     *             - LAYOUT_MATCH_PARENT
-     *             - LAYOUT_WRAP_CONTENT
-     *             or adding an horizontal mode and a vertical mode:
-     *             - LAYOUT_HORIZONTAL_MATCH_PARENT
-     *             - LAYOUT_HORIZONTAL_WRAP_CONTENT
-     *             - LAYOUT_HORIZONTAL_FIXED
-     *             - LAYOUT_VERTICAL_MATCH_PARENT
-     *             - LAYOUT_VERTICAL_WRAP_CONTENT
-     *             - LAYOUT_VERTICAL_FIXED
-     *             The LAYOUT_*_FIXED modes will use the intrinsic document size
+     * @param sizing    set the type of sizing for the content (NONE|SIZING_LAYOUT|SIZING_SCALE)
+     * @param mode      set the mode of sizing, either LAYOUT modes or SCALE modes
+     *                  the LAYOUT modes are:
+     *                  - LAYOUT_MATCH_PARENT
+     *                  - LAYOUT_WRAP_CONTENT
+     *                  or adding an horizontal mode and a vertical mode:
+     *                  - LAYOUT_HORIZONTAL_MATCH_PARENT
+     *                  - LAYOUT_HORIZONTAL_WRAP_CONTENT
+     *                  - LAYOUT_HORIZONTAL_FIXED
+     *                  - LAYOUT_VERTICAL_MATCH_PARENT
+     *                  - LAYOUT_VERTICAL_WRAP_CONTENT
+     *                  - LAYOUT_VERTICAL_FIXED
+     *                  The LAYOUT_*_FIXED modes will use the intrinsic document size
      */
     public void setRootContentBehavior(int scroll, int alignment, int sizing, int mode) {
         RootContentBehavior.COMPANION.apply(mBuffer, scroll, alignment, sizing, mode);
     }
 
+    /**
+     * add Drawing the specified arc, which will be scaled to fit inside the specified oval.
+     * <br>
+     * If the start angle is negative or >= 360, the start angle is treated as start angle modulo
+     * 360.
+     * <br>
+     * If the sweep angle is >= 360, then the oval is drawn completely. Note that this differs
+     * slightly from SkPath::arcTo, which treats the sweep angle modulo 360. If the sweep angle is
+     * negative, the sweep angle is treated as sweep angle modulo 360
+     * <br>
+     * The arc is drawn clockwise. An angle of 0 degrees correspond to the geometric angle of 0
+     * degrees (3 o'clock on a watch.)
+     * <br>
+     *
+     * @param left       left coordinate of oval used to define the shape and size of the arc
+     * @param top        top coordinate of oval used to define the shape and size of the arc
+     * @param right      right coordinate of oval used to define the shape and size of the arc
+     * @param bottom     bottom coordinate of oval used to define the shape and size of the arc
+     * @param startAngle Starting angle (in degrees) where the arc begins
+     * @param sweepAngle Sweep angle (in degrees) measured clockwise
+     */
+    public void addDrawArc(float left,
+                           float top,
+                           float right,
+                           float bottom,
+                           float startAngle,
+                           float sweepAngle) {
+        DrawArc.COMPANION.apply(mBuffer, left, top, right, bottom, startAngle, sweepAngle);
+    }
+
+    /**
+     * @param image              The bitmap to be drawn
+     * @param left               left coordinate of rectangle that the bitmap will be to fit into
+     * @param top                top coordinate of rectangle that the bitmap will be to fit into
+     * @param right              right coordinate of rectangle that the bitmap will be to fit into
+     * @param bottom             bottom coordinate of rectangle that the bitmap will be to fit into
+     * @param contentDescription content description of the image
+     */
+    public void addDrawBitmap(Object image,
+                              float left,
+                              float top,
+                              float right,
+                              float bottom,
+                              String contentDescription) {
+        int imageId = mRemoteComposeState.dataGetId(image);
+        if (imageId == -1) {
+            imageId = mRemoteComposeState.cache(image);
+            byte[] data = mPlatform.imageToByteArray(image);
+            int imageWidth = mPlatform.getImageWidth(image);
+            int imageHeight = mPlatform.getImageHeight(image);
+
+            BitmapData.COMPANION.apply(mBuffer, imageId, imageWidth, imageHeight, data);
+        }
+        int contentDescriptionId = 0;
+        if (contentDescription != null) {
+            contentDescriptionId = addText(contentDescription);
+        }
+        DrawBitmap.COMPANION.apply(
+                mBuffer, imageId, left, top, right, bottom, contentDescriptionId
+        );
+    }
+
+    /**
+     * Draw the specified circle using the specified paint. If radius is <= 0, then nothing will be
+     * drawn.
+     *
+     * @param centerX The x-coordinate of the center of the circle to be drawn
+     * @param centerY The y-coordinate of the center of the circle to be drawn
+     * @param radius  The radius of the circle to be drawn
+     */
+    public void addDrawCircle(float centerX, float centerY, float radius) {
+        DrawCircle.COMPANION.apply(mBuffer, centerX, centerY, radius);
+    }
+
+    /**
+     * Draw a line segment with the specified start and stop x,y coordinates, using the specified
+     * paint.
+     *
+     * @param x1 The x-coordinate of the start point of the line
+     * @param y1 The y-coordinate of the start point of the line
+     * @param x2 The x-coordinate of the end point of the line
+     * @param y2 The y-coordinate of the end point of the line
+     */
+    public void addDrawLine(float x1, float y1, float x2, float y2) {
+        DrawLine.COMPANION.apply(mBuffer, x1, y1, x2, y2);
+    }
+
+    /**
+     * Draw the specified oval using the specified paint.
+     *
+     * @param left   left coordinate of oval
+     * @param top    top coordinate of oval
+     * @param right  right coordinate of oval
+     * @param bottom bottom coordinate of oval
+     */
+    public void addDrawOval(float left, float top, float right, float bottom) {
+        DrawOval.COMPANION.apply(mBuffer, left, top, right, bottom);
+    }
+
+    /**
+     * Draw the specified path
+     * <p>
+     * Note: path objects are not immutable
+     * modifying them and calling this will not change the drawing
+     *
+     * @param path The path to be drawn
+     */
+    public void addDrawPath(Object path) {
+        int id = mRemoteComposeState.dataGetId(path);
+        if (id == -1) { // never been seen before
+            id = addPathData(path);
+        }
+        addDrawPath(id);
+    }
+
+
+    /**
+     * Draw the specified path
+     *
+     * @param pathId
+     */
+    public void addDrawPath(int pathId) {
+        DrawPath.COMPANION.apply(mBuffer, pathId);
+    }
+
+    /**
+     * Draw the specified Rect
+     *
+     * @param left   left coordinate of rectangle to be drawn
+     * @param top    top coordinate of rectangle to be drawn
+     * @param right  right coordinate of rectangle to be drawn
+     * @param bottom bottom coordinate of rectangle to be drawn
+     */
+    public void addDrawRect(float left, float top, float right, float bottom) {
+        DrawRect.COMPANION.apply(mBuffer, left, top, right, bottom);
+    }
+
+    /**
+     * Draw the specified round-rect
+     *
+     * @param left    left coordinate of rectangle to be drawn
+     * @param top     left coordinate of rectangle to be drawn
+     * @param right   left coordinate of rectangle to be drawn
+     * @param bottom  left coordinate of rectangle to be drawn
+     * @param radiusX The x-radius of the oval used to round the corners
+     * @param radiusY The y-radius of the oval used to round the corners
+     */
+    public void addDrawRoundRect(float left, float top, float right, float bottom,
+                                 float radiusX, float radiusY) {
+        DrawRoundRect.COMPANION.apply(mBuffer, left, top, right, bottom, radiusX, radiusY);
+    }
+
+    /**
+     * Draw the text, with origin at (x,y) along the specified path.
+     *
+     * @param text    The text to be drawn
+     * @param path    The path the text should follow for its baseline
+     * @param hOffset The distance along the path to add to the text's starting position
+     * @param vOffset The distance above(-) or below(+) the path to position the text
+     */
+    public void addDrawTextOnPath(String text, Object path, float hOffset, float vOffset) {
+        int pathId = mRemoteComposeState.dataGetId(path);
+        if (pathId == -1) { // never been seen before
+            pathId = addPathData(path);
+        }
+        int textId = addText(text);
+        DrawTextOnPath.COMPANION.apply(mBuffer, textId, pathId, hOffset, vOffset);
+    }
+
+    /**
+     * Draw the text, with origin at (x,y). The origin is interpreted
+     * based on the Align setting in the paint.
+     *
+     * @param text         The text to be drawn
+     * @param start        The index of the first character in text to draw
+     * @param end          (end - 1) is the index of the last character in text to draw
+     * @param contextStart
+     * @param contextEnd
+     * @param x            The x-coordinate of the origin of the text being drawn
+     * @param y            The y-coordinate of the baseline of the text being drawn
+     * @param rtl          Draw RTTL
+     */
+    public void addDrawTextRun(String text,
+                               int start,
+                               int end,
+                               int contextStart,
+                               int contextEnd,
+                               float x,
+                               float y,
+                               boolean rtl) {
+        int textId = addText(text);
+        DrawTextRun.COMPANION.apply(
+                mBuffer, textId, start, end,
+                contextStart, contextEnd, x, y, rtl);
+    }
+
+    /**
+     * draw an interpolation between two paths that have the same pattern
+     * <p>
+     * Warning paths objects are not immutable and this is not taken into consideration
+     *
+     * @param path1 The path1 to be drawn between
+     * @param path2 The path2 to be drawn between
+     * @param tween The ratio of path1 and path2 to 0 = all path 1, 1 = all path2
+     * @param start The start of the subrange of paths to draw 0 = start form start 0.5 is half way
+     * @param stop  The end of the subrange of paths to draw 1 = end at the end 0.5 is end half way
+     */
+    public void addDrawTweenPath(Object path1,
+                                 Object path2,
+                                 float tween,
+                                 float start,
+                                 float stop) {
+        int path1Id = mRemoteComposeState.dataGetId(path1);
+        if (path1Id == -1) { // never been seen before
+            path1Id = addPathData(path1);
+        }
+        int path2Id = mRemoteComposeState.dataGetId(path2);
+        if (path2Id == -1) { // never been seen before
+            path2Id = addPathData(path2);
+        }
+        addDrawTweenPath(path1Id, path2Id, tween, start, stop);
+    }
+
+    /**
+     * draw an interpolation between two paths that have the same pattern
+     *
+     * @param path1Id The path1 to be drawn between
+     * @param path2Id The path2 to be drawn between
+     * @param tween   The ratio of path1 and path2 to 0 = all path 1, 1 = all path2
+     * @param start   The start of the subrange of paths to draw 0 = start form start .5 is 1/2 way
+     * @param stop    The end of the subrange of paths to draw 1 = end at the end .5 is end 1/2 way
+     */
+    public void addDrawTweenPath(int path1Id,
+                                 int path2Id,
+                                 float tween,
+                                 float start,
+                                 float stop) {
+        DrawTweenPath.COMPANION.apply(
+                mBuffer, path1Id, path2Id,
+                tween, start, stop);
+    }
+
+    /**
+     * Add a path object
+     *
+     * @param path
+     * @return the id of the path on the wire
+     */
+    public int addPathData(Object path) {
+        float[] pathData = mPlatform.pathToFloatArray(path);
+        int id = mRemoteComposeState.cache(path);
+        PathData.COMPANION.apply(mBuffer, id, pathData);
+        return id;
+    }
+
+    public void addPaint(PaintBundle paint) {
+        PaintData.COMPANION.apply(mBuffer, paint);
+    }
     ///////////////////////////////////////////////////////////////////////////////////////////////
 
     public void inflateFromBuffer(ArrayList<Operation> operations) {
         mBuffer.setIndex(0);
         while (mBuffer.available()) {
             int opId = mBuffer.readByte();
+            System.out.println(">>> " + opId);
             CompanionOperation operation = Operations.map.get(opId);
             if (operation == null) {
-                throw new RuntimeException("Unknown operation encountered");
+                throw new RuntimeException("Unknown operation encountered " + opId);
             }
             operation.read(mBuffer, operations);
         }
@@ -259,7 +540,7 @@
     }
 
     public static RemoteComposeBuffer fromInputStream(InputStream inputStream,
-                                               RemoteComposeState remoteComposeState) {
+                                                      RemoteComposeState remoteComposeState) {
         RemoteComposeBuffer buffer = new RemoteComposeBuffer(remoteComposeState);
         read(inputStream, buffer);
         return buffer;
@@ -318,5 +599,86 @@
         }
     }
 
+    /**
+     * add a Pre-concat the current matrix with the specified skew.
+     *
+     * @param skewX The amount to skew in X
+     * @param skewY The amount to skew in Y
+     */
+    public void addMatrixSkew(float skewX, float skewY) {
+        MatrixSkew.COMPANION.apply(mBuffer, skewX, skewY);
+    }
+
+    /**
+     * This call balances a previous call to save(), and is used to remove all
+     * modifications to the matrix/clip state since the last save call.
+     * Do not call restore() more times than save() was called.
+     */
+    public void addMatrixRestore() {
+        MatrixRestore.COMPANION.apply(mBuffer);
+    }
+
+    /**
+     * Add a saves the current matrix and clip onto a private stack.
+     * <p>
+     * Subsequent calls to translate,scale,rotate,skew,concat or clipRect,
+     * clipPath will all operate as usual, but when the balancing call to
+     * restore() is made, those calls will be forgotten, and the settings that
+     * existed before the save() will be reinstated.
+     */
+    public void addMatrixSave() {
+        MatrixSave.COMPANION.apply(mBuffer);
+    }
+
+    /**
+     * add a pre-concat the current matrix with the specified rotation.
+     *
+     * @param angle   The amount to rotate, in degrees
+     * @param centerX The x-coord for the pivot point (unchanged by the rotation)
+     * @param centerY The y-coord for the pivot point (unchanged by the rotation)
+     */
+    public void addMatrixRotate(float angle, float centerX, float centerY) {
+        MatrixRotate.COMPANION.apply(mBuffer, angle, centerX, centerY);
+    }
+
+    /**
+     * add a Pre-concat to the current matrix with the specified translation
+     *
+     * @param dx The distance to translate in X
+     * @param dy The distance to translate in Y
+     */
+    public void addMatrixTranslate(float dx, float dy) {
+        MatrixTranslate.COMPANION.apply(mBuffer, dx, dy);
+    }
+
+    /**
+     * Add a pre-concat of the current matrix with the specified scale.
+     *
+     * @param scaleX  The amount to scale in X
+     * @param scaleY  The amount to scale in Y
+     */
+    public void addMatrixScale(float scaleX, float scaleY) {
+        MatrixScale.COMPANION.apply(mBuffer, scaleX, scaleY, Float.NaN, Float.NaN);
+    }
+
+    /**
+     * Add a pre-concat of the current matrix with the specified scale.
+     *
+     * @param scaleX  The amount to scale in X
+     * @param scaleY  The amount to scale in Y
+     * @param centerX The x-coord for the pivot point (unchanged by the scale)
+     * @param centerY The y-coord for the pivot point (unchanged by the scale)
+     */
+    public void addMatrixScale(float scaleX, float scaleY, float centerX, float centerY) {
+        MatrixScale.COMPANION.apply(mBuffer, scaleX, scaleY, centerX, centerY);
+    }
+
+    public void addClipPath(int pathId) {
+        ClipPath.COMPANION.apply(mBuffer, pathId);
+    }
+
+    public void addClipRect(float left, float top, float right, float bottom) {
+        ClipRect.COMPANION.apply(mBuffer, left, top, right, bottom);
+    }
 }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java b/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java
index 1b7c6fd..d16cbc5 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java
@@ -37,6 +37,8 @@
     public float mWidth = 0f;
     public float mHeight = 0f;
 
+    public abstract void loadPathData(int instanceId, float[] floatPath);
+
     /**
      * The context can be used in a few different mode, allowing operations to skip being executed:
      * - UNSET : all operations will get executed
diff --git a/core/java/com/android/internal/widget/remotecompose/core/WireBuffer.java b/core/java/com/android/internal/widget/remotecompose/core/WireBuffer.java
index 7c9fda5..fc3202e 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/WireBuffer.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/WireBuffer.java
@@ -37,7 +37,7 @@
         this(BUFFER_SIZE);
     }
 
-    public void resize(int need) {
+    private void resize(int need) {
         if (mSize + need >= mMaxSize) {
             mMaxSize = Math.max(mMaxSize * 2, mSize + need);
             mBuffer = Arrays.copyOf(mBuffer, mMaxSize);
@@ -120,7 +120,7 @@
     }
 
     public int readByte() {
-        byte value = mBuffer[mIndex];
+        int value = 0xFF & mBuffer[mIndex];
         mIndex++;
         return value;
     }
@@ -130,6 +130,14 @@
         int v2 = (mBuffer[mIndex++] & 0xFF) << 0;
         return v1 + v2;
     }
+    public int peekInt() {
+        int tmp = mIndex;
+        int v1 = (mBuffer[tmp++] & 0xFF) << 24;
+        int v2 = (mBuffer[tmp++] & 0xFF) << 16;
+        int v3 = (mBuffer[tmp++] & 0xFF) << 8;
+        int v4 = (mBuffer[tmp++] & 0xFF) << 0;
+        return v1 + v2 + v3 + v4;
+    }
 
     public int readInt() {
         int v1 = (mBuffer[mIndex++] & 0xFF) << 24;
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/ClipPath.java b/core/java/com/android/internal/widget/remotecompose/core/operations/ClipPath.java
new file mode 100644
index 0000000..8d4a787
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/ClipPath.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2024 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.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class ClipPath extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+    int mId;
+    int mRegionOp;
+
+    public ClipPath(int pathId, int regionOp) {
+        mId = pathId;
+        mRegionOp = regionOp;
+    }
+
+    public static final int REPLACE = Companion.PATH_CLIP_REPLACE;
+    public static final int DIFFERENCE = Companion.PATH_CLIP_DIFFERENCE;
+    public static final int INTERSECT = Companion.PATH_CLIP_INTERSECT;
+    public static final int UNION = Companion.PATH_CLIP_UNION;
+    public static final int XOR = Companion.PATH_CLIP_XOR;
+    public static final int REVERSE_DIFFERENCE = Companion.PATH_CLIP_REVERSE_DIFFERENCE;
+    public static final int UNDEFINED = Companion.PATH_CLIP_UNDEFINED;
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mId);
+    }
+
+    @Override
+    public String toString() {
+        return "ClipPath " + mId + ";";
+    }
+
+    public static class Companion implements CompanionOperation {
+        public static final int PATH_CLIP_REPLACE = 0;
+        public static final int PATH_CLIP_DIFFERENCE = 1;
+        public static final int PATH_CLIP_INTERSECT = 2;
+        public static final int PATH_CLIP_UNION = 3;
+        public static final int PATH_CLIP_XOR = 4;
+        public static final int PATH_CLIP_REVERSE_DIFFERENCE = 5;
+        public static final int PATH_CLIP_UNDEFINED = 6;
+
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            int pack = buffer.readInt();
+            int id = pack & 0xFFFFF;
+            int regionOp = pack >> 24;
+            ClipPath op = new ClipPath(id, regionOp);
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "ClipPath";
+        }
+
+        @Override
+        public int id() {
+            return Operations.CLIP_PATH;
+        }
+
+        public void apply(WireBuffer buffer, int id) {
+            buffer.start(Operations.CLIP_PATH);
+            buffer.writeInt(id);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.clipPath(mId, mRegionOp);
+    }
+}
+
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/ClipRect.java b/core/java/com/android/internal/widget/remotecompose/core/operations/ClipRect.java
new file mode 100644
index 0000000..803618a
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/ClipRect.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2024 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.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class ClipRect extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+    float mLeft;
+    float mTop;
+    float mRight;
+    float mBottom;
+
+    public ClipRect(
+            float left,
+            float top,
+            float right,
+            float bottom) {
+        mLeft = left;
+        mTop = top;
+        mRight = right;
+        mBottom = bottom;
+
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mLeft, mTop, mRight, mBottom);
+    }
+
+    @Override
+    public String toString() {
+        return "ClipRect " + mLeft + " " + mTop
+                + " " + mRight + " " + mBottom + ";";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            float sLeft = buffer.readFloat();
+            float srcTop = buffer.readFloat();
+            float srcRight = buffer.readFloat();
+            float srcBottom = buffer.readFloat();
+
+            ClipRect op = new ClipRect(sLeft, srcTop, srcRight, srcBottom);
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "ClipRect";
+        }
+
+        @Override
+        public int id() {
+            return Operations.CLIP_RECT;
+        }
+
+        public void apply(WireBuffer buffer,
+                          float left,
+                          float top,
+                          float right,
+                          float bottom) {
+            buffer.start(Operations.CLIP_RECT);
+            buffer.writeFloat(left);
+            buffer.writeFloat(top);
+            buffer.writeFloat(right);
+            buffer.writeFloat(bottom);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.clipRect(mLeft,
+                mTop,
+                mRight,
+                mBottom);
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawArc.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawArc.java
new file mode 100644
index 0000000..e829975
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawArc.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2024 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.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class DrawArc extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+    float mLeft;
+    float mTop;
+    float mRight;
+    float mBottom;
+    float mStartAngle;
+    float mSweepAngle;
+
+    public DrawArc(
+            float left,
+            float top,
+            float right,
+            float bottom,
+            float startAngle,
+            float sweepAngle) {
+        mLeft = left;
+        mTop = top;
+        mRight = right;
+        mBottom = bottom;
+        mStartAngle = startAngle;
+        mSweepAngle = sweepAngle;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mLeft,
+                mTop,
+                mRight,
+                mBottom,
+                mStartAngle,
+                mSweepAngle);
+    }
+
+    @Override
+    public String toString() {
+        return "DrawArc " + mLeft + " " + mTop
+                + " " + mRight + " " + mBottom + " "
+                + "- " + mStartAngle + " " + mSweepAngle + ";";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            float sLeft = buffer.readFloat();
+            float srcTop = buffer.readFloat();
+            float srcRight = buffer.readFloat();
+            float srcBottom = buffer.readFloat();
+            float mStartAngle = buffer.readFloat();
+            float mSweepAngle = buffer.readFloat();
+            DrawArc op = new DrawArc(sLeft, srcTop, srcRight, srcBottom,
+                    mStartAngle, mSweepAngle);
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "DrawArc";
+        }
+
+        @Override
+        public int id() {
+            return Operations.DRAW_ARC;
+        }
+
+        public void apply(WireBuffer buffer,
+                          float left,
+                          float top,
+                          float right,
+                          float bottom,
+                          float startAngle,
+                          float sweepAngle) {
+            buffer.start(Operations.DRAW_ARC);
+            buffer.writeFloat(left);
+            buffer.writeFloat(top);
+            buffer.writeFloat(right);
+            buffer.writeFloat(bottom);
+            buffer.writeFloat(startAngle);
+            buffer.writeFloat(sweepAngle);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.drawArc(mLeft,
+                mTop,
+                mRight,
+                mBottom,
+                mStartAngle,
+                mSweepAngle);
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBitmap.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBitmap.java
new file mode 100644
index 0000000..2e971f5
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBitmap.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2024 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.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class DrawBitmap extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+    float mLeft;
+    float mTop;
+    float mRight;
+    float mBottom;
+    int mId;
+    int mDescriptionId = 0;
+
+    public DrawBitmap(
+            int imageId,
+            float left,
+            float top,
+            float right,
+            float bottom,
+            int descriptionId) {
+        mLeft = left;
+        mTop = top;
+        mRight = right;
+        mBottom = bottom;
+        mId = imageId;
+        mDescriptionId = descriptionId;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mId, mLeft, mTop, mRight, mBottom, mDescriptionId);
+    }
+
+    @Override
+    public String toString() {
+        return "DrawBitmap (desc=" + mDescriptionId + ")" + mLeft + " " + mTop
+                + " " + mRight + " " + mBottom + ";";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            int id = buffer.readInt();
+            float sLeft = buffer.readFloat();
+            float srcTop = buffer.readFloat();
+            float srcRight = buffer.readFloat();
+            float srcBottom = buffer.readFloat();
+            int discriptionId = buffer.readInt();
+
+            DrawBitmap op = new DrawBitmap(id, sLeft, srcTop, srcRight, srcBottom, discriptionId);
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "DrawOval";
+        }
+
+        @Override
+        public int id() {
+            return Operations.DRAW_BITMAP;
+        }
+
+        public void apply(WireBuffer buffer,
+                          int id,
+                          float left,
+                          float top,
+                          float right,
+                          float bottom,
+                          int descriptionId) {
+            buffer.start(Operations.DRAW_BITMAP);
+            buffer.writeInt(id);
+            buffer.writeFloat(left);
+            buffer.writeFloat(top);
+            buffer.writeFloat(right);
+            buffer.writeFloat(bottom);
+            buffer.writeInt(descriptionId);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.drawBitmap(mId, mLeft,
+                mTop,
+                mRight,
+                mBottom);
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBitmapInt.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBitmapInt.java
index 3fbdf94..c2a56e7 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBitmapInt.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBitmapInt.java
@@ -76,7 +76,8 @@
     }
 
     public static class Companion implements CompanionOperation {
-        private Companion() {}
+        private Companion() {
+        }
 
         @Override
         public String name() {
@@ -89,9 +90,9 @@
         }
 
         public void apply(WireBuffer buffer, int imageId,
-                   int srcLeft, int srcTop, int srcRight, int srcBottom,
-                   int dstLeft, int dstTop, int dstRight, int dstBottom,
-                   int cdId) {
+                          int srcLeft, int srcTop, int srcRight, int srcBottom,
+                          int dstLeft, int dstTop, int dstRight, int dstBottom,
+                          int cdId) {
             buffer.start(Operations.DRAW_BITMAP_INT);
             buffer.writeInt(imageId);
             buffer.writeInt(srcLeft);
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawCircle.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawCircle.java
new file mode 100644
index 0000000..9ce754d
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawCircle.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2024 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.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class DrawCircle extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+    float mCenterX;
+    float mCenterY;
+    float mRadius;
+
+    public DrawCircle(float centerX, float centerY, float radius) {
+        mCenterX = centerX;
+        mCenterY = centerY;
+        mRadius = radius;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mCenterX,
+                mCenterY,
+                mRadius);
+    }
+
+    @Override
+    public String toString() {
+        return "";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            float centerX = buffer.readFloat();
+            float centerY = buffer.readFloat();
+            float radius = buffer.readFloat();
+
+            DrawCircle op = new DrawCircle(centerX, centerY, radius);
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "";
+        }
+
+        @Override
+        public int id() {
+            return 0;
+        }
+
+        public void apply(WireBuffer buffer, float centerX, float centerY, float radius) {
+            buffer.start(Operations.DRAW_CIRCLE);
+            buffer.writeFloat(centerX);
+            buffer.writeFloat(centerY);
+            buffer.writeFloat(radius);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.drawCircle(mCenterX,
+                mCenterY,
+                mRadius);
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawLine.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawLine.java
new file mode 100644
index 0000000..c7a8315
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawLine.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2024 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.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class DrawLine extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+    float mX1;
+    float mY1;
+    float mX2;
+    float mY2;
+
+    public DrawLine(
+            float x1,
+            float y1,
+            float x2,
+            float y2) {
+        mX1 = x1;
+        mY1 = y1;
+        mX2 = x2;
+        mY2 = y2;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mX1,
+                mY1,
+                mX2,
+                mY2);
+    }
+
+    @Override
+    public String toString() {
+        return "DrawArc " + mX1 + " " + mY1
+                + " " + mX2 + " " + mY2 + ";";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            float x1 = buffer.readFloat();
+            float y1 = buffer.readFloat();
+            float x2 = buffer.readFloat();
+            float y2 = buffer.readFloat();
+
+            DrawLine op = new DrawLine(x1, y1, x2, y2);
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "DrawLine";
+        }
+
+        @Override
+        public int id() {
+            return Operations.DRAW_LINE;
+        }
+
+        public void apply(WireBuffer buffer,
+                          float x1,
+                          float y1,
+                          float x2,
+                          float y2) {
+            buffer.start(Operations.DRAW_LINE);
+            buffer.writeFloat(x1);
+            buffer.writeFloat(y1);
+            buffer.writeFloat(x2);
+            buffer.writeFloat(y2);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.drawLine(mX1,
+                mY1,
+                mX2,
+                mY2);
+    }
+
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawOval.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawOval.java
new file mode 100644
index 0000000..7143753
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawOval.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2024 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.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class DrawOval extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+    float mLeft;
+    float mTop;
+    float mRight;
+    float mBottom;
+
+
+    public DrawOval(
+            float left,
+            float top,
+            float right,
+            float bottom) {
+        mLeft = left;
+        mTop = top;
+        mRight = right;
+        mBottom = bottom;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mLeft, mTop, mRight, mBottom);
+    }
+
+    @Override
+    public String toString() {
+        return "DrawOval " + mLeft + " " + mTop
+                + " " + mRight + " " + mBottom + ";";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            float sLeft = buffer.readFloat();
+            float srcTop = buffer.readFloat();
+            float srcRight = buffer.readFloat();
+            float srcBottom = buffer.readFloat();
+
+            DrawOval op = new DrawOval(sLeft, srcTop, srcRight, srcBottom);
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "DrawOval";
+        }
+
+        @Override
+        public int id() {
+            return Operations.DRAW_OVAL;
+        }
+
+        public void apply(WireBuffer buffer,
+                          float left,
+                          float top,
+                          float right,
+                          float bottom) {
+            buffer.start(Operations.DRAW_OVAL);
+            buffer.writeFloat(left);
+            buffer.writeFloat(top);
+            buffer.writeFloat(right);
+            buffer.writeFloat(bottom);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.drawOval(mLeft,
+                mTop,
+                mRight,
+                mBottom);
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawPath.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawPath.java
new file mode 100644
index 0000000..7b8a9e9
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawPath.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2024 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.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class DrawPath extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+    int mId;
+    float mStart = 0;
+    float mEnd = 1;
+
+    public DrawPath(int pathId) {
+        mId = pathId;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mId);
+    }
+
+    @Override
+    public String toString() {
+        return "DrawPath " + ";";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            int id = buffer.readInt();
+            DrawPath op = new DrawPath(id);
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "DrawPath";
+        }
+
+        @Override
+        public int id() {
+            return Operations.DRAW_PATH;
+        }
+
+        public void apply(WireBuffer buffer, int id) {
+            buffer.start(Operations.DRAW_PATH);
+            buffer.writeInt(id);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.drawPath(mId, mStart, mEnd);
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawRect.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawRect.java
new file mode 100644
index 0000000..4775241
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawRect.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2024 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.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class DrawRect extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+    float mLeft;
+    float mTop;
+    float mRight;
+    float mBottom;
+
+    public DrawRect(
+            float left,
+            float top,
+            float right,
+            float bottom) {
+        mLeft = left;
+        mTop = top;
+        mRight = right;
+        mBottom = bottom;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mLeft, mTop, mRight, mBottom);
+    }
+
+    @Override
+    public String toString() {
+        return "DrawRect " + mLeft + " " + mTop
+                + " " + mRight + " " + mBottom + ";";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            float sLeft = buffer.readFloat();
+            float srcTop = buffer.readFloat();
+            float srcRight = buffer.readFloat();
+            float srcBottom = buffer.readFloat();
+
+            DrawRect op = new DrawRect(sLeft, srcTop, srcRight, srcBottom);
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "DrawRect";
+        }
+
+        @Override
+        public int id() {
+            return Operations.DRAW_RECT;
+        }
+
+        public void apply(WireBuffer buffer,
+                          float left,
+                          float top,
+                          float right,
+                          float bottom) {
+            buffer.start(Operations.DRAW_RECT);
+            buffer.writeFloat(left);
+            buffer.writeFloat(top);
+            buffer.writeFloat(right);
+            buffer.writeFloat(bottom);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.drawRect(mLeft,
+                mTop,
+                mRight,
+                mBottom);
+    }
+
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawRoundRect.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawRoundRect.java
new file mode 100644
index 0000000..8da16e7
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawRoundRect.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2024 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.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class DrawRoundRect extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+    float mLeft;
+    float mTop;
+    float mRight;
+    float mBottom;
+    float mRadiusX;
+    float mRadiusY;
+
+    public DrawRoundRect(
+            float left,
+            float top,
+            float right,
+            float bottom,
+            float radiusX,
+            float radiusY) {
+        mLeft = left;
+        mTop = top;
+        mRight = right;
+        mBottom = bottom;
+        mRadiusX = radiusX;
+        mRadiusY = radiusY;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mLeft, mTop, mRight, mBottom, mRadiusX, mRadiusY);
+    }
+
+    @Override
+    public String toString() {
+        return "DrawRoundRect " + mLeft + " " + mTop
+                + " " + mRight + " " + mBottom
+                + " (" + mRadiusX + " " + mRadiusY + ");";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            float sLeft = buffer.readFloat();
+            float srcTop = buffer.readFloat();
+            float srcRight = buffer.readFloat();
+            float srcBottom = buffer.readFloat();
+            float srcRadiusX = buffer.readFloat();
+            float srcRadiusY = buffer.readFloat();
+
+            DrawRoundRect op = new DrawRoundRect(sLeft, srcTop, srcRight,
+                    srcBottom, srcRadiusX, srcRadiusY);
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "DrawOval";
+        }
+
+        @Override
+        public int id() {
+            return Operations.DRAW_ROUND_RECT;
+        }
+
+        public void apply(WireBuffer buffer,
+                          float left,
+                          float top,
+                          float right,
+                          float bottom,
+                          float radiusX,
+                          float radiusY) {
+            buffer.start(Operations.DRAW_ROUND_RECT);
+            buffer.writeFloat(left);
+            buffer.writeFloat(top);
+            buffer.writeFloat(right);
+            buffer.writeFloat(bottom);
+            buffer.writeFloat(radiusX);
+            buffer.writeFloat(radiusY);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.drawRoundRect(mLeft,
+                mTop,
+                mRight,
+                mBottom,
+                mRadiusX,
+                mRadiusY
+        );
+    }
+
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawTextOnPath.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawTextOnPath.java
new file mode 100644
index 0000000..1856e30
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawTextOnPath.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2024 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.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class DrawTextOnPath extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+    int mPathId;
+    public int mTextId;
+    float mVOffset;
+    float mHOffset;
+
+    public DrawTextOnPath(int textId, int pathId, float hOffset, float vOffset) {
+        mPathId = pathId;
+        mTextId = textId;
+        mHOffset = vOffset;
+        mVOffset = hOffset;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mTextId, mPathId, mHOffset, mVOffset);
+    }
+
+    @Override
+    public String toString() {
+        return "DrawTextOnPath " + " " + mPathId + ";";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            int textId = buffer.readInt();
+            int pathId = buffer.readInt();
+            float hOffset = buffer.readFloat();
+            float vOffset = buffer.readFloat();
+            DrawTextOnPath op = new DrawTextOnPath(textId, pathId, hOffset, vOffset);
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "DrawTextOnPath";
+        }
+
+        @Override
+        public int id() {
+            return Operations.DRAW_TEXT_ON_PATH;
+        }
+
+        public void apply(WireBuffer buffer, int textId, int pathId, float hOffset, float vOffset) {
+            buffer.start(Operations.DRAW_TEXT_ON_PATH);
+            buffer.writeInt(textId);
+            buffer.writeInt(pathId);
+            buffer.writeFloat(hOffset);
+            buffer.writeFloat(vOffset);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.drawTextOnPath(mTextId, mPathId, mHOffset, mVOffset);
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawTextRun.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawTextRun.java
new file mode 100644
index 0000000..a099252
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawTextRun.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2024 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.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class DrawTextRun extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+    int mTextID;
+    int mStart = 0;
+    int mEnd = 0;
+    int mContextStart = 0;
+    int mContextEnd = 0;
+    float mX = 0f;
+    float mY = 0f;
+    boolean mRtl = false;
+
+    public DrawTextRun(int textID,
+                       int start,
+                       int end,
+                       int contextStart,
+                       int contextEnd,
+                       float x,
+                       float y,
+                       boolean rtl) {
+        mTextID = textID;
+        mStart = start;
+        mEnd = end;
+        mContextStart = contextStart;
+        mContextEnd = contextEnd;
+        mX = x;
+        mY = y;
+        mRtl = rtl;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mTextID, mStart, mEnd, mContextStart, mContextEnd, mX, mY, mRtl);
+
+    }
+
+    @Override
+    public String toString() {
+        return "";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            int text = buffer.readInt();
+            int start = buffer.readInt();
+            int end = buffer.readInt();
+            int contextStart = buffer.readInt();
+            int contextEnd = buffer.readInt();
+            float x = buffer.readFloat();
+            float y = buffer.readFloat();
+            boolean rtl = buffer.readBoolean();
+            DrawTextRun op = new DrawTextRun(text, start, end, contextStart, contextEnd, x, y, rtl);
+
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "";
+        }
+
+        @Override
+        public int id() {
+            return 0;
+        }
+
+        public void apply(WireBuffer buffer,
+                          int textID,
+                          int start,
+                          int end,
+                          int contextStart,
+                          int contextEnd,
+                          float x,
+                          float y,
+                          boolean rtl) {
+            buffer.start(Operations.DRAW_TEXT_RUN);
+            buffer.writeInt(textID);
+            buffer.writeInt(start);
+            buffer.writeInt(end);
+            buffer.writeInt(contextStart);
+            buffer.writeInt(contextEnd);
+            buffer.writeFloat(x);
+            buffer.writeFloat(y);
+            buffer.writeBoolean(rtl);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.drawTextRun(mTextID, mStart, mEnd, mContextStart, mContextEnd, mX, mY, mRtl);
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawTweenPath.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawTweenPath.java
new file mode 100644
index 0000000..ef0a4ad
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawTweenPath.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2024 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.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class DrawTweenPath extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+    float mTween;
+    float mStart;
+    float mStop;
+    int mPath1Id;
+    int mPath2Id;
+
+    public DrawTweenPath(
+            int path1Id,
+            int path2Id,
+            float tween,
+            float start,
+            float stop) {
+        mTween = tween;
+        mStart = start;
+        mStop = stop;
+        mPath1Id = path1Id;
+        mPath2Id = path2Id;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mPath1Id,
+                mPath2Id,
+                mTween,
+                mStart,
+                mStop);
+    }
+
+    @Override
+    public String toString() {
+        return "DrawTweenPath " + mPath1Id + " " + mPath2Id
+                + " " + mTween + " " + mStart + " "
+                + "- " + mStop + ";";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            int path1Id = buffer.readInt();
+            int path2Id = buffer.readInt();
+            float tween = buffer.readFloat();
+            float start = buffer.readFloat();
+            float stop = buffer.readFloat();
+            DrawTweenPath op = new DrawTweenPath(path1Id, path2Id,
+                    tween, start, stop);
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "DrawTweenPath";
+        }
+
+        @Override
+        public int id() {
+            return Operations.DRAW_TWEEN_PATH;
+        }
+
+        public void apply(WireBuffer buffer,
+                          int path1Id,
+                          int path2Id,
+                          float tween,
+                          float start,
+                          float stop) {
+            buffer.start(Operations.DRAW_TWEEN_PATH);
+            buffer.writeInt(path1Id);
+            buffer.writeInt(path2Id);
+            buffer.writeFloat(tween);
+            buffer.writeFloat(start);
+            buffer.writeFloat(stop);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.drawTweenPath(mPath1Id,
+                mPath2Id,
+                mTween,
+                mStart,
+                mStop);
+    }
+
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/Header.java b/core/java/com/android/internal/widget/remotecompose/core/operations/Header.java
index eca43c5..aabed15 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/Header.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/Header.java
@@ -26,11 +26,11 @@
 
 /**
  * Describe some basic information for a RemoteCompose document
- *
+ * <p>
  * It encodes the version of the document (following semantic versioning) as well
  * as the dimensions of the document in pixels.
  */
-public class Header  implements RemoteComposeOperation {
+public class Header implements RemoteComposeOperation {
     public static final int MAJOR_VERSION = 0;
     public static final int MINOR_VERSION = 1;
     public static final int PATCH_VERSION = 0;
@@ -89,7 +89,8 @@
     }
 
     public static class Companion implements CompanionOperation {
-        private Companion() {}
+        private Companion() {
+        }
 
         @Override
         public String name() {
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixRestore.java b/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixRestore.java
new file mode 100644
index 0000000..482e0e2
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixRestore.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2024 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.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class MatrixRestore extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+
+    public MatrixRestore() {
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer);
+    }
+
+    @Override
+    public String toString() {
+        return "MatrixRestore;";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+
+            MatrixRestore op = new MatrixRestore();
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "MatrixRestore";
+        }
+
+        @Override
+        public int id() {
+            return Operations.MATRIX_RESTORE;
+        }
+
+        public void apply(WireBuffer buffer) {
+            buffer.start(Operations.MATRIX_RESTORE);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.matrixRestore();
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixRotate.java b/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixRotate.java
new file mode 100644
index 0000000..d6c89e0
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixRotate.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2024 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.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class MatrixRotate extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+    float mRotate, mPivotX, mPivotY;
+
+    public MatrixRotate(float rotate, float pivotX, float pivotY) {
+        mRotate = rotate;
+        mPivotX = pivotX;
+        mPivotY = pivotY;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mRotate, mPivotX, mPivotY);
+    }
+
+    @Override
+    public String toString() {
+        return "DrawArc " + mRotate + ", " + mPivotX + ", " + mPivotY + ";";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            float rotate = buffer.readFloat();
+            float pivotX = buffer.readFloat();
+            float pivotY = buffer.readFloat();
+            MatrixRotate op = new MatrixRotate(rotate, pivotX, pivotY);
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "Matrix";
+        }
+
+        @Override
+        public int id() {
+            return Operations.MATRIX_ROTATE;
+        }
+
+        public void apply(WireBuffer buffer, float rotate, float pivotX, float pivotY) {
+            buffer.start(Operations.MATRIX_ROTATE);
+            buffer.writeFloat(rotate);
+            buffer.writeFloat(pivotX);
+            buffer.writeFloat(pivotY);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.matrixRotate(mRotate, mPivotX, mPivotY);
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixSave.java b/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixSave.java
new file mode 100644
index 0000000..d3d5bfb
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixSave.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2024 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.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class MatrixSave extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+
+    public MatrixSave() {
+
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer);
+    }
+
+    @Override
+    public String toString() {
+        return "MatrixSave;";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+
+            MatrixSave op = new MatrixSave();
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "Matrix";
+        }
+
+        @Override
+        public int id() {
+            return Operations.MATRIX_SAVE;
+        }
+
+        public void apply(WireBuffer buffer) {
+            buffer.start(Operations.MATRIX_SAVE);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.matrixSave();
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixScale.java b/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixScale.java
new file mode 100644
index 0000000..28aa68dd
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixScale.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2024 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.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class MatrixScale extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+    float mScaleX, mScaleY;
+    float mCenterX, mCenterY;
+
+    public MatrixScale(float scaleX, float scaleY, float centerX, float centerY) {
+        mScaleX = scaleX;
+        mScaleY = scaleY;
+        mCenterX = centerX;
+        mCenterY = centerY;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mScaleX, mScaleY, mCenterX, mCenterY);
+    }
+
+    @Override
+    public String toString() {
+        return "MatrixScale " + mScaleY + ", " + mScaleY + ";";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            float scaleX = buffer.readFloat();
+            float scaleY = buffer.readFloat();
+            float centerX = buffer.readFloat();
+            float centerY = buffer.readFloat();
+            MatrixScale op = new MatrixScale(scaleX, scaleY, centerX, centerY);
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "Matrix";
+        }
+
+        @Override
+        public int id() {
+            return Operations.MATRIX_SCALE;
+        }
+
+        public void apply(WireBuffer buffer, float scaleX, float scaleY,
+                float centerX, float centerY) {
+            buffer.start(Operations.MATRIX_SCALE);
+            buffer.writeFloat(scaleX);
+            buffer.writeFloat(scaleY);
+            buffer.writeFloat(centerX);
+            buffer.writeFloat(centerY);
+
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.mtrixScale(mScaleX, mScaleY, mCenterX, mCenterY);
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixSkew.java b/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixSkew.java
new file mode 100644
index 0000000..a388899
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixSkew.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2024 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.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class MatrixSkew extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+    float mSkewX, mSkewY;
+
+    public MatrixSkew(float skewX, float skewY) {
+        mSkewX = skewX;
+        mSkewY = skewY;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mSkewX, mSkewY);
+    }
+
+    @Override
+    public String toString() {
+        return "DrawArc " + mSkewY + ", " + mSkewY + ";";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            float skewX = buffer.readFloat();
+            float skewY = buffer.readFloat();
+            MatrixSkew op = new MatrixSkew(skewX, skewY);
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "Matrix";
+        }
+
+        @Override
+        public int id() {
+            return Operations.MATRIX_SKEW;
+        }
+
+        public void apply(WireBuffer buffer, float skewX, float skewY) {
+            buffer.start(Operations.MATRIX_SKEW);
+            buffer.writeFloat(skewX);
+            buffer.writeFloat(skewY);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.matrixSkew(mSkewX, mSkewY);
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixTranslate.java b/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixTranslate.java
new file mode 100644
index 0000000..3298752
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixTranslate.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2024 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.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class MatrixTranslate extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+    float mTranslateX, mTranslateY;
+
+    public MatrixTranslate(float translateX, float translateY) {
+        mTranslateX = translateX;
+        mTranslateY = translateY;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mTranslateX, mTranslateY);
+    }
+
+    @Override
+    public String toString() {
+        return "DrawArc " + mTranslateY + ", " + mTranslateY + ";";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            float translateX = buffer.readFloat();
+            float translateY = buffer.readFloat();
+            MatrixTranslate op = new MatrixTranslate(translateX, translateY);
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "Matrix";
+        }
+
+        @Override
+        public int id() {
+            return Operations.MATRIX_TRANSLATE;
+        }
+
+        public void apply(WireBuffer buffer, float translateX, float translateY) {
+            buffer.start(Operations.MATRIX_TRANSLATE);
+            buffer.writeFloat(translateX);
+            buffer.writeFloat(translateY);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.matrixTranslate(mTranslateX, mTranslateY);
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/PaintData.java b/core/java/com/android/internal/widget/remotecompose/core/operations/PaintData.java
new file mode 100644
index 0000000..e5683ec
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/PaintData.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2024 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.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.operations.paint.PaintBundle;
+
+import java.util.List;
+
+public class PaintData extends PaintOperation {
+    public PaintBundle mPaintData = new PaintBundle();
+    public static final Companion COMPANION = new Companion();
+    public static final int MAX_STRING_SIZE = 4000;
+
+    public PaintData() {
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mPaintData);
+    }
+
+    @Override
+    public String toString() {
+        return "PaintData " + "\"" + mPaintData + "\"";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public String name() {
+            return "TextData";
+        }
+
+        @Override
+        public int id() {
+            return Operations.PAINT_VALUES;
+        }
+
+        public void apply(WireBuffer buffer, PaintBundle paintBundle) {
+            buffer.start(Operations.PAINT_VALUES);
+            paintBundle.writeBundle(buffer);
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            PaintData data = new PaintData();
+            data.mPaintData.readBundle(buffer);
+            operations.add(data);
+        }
+    }
+
+    @Override
+    public String deepToString(String indent) {
+        return indent + toString();
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.applyPaint(mPaintData);
+    }
+
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/PathData.java b/core/java/com/android/internal/widget/remotecompose/core/operations/PathData.java
new file mode 100644
index 0000000..2646b27
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/PathData.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2024 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.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class PathData implements Operation {
+    public static final Companion COMPANION = new Companion();
+    int mInstanceId;
+    float[] mRef;
+    float[] mFloatPath;
+    float[] mRetFloats;
+
+    PathData(int instanceId, float[] floatPath) {
+        mInstanceId = instanceId;
+        mFloatPath = floatPath;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mInstanceId, mFloatPath);
+    }
+
+    @Override
+    public String deepToString(String indent) {
+        return pathString(mFloatPath);
+    }
+
+    public float[] getFloatPath(PaintContext context) {
+        float[] ret = mRetFloats; // Assume retFloats is declared elsewhere
+        if (ret == null) {
+            return mFloatPath; // Assume floatPath is declared elsewhere
+        }
+        float[] localRef = mRef; // Assume ref is of type Float[]
+        if (localRef == null) {
+            for (int i = 0; i < mFloatPath.length; i++) {
+                ret[i] = mFloatPath[i];
+            }
+        } else {
+            for (int i = 0; i < mFloatPath.length; i++) {
+                float lr = localRef[i];
+                if (Float.isNaN(lr)) {
+                    ret[i] = Utils.getActualValue(lr);
+                } else {
+                    ret[i] = mFloatPath[i];
+                }
+            }
+        }
+        return ret;
+    }
+
+    public static final int MOVE = 10;
+    public static final int LINE = 11;
+    public static final int QUADRATIC = 12;
+    public static final int CONIC = 13;
+    public static final int CUBIC = 14;
+    public static final int CLOSE = 15;
+    public static final int DONE = 16;
+    public static final float MOVE_NAN = Utils.asNan(MOVE);
+    public static final float LINE_NAN = Utils.asNan(LINE);
+    public static final float QUADRATIC_NAN = Utils.asNan(QUADRATIC);
+    public static final float CONIC_NAN = Utils.asNan(CONIC);
+    public static final float CUBIC_NAN = Utils.asNan(CUBIC);
+    public static final float CLOSE_NAN = Utils.asNan(CLOSE);
+    public static final float DONE_NAN = Utils.asNan(DONE);
+
+    public static class Companion implements CompanionOperation {
+
+        private Companion() {
+        }
+
+        @Override
+        public String name() {
+            return "BitmapData";
+        }
+
+        @Override
+        public int id() {
+            return Operations.DATA_PATH;
+        }
+
+        public void apply(WireBuffer buffer, int id, float[] data) {
+            buffer.start(Operations.DATA_PATH);
+            buffer.writeInt(id);
+            buffer.writeInt(data.length);
+            for (int i = 0; i < data.length; i++) {
+                buffer.writeFloat(data[i]);
+            }
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            int imageId = buffer.readInt();
+            int len = buffer.readInt();
+            float[] data = new float[len];
+            for (int i = 0; i < data.length; i++) {
+                data[i] = buffer.readFloat();
+            }
+            operations.add(new PathData(imageId, data));
+        }
+    }
+
+    public static String pathString(float[] path) {
+        if (path == null) {
+            return "null";
+        }
+        StringBuilder str = new StringBuilder();
+        for (int i = 0; i < path.length; i++) {
+            if (i != 0) {
+                str.append(" ");
+            }
+            if (Float.isNaN(path[i])) {
+                int id = Utils.idFromNan(path[i]); // Assume idFromNan is defined elsewhere
+                if (id <= DONE) { // Assume DONE is a constant
+                    switch (id) {
+                        case MOVE:
+                            str.append("M");
+                            break;
+                        case LINE:
+                            str.append("L");
+                            break;
+                        case QUADRATIC:
+                            str.append("Q");
+                            break;
+                        case CONIC:
+                            str.append("R");
+                            break;
+                        case CUBIC:
+                            str.append("C");
+                            break;
+                        case CLOSE:
+                            str.append("Z");
+                            break;
+                        case DONE:
+                            str.append(".");
+                            break;
+                        default:
+                            str.append("X");
+                            break;
+                    }
+                } else {
+                    str.append("(" + id + ")");
+                }
+            } else {
+                str.append(path[i]);
+            }
+        }
+        return str.toString();
+    }
+
+    @Override
+    public void apply(RemoteContext context) {
+        context.loadPathData(mInstanceId, mFloatPath);
+    }
+
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/RootContentBehavior.java b/core/java/com/android/internal/widget/remotecompose/core/operations/RootContentBehavior.java
index ad4caea..6d924eb 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/RootContentBehavior.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/RootContentBehavior.java
@@ -28,7 +28,7 @@
 
 /**
  * Describe some basic information for a RemoteCompose document
- *
+ * <p>
  * It encodes the version of the document (following semantic versioning) as well
  * as the dimensions of the document in pixels.
  */
@@ -100,21 +100,21 @@
     /**
      * Sets the way the player handles the content
      *
-     * @param scroll set the horizontal behavior (NONE|SCROLL_HORIZONTAL|SCROLL_VERTICAL)
+     * @param scroll    set the horizontal behavior (NONE|SCROLL_HORIZONTAL|SCROLL_VERTICAL)
      * @param alignment set the alignment of the content (TOP|CENTER|BOTTOM|START|END)
-     * @param sizing set the type of sizing for the content (NONE|SIZING_LAYOUT|SIZING_SCALE)
-     * @param mode set the mode of sizing, either LAYOUT modes or SCALE modes
-     *             the LAYOUT modes are:
-     *             - LAYOUT_MATCH_PARENT
-     *             - LAYOUT_WRAP_CONTENT
-     *             or adding an horizontal mode and a vertical mode:
-     *             - LAYOUT_HORIZONTAL_MATCH_PARENT
-     *             - LAYOUT_HORIZONTAL_WRAP_CONTENT
-     *             - LAYOUT_HORIZONTAL_FIXED
-     *             - LAYOUT_VERTICAL_MATCH_PARENT
-     *             - LAYOUT_VERTICAL_WRAP_CONTENT
-     *             - LAYOUT_VERTICAL_FIXED
-     *             The LAYOUT_*_FIXED modes will use the intrinsic document size
+     * @param sizing    set the type of sizing for the content (NONE|SIZING_LAYOUT|SIZING_SCALE)
+     * @param mode      set the mode of sizing, either LAYOUT modes or SCALE modes
+     *                  the LAYOUT modes are:
+     *                  - LAYOUT_MATCH_PARENT
+     *                  - LAYOUT_WRAP_CONTENT
+     *                  or adding an horizontal mode and a vertical mode:
+     *                  - LAYOUT_HORIZONTAL_MATCH_PARENT
+     *                  - LAYOUT_HORIZONTAL_WRAP_CONTENT
+     *                  - LAYOUT_HORIZONTAL_FIXED
+     *                  - LAYOUT_VERTICAL_MATCH_PARENT
+     *                  - LAYOUT_VERTICAL_WRAP_CONTENT
+     *                  - LAYOUT_VERTICAL_FIXED
+     *                  The LAYOUT_*_FIXED modes will use the intrinsic document size
      */
     public RootContentBehavior(int scroll, int alignment, int sizing, int mode) {
         switch (scroll) {
@@ -149,10 +149,12 @@
         switch (sizing) {
             case SIZING_LAYOUT: {
                 Log.e(TAG, "sizing_layout is not yet supported");
-            } break;
+            }
+            break;
             case SIZING_SCALE: {
                 mSizing = sizing;
-            } break;
+            }
+            break;
             default: {
                 Log.e(TAG, "incorrect sizing value " + sizing);
             }
@@ -200,7 +202,8 @@
     }
 
     public static class Companion implements CompanionOperation {
-        private Companion() {}
+        private Companion() {
+        }
 
         @Override
         public String name() {
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/Utils.java b/core/java/com/android/internal/widget/remotecompose/core/operations/Utils.java
new file mode 100644
index 0000000..00e2f20
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/Utils.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 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.internal.widget.remotecompose.core.operations;
+
+public class Utils {
+    public static float asNan(int v) {
+        return Float.intBitsToFloat(v | -0x800000);
+    }
+
+    public static int idFromNan(float value) {
+        int b =  Float.floatToRawIntBits(value);
+        return b & 0xFFFFF;
+    }
+
+    public static float getActualValue(float lr) {
+        return 0;
+    }
+
+    String getFloatString(float value) {
+        if (Float.isNaN(value)) {
+            int id = idFromNan(value);
+            if (id > 0) {
+                return "NaN(" + id + ")";
+            }
+        }
+        return "" + value;
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintBundle.java b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintBundle.java
new file mode 100644
index 0000000..8abb0bf
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintBundle.java
@@ -0,0 +1,829 @@
+/*
+ * Copyright (C) 2024 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.internal.widget.remotecompose.core.operations.paint;
+
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.Arrays;
+
+public class PaintBundle {
+    int[] mArray = new int[200];
+    int mPos = 0;
+
+    public void applyPaintChange(PaintChanges p) {
+        int i = 0;
+        int mask = 0;
+        while (i < mPos) {
+            int cmd = mArray[i++];
+            mask = mask | (1 << (cmd - 1));
+            switch (cmd & 0xFFFF) {
+                case TEXT_SIZE: {
+                    p.setTextSize(Float.intBitsToFloat(mArray[i++]));
+                    break;
+                }
+                case TYPEFACE:
+                    int style = (cmd >> 16);
+                    int weight = style & 0x3ff;
+                    boolean italic = (style >> 10) > 0;
+                    int font_type = mArray[i++];
+
+                    p.setTypeFace(font_type, weight, italic);
+                    break;
+                case COLOR: {
+                    p.setColor(mArray[i++]);
+                    break;
+                }
+                case STROKE_WIDTH: {
+                    p.setStrokeWidth(Float.intBitsToFloat(mArray[i++]));
+                    break;
+                }
+                case STROKE_MITER: {
+                    p.setStrokeMiter(Float.intBitsToFloat(mArray[i++]));
+                    break;
+                }
+                case STROKE_CAP: {
+                    p.setStrokeCap(cmd >> 16);
+                    break;
+                }
+                case STYLE: {
+                    p.setStyle(cmd >> 16);
+                    break;
+                }
+                case SHADER: {
+                    break;
+                }
+                case STROKE_JOIN: {
+                    p.setStrokeJoin(cmd >> 16);
+                    break;
+                }
+                case IMAGE_FILTER_QUALITY: {
+                    p.setImageFilterQuality(cmd >> 16);
+                    break;
+                }
+                case BLEND_MODE: {
+                    p.setBlendMode(cmd >> 16);
+                    break;
+                }
+                case FILTER_BITMAP: {
+                    p.setFilterBitmap(!((cmd >> 16) == 0));
+                    break;
+                }
+
+                case GRADIENT: {
+                    i = callSetGradient(cmd, mArray, i, p);
+                    break;
+                }
+                case COLOR_FILTER: {
+                    p.setColorFilter(mArray[i++], cmd >> 16);
+                    break;
+                }
+                case ALPHA: {
+                    p.setAlpha(Float.intBitsToFloat(mArray[i++]));
+                    break;
+                }
+            }
+        }
+
+        mask = (~mask) & PaintChanges.VALID_BITS;
+
+        p.clear(mask);
+    }
+
+    private String toName(int id) {
+        switch (id) {
+            case TEXT_SIZE:
+                return "TEXT_SIZE";
+
+            case COLOR:
+                return "COLOR";
+            case STROKE_WIDTH:
+                return "STROKE_WIDTH";
+            case STROKE_MITER:
+                return "STROKE_MITER";
+            case TYPEFACE:
+                return "TYPEFACE";
+            case STROKE_CAP:
+                return "CAP";
+            case STYLE:
+                return "STYLE";
+            case SHADER:
+                return "SHADER";
+            case IMAGE_FILTER_QUALITY:
+                return "IMAGE_FILTER_QUALITY";
+            case BLEND_MODE:
+                return "BLEND_MODE";
+            case FILTER_BITMAP:
+                return "FILTER_BITMAP";
+            case GRADIENT:
+                return "GRADIENT_LINEAR";
+            case ALPHA:
+                return "ALPHA";
+            case COLOR_FILTER:
+                return "COLOR_FILTER";
+
+        }
+        return "????" + id + "????";
+    }
+
+    private static String colorInt(int color) {
+        String str = "000000000000" + Integer.toHexString(color);
+        return "0x" + str.substring(str.length() - 8);
+    }
+
+    private static String colorInt(int[] color) {
+        String str = "[";
+        for (int i = 0; i < color.length; i++) {
+            if (i > 0) {
+                str += ", ";
+            }
+            str += colorInt(color[i]);
+        }
+        return str + "]";
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder ret = new StringBuilder("\n");
+        int i = 0;
+        while (i < mPos) {
+            int cmd = mArray[i++];
+            int type = cmd & 0xFFFF;
+            switch (type) {
+
+                case TEXT_SIZE: {
+                    ret.append("    TextSize(" + Float.intBitsToFloat(mArray[i++]));
+                }
+
+                break;
+                case TYPEFACE: {
+                    int style = (cmd >> 16);
+                    int weight = style & 0x3ff;
+                    boolean italic = (style >> 10) > 0;
+                    int font_type = mArray[i++];
+                    ret.append("    TypeFace(" + (font_type + ", "
+                            + weight + ", " + italic));
+                }
+                break;
+                case COLOR: {
+                    ret.append("    Color(" + colorInt(mArray[i++]));
+                }
+                break;
+                case STROKE_WIDTH: {
+                    ret.append("    StrokeWidth("
+                            + (Float.intBitsToFloat(mArray[i++])));
+                }
+                break;
+                case STROKE_MITER: {
+                    ret.append("    StrokeMiter("
+                            + (Float.intBitsToFloat(mArray[i++])));
+                }
+                break;
+                case STROKE_CAP: {
+                    ret.append("    StrokeCap("
+                            + (cmd >> 16));
+                }
+                break;
+                case STYLE: {
+                    ret.append("    Style(" + (cmd >> 16));
+                }
+                break;
+                case COLOR_FILTER: {
+                    ret.append("    ColorFilter(color="
+                            + colorInt(mArray[i++])
+                            + ", mode=" + blendModeString(cmd >> 16));
+                }
+                break;
+                case SHADER: {
+                }
+                break;
+                case ALPHA: {
+                    ret.append("    Alpha("
+                            + (Float.intBitsToFloat(mArray[i++])));
+                }
+                break;
+                case IMAGE_FILTER_QUALITY: {
+                    ret.append("    ImageFilterQuality(" + (cmd >> 16));
+                }
+                break;
+                case BLEND_MODE: {
+                    ret.append("    BlendMode(" + blendModeString(cmd >> 16));
+                }
+                break;
+                case FILTER_BITMAP: {
+                    ret.append("    FilterBitmap("
+                            + (!((cmd >> 16) == 0)));
+                }
+                break;
+                case STROKE_JOIN: {
+                    ret.append("    StrokeJoin(" + (cmd >> 16));
+                }
+                break;
+                case ANTI_ALIAS: {
+                    ret.append("    AntiAlias(" + (cmd >> 16));
+                }
+                break;
+                case GRADIENT: {
+                    i = callPrintGradient(cmd, mArray, i, ret);
+                }
+            }
+            ret.append("),\n");
+        }
+        return ret.toString();
+    }
+
+
+    int callPrintGradient(int cmd, int[] array, int i, StringBuilder p) {
+        int ret = i;
+        int type = (cmd >> 16);
+        switch (type) {
+
+            case 0: {
+                p.append("    LinearGradient(\n");
+                int len = array[ret++];
+                int[] colors = null;
+                if (len > 0) {
+                    colors = new int[len];
+                    for (int j = 0; j < colors.length; j++) {
+                        colors[j] = array[ret++];
+
+                    }
+                }
+                len = array[ret++];
+                float[] stops = null;
+                if (len > 0) {
+                    stops = new float[len];
+                    for (int j = 0; j < stops.length; j++) {
+                        stops[j] = Float.intBitsToFloat(array[ret++]);
+                    }
+                }
+
+                p.append("      colors = " + colorInt(colors) + ",\n");
+                p.append("      stops = " + Arrays.toString(stops) + ",\n");
+                p.append("      start = ");
+                p.append("[" + Float.intBitsToFloat(array[ret++]));
+                p.append(", " + Float.intBitsToFloat(array[ret++]) + "],\n");
+                p.append("      end = ");
+                p.append("[" + Float.intBitsToFloat(array[ret++]));
+                p.append(", " + Float.intBitsToFloat(array[ret++]) + "],\n");
+                int tileMode = array[ret++];
+                p.append("      tileMode = " + tileMode + "\n    ");
+            }
+
+            break;
+            case 1: {
+                p.append("    RadialGradient(\n");
+                int len = array[ret++];
+                int[] colors = null;
+                if (len > 0) {
+                    colors = new int[len];
+                    for (int j = 0; j < colors.length; j++) {
+                        colors[j] = array[ret++];
+
+                    }
+                }
+                len = array[ret++];
+                float[] stops = null;
+                if (len > 0) {
+                    stops = new float[len];
+                    for (int j = 0; j < stops.length; j++) {
+                        stops[j] = Float.intBitsToFloat(array[ret++]);
+                    }
+                }
+
+                p.append("      colors = " + colorInt(colors) + ",\n");
+                p.append("      stops = " + Arrays.toString(stops) + ",\n");
+                p.append("      center = ");
+                p.append("[" + Float.intBitsToFloat(array[ret++]));
+                p.append(", " + Float.intBitsToFloat(array[ret++]) + "],\n");
+                p.append("      radius =");
+                p.append(" " + Float.intBitsToFloat(array[ret++]) + ",\n");
+                int tileMode = array[ret++];
+                p.append("      tileMode = " + tileMode + "\n    ");
+            }
+
+            break;
+            case 2: {
+                p.append("    SweepGradient(\n");
+                int len = array[ret++];
+                int[] colors = null;
+                if (len > 0) {
+                    colors = new int[len];
+                    for (int j = 0; j < colors.length; j++) {
+                        colors[j] = array[ret++];
+
+                    }
+                }
+                len = array[ret++];
+                float[] stops = null;
+                if (len > 0) {
+                    stops = new float[len];
+                    for (int j = 0; j < stops.length; j++) {
+                        stops[j] = Float.intBitsToFloat(array[ret++]);
+                    }
+                }
+
+                p.append("      colors = " + colorInt(colors) + ",\n");
+                p.append("      stops = " + Arrays.toString(stops) + ",\n");
+                p.append("      center = ");
+                p.append("[" + Float.intBitsToFloat(array[ret++]));
+                p.append(", " + Float.intBitsToFloat(array[ret++]) + "],\n    ");
+
+            }
+            break;
+            default: {
+                p.append("GRADIENT_??????!!!!");
+            }
+        }
+
+        return ret;
+    }
+
+    int callSetGradient(int cmd, int[] array, int i, PaintChanges p) {
+        int ret = i;
+        int gradientType = (cmd >> 16);
+
+        int len = array[ret++];
+        int[] colors = null;
+        if (len > 0) {
+            colors = new int[len];
+            for (int j = 0; j < colors.length; j++) {
+                colors[j] = array[ret++];
+            }
+        }
+        len = array[ret++];
+        float[] stops = null;
+        if (len > 0) {
+            stops = new float[len];
+            for (int j = 0; j < colors.length; j++) {
+                stops[j] = Float.intBitsToFloat(array[ret++]);
+            }
+        }
+
+        if (colors == null) {
+            return ret;
+        }
+
+
+        switch (gradientType) {
+
+            case LINEAR_GRADIENT: {
+                float startX = Float.intBitsToFloat(array[ret++]);
+                float startY = Float.intBitsToFloat(array[ret++]);
+                float endX = Float.intBitsToFloat(array[ret++]);
+                float endY = Float.intBitsToFloat(array[ret++]);
+                int tileMode = array[ret++];
+                p.setLinearGradient(colors, stops, startX,
+                        startY, endX, endY, tileMode);
+            }
+
+            break;
+            case RADIAL_GRADIENT: {
+                float centerX = Float.intBitsToFloat(array[ret++]);
+                float centerY = Float.intBitsToFloat(array[ret++]);
+                float radius = Float.intBitsToFloat(array[ret++]);
+                int tileMode = array[ret++];
+                p.setRadialGradient(colors, stops, centerX, centerY,
+                        radius, tileMode);
+            }
+            break;
+            case SWEEP_GRADIENT: {
+                float centerX = Float.intBitsToFloat(array[ret++]);
+                float centerY = Float.intBitsToFloat(array[ret++]);
+                p.setSweepGradient(colors, stops, centerX, centerY);
+            }
+        }
+
+        return ret;
+    }
+
+    public void writeBundle(WireBuffer buffer) {
+        buffer.writeInt(mPos);
+        for (int index = 0; index < mPos; index++) {
+            buffer.writeInt(mArray[index]);
+        }
+    }
+
+    public void readBundle(WireBuffer buffer) {
+        int len = buffer.readInt();
+        if (len <= 0 || len > 1024) {
+            throw new RuntimeException("buffer corrupt paint len = " + len);
+        }
+        mArray = new int[len];
+        for (int i = 0; i < mArray.length; i++) {
+            mArray[i] = buffer.readInt();
+        }
+        mPos = len;
+    }
+
+    public static final int TEXT_SIZE = 1;  // float
+
+    public static final int COLOR = 4;  // int
+    public static final int STROKE_WIDTH = 5; // float
+    public static final int STROKE_MITER = 6;
+    public static final int STROKE_CAP = 7; // int
+    public static final int STYLE = 8; // int
+    public static final int SHADER = 9; // int
+    public static final int IMAGE_FILTER_QUALITY = 10; // int
+    public static final int GRADIENT = 11;
+    public static final int ALPHA = 12;
+    public static final int COLOR_FILTER = 13;
+    public static final int ANTI_ALIAS = 14;
+    public static final int STROKE_JOIN = 15;
+    public static final int TYPEFACE = 16;
+    public static final int FILTER_BITMAP = 17;
+    public static final int BLEND_MODE = 18;
+
+
+    public static final int BLEND_MODE_CLEAR = 0;
+    public static final int BLEND_MODE_SRC = 1;
+    public static final int BLEND_MODE_DST = 2;
+    public static final int BLEND_MODE_SRC_OVER = 3;
+    public static final int BLEND_MODE_DST_OVER = 4;
+    public static final int BLEND_MODE_SRC_IN = 5;
+    public static final int BLEND_MODE_DST_IN = 6;
+    public static final int BLEND_MODE_SRC_OUT = 7;
+    public static final int BLEND_MODE_DST_OUT = 8;
+    public static final int BLEND_MODE_SRC_ATOP = 9;
+    public static final int BLEND_MODE_DST_ATOP = 10;
+    public static final int BLEND_MODE_XOR = 11;
+    public static final int BLEND_MODE_PLUS = 12;
+    public static final int BLEND_MODE_MODULATE = 13;
+    public static final int BLEND_MODE_SCREEN = 14;
+    public static final int BLEND_MODE_OVERLAY = 15;
+    public static final int BLEND_MODE_DARKEN = 16;
+    public static final int BLEND_MODE_LIGHTEN = 17;
+    public static final int BLEND_MODE_COLOR_DODGE = 18;
+    public static final int BLEND_MODE_COLOR_BURN = 19;
+    public static final int BLEND_MODE_HARD_LIGHT = 20;
+    public static final int BLEND_MODE_SOFT_LIGHT = 21;
+    public static final int BLEND_MODE_DIFFERENCE = 22;
+    public static final int BLEND_MODE_EXCLUSION = 23;
+    public static final int BLEND_MODE_MULTIPLY = 24;
+    public static final int BLEND_MODE_HUE = 25;
+    public static final int BLEND_MODE_SATURATION = 26;
+    public static final int BLEND_MODE_COLOR = 27;
+    public static final int BLEND_MODE_LUMINOSITY = 28;
+    public static final int BLEND_MODE_NULL = 29;
+    public static final int PORTER_MODE_ADD = 30;
+
+    public static final int FONT_NORMAL = 0;
+    public static final int FONT_BOLD = 1;
+    public static final int FONT_ITALIC = 2;
+    public static final int FONT_BOLD_ITALIC = 3;
+
+    public static final int FONT_TYPE_DEFAULT = 0;
+    public static final int FONT_TYPE_SANS_SERIF = 1;
+    public static final int FONT_TYPE_SERIF = 2;
+    public static final int FONT_TYPE_MONOSPACE = 3;
+
+    public static final int STYLE_FILL = 0;
+    public static final int STYLE_STROKE = 1;
+    public static final int STYLE_FILL_AND_STROKE = 2;
+    public static final int LINEAR_GRADIENT = 0;
+    public static final int RADIAL_GRADIENT = 1;
+    public static final int SWEEP_GRADIENT = 2;
+
+    /**
+     * sets a shader that draws a linear gradient along a line.
+     *
+     * @param startX   The x-coordinate for the start of the gradient line
+     * @param startY   The y-coordinate for the start of the gradient line
+     * @param endX     The x-coordinate for the end of the gradient line
+     * @param endY     The y-coordinate for the end of the gradient line
+     * @param colors   The sRGB colors to be distributed along the gradient line
+     * @param stops    May be null. The relative positions [0..1] of
+     *                 each corresponding color in the colors array. If this is null,
+     *                 the colors are distributed evenly along the gradient line.
+     * @param tileMode The Shader tiling mode
+     */
+    public void setLinearGradient(int[] colors,
+                                  float[] stops,
+                                  float startX,
+                                  float startY,
+                                  float endX,
+                                  float endY,
+                                  int tileMode) {
+        int startPos = mPos;
+        int len;
+        mArray[mPos++] = GRADIENT | (LINEAR_GRADIENT << 16);
+        mArray[mPos++] = len = (colors == null) ? 0 : colors.length;
+        for (int i = 0; i < len; i++) {
+            mArray[mPos++] = colors[i];
+        }
+
+        mArray[mPos++] = len = (stops == null) ? 0 : stops.length;
+        for (int i = 0; i < len; i++) {
+            mArray[mPos++] = Float.floatToRawIntBits(stops[i]);
+        }
+        mArray[mPos++] = Float.floatToRawIntBits(startX);
+        mArray[mPos++] = Float.floatToRawIntBits(startY);
+        mArray[mPos++] = Float.floatToRawIntBits(endX);
+        mArray[mPos++] = Float.floatToRawIntBits(endY);
+        mArray[mPos++] = tileMode;
+    }
+
+    /**
+     * Set a shader that draws a sweep gradient around a center point.
+     *
+     * @param centerX The x-coordinate of the center
+     * @param centerY The y-coordinate of the center
+     * @param colors  The sRGB colors to be distributed around the center.
+     *                There must be at least 2 colors in the array.
+     * @param stops   May be NULL. The relative position of
+     *                each corresponding color in the colors array, beginning
+     *                with 0 and ending with 1.0. If the values are not
+     *                monotonic, the drawing may produce unexpected results.
+     *                If positions is NULL, then the colors are automatically
+     *                spaced evenly.
+     */
+    public void setSweepGradient(int[] colors, float[] stops, float centerX, float centerY) {
+        int startPos = mPos;
+        int len;
+        mArray[mPos++] = GRADIENT | (SWEEP_GRADIENT << 16);
+        mArray[mPos++] = len = (colors == null) ? 0 : colors.length;
+        for (int i = 0; i < len; i++) {
+            mArray[mPos++] = colors[i];
+        }
+
+        mArray[mPos++] = len = (stops == null) ? 0 : stops.length;
+        for (int i = 0; i < len; i++) {
+            mArray[mPos++] = Float.floatToRawIntBits(stops[i]);
+        }
+        mArray[mPos++] = Float.floatToRawIntBits(centerX);
+        mArray[mPos++] = Float.floatToRawIntBits(centerY);
+    }
+
+    /**
+     * Sets a shader that draws a radial gradient given the center and radius.
+     *
+     * @param centerX  The x-coordinate of the center of the radius
+     * @param centerY  The y-coordinate of the center of the radius
+     * @param radius   Must be positive. The radius of the gradient.
+     * @param colors   The sRGB colors distributed between the center and edge
+     * @param stops    May be <code>null</code>.
+     *                 Valid values are between <code>0.0f</code> and
+     *                 <code>1.0f</code>. The relative position of each
+     *                 corresponding color in
+     *                 the colors array. If <code>null</code>, colors are
+     *                 distributed evenly
+     *                 between the center and edge of the circle.
+     * @param tileMode The Shader tiling mode
+     */
+    public void setRadialGradient(int[] colors,
+                                  float[] stops,
+                                  float centerX,
+                                  float centerY,
+                                  float radius,
+                                  int tileMode) {
+        int startPos = mPos;
+        int len;
+        mArray[mPos++] = GRADIENT | (RADIAL_GRADIENT << 16);
+        mArray[mPos++] = len = (colors == null) ? 0 : colors.length;
+        for (int i = 0; i < len; i++) {
+            mArray[mPos++] = colors[i];
+        }
+        mArray[mPos++] = len = (stops == null) ? 0 : stops.length;
+
+        for (int i = 0; i < len; i++) {
+            mArray[mPos++] = Float.floatToRawIntBits(stops[i]);
+        }
+        mArray[mPos++] = Float.floatToRawIntBits(centerX);
+        mArray[mPos++] = Float.floatToRawIntBits(centerY);
+        mArray[mPos++] = Float.floatToRawIntBits(radius);
+        mArray[mPos++] = tileMode;
+
+    }
+
+    /**
+     * Create a color filter that uses the specified color and Porter-Duff mode.
+     *
+     * @param color The ARGB source color used with the Porter-Duff mode
+     * @param mode  The porter-duff mode that is applied
+     */
+    public void setColorFilter(int color, int mode) {
+        mArray[mPos] = COLOR_FILTER | (mode << 16);
+        mPos++;
+        mArray[mPos++] = color;
+    }
+
+    /**
+     * Set the paint's text size. This value must be > 0
+     *
+     * @param size set the paint's text size in pixel units.
+     */
+    public void setTextSize(float size) {
+        int p = mPos;
+        mArray[mPos] = TEXT_SIZE;
+        mPos++;
+        mArray[mPos] = Float.floatToRawIntBits(size);
+        mPos++;
+    }
+
+    /**
+     * @param fontType 0 = default 1 = sans serif 2 = serif 3 = monospace
+     * @param weight    100-1000
+     * @param italic    tur
+     */
+    public void setTextStyle(int fontType, int weight, boolean italic) {
+        int style = (weight & 0x3FF) | (italic ? 2048 : 0);  // pack the weight and italic
+        mArray[mPos++] = TYPEFACE | (style << 16);
+        mArray[mPos++] = fontType;
+    }
+
+    /**
+     * Set the width for stroking.
+     * Pass 0 to stroke in hairline mode.
+     * Hairlines always draws a single pixel independent of the canvas's matrix.
+     *
+     * @param width set the paint's stroke width, used whenever the paint's
+     *              style is Stroke or StrokeAndFill.
+     */
+    public void setStrokeWidth(float width) {
+        mArray[mPos] = STROKE_WIDTH;
+        mPos++;
+        mArray[mPos] = Float.floatToRawIntBits(width);
+        mPos++;
+    }
+
+    public void setColor(int color) {
+        mArray[mPos] = COLOR;
+        mPos++;
+        mArray[mPos] = color;
+        mPos++;
+    }
+
+    /**
+     * Set the paint's Cap.
+     *
+     * @param cap set the paint's line cap style, used whenever the paint's
+     *            style is Stroke or StrokeAndFill.
+     */
+    public void setStrokeCap(int cap) {
+        mArray[mPos] = STROKE_CAP | (cap << 16);
+        mPos++;
+    }
+
+    public void setStyle(int style) {
+        mArray[mPos] = STYLE | (style << 16);
+        mPos++;
+    }
+
+    public void setShader(int shader, String shaderString) {
+        mArray[mPos] = SHADER | (shader << 16);
+        mPos++;
+    }
+
+    public void setAlpha(float alpha) {
+        mArray[mPos] = ALPHA;
+        mPos++;
+        mArray[mPos] = Float.floatToRawIntBits(alpha);
+        mPos++;
+    }
+
+    /**
+     * Set the paint's stroke miter value. This is used to control the behavior
+     * of miter joins when the joins angle is sharp. This value must be >= 0.
+     *
+     * @param miter set the miter limit on the paint, used whenever the paint's
+     *              style is Stroke or StrokeAndFill.
+     */
+    public void setStrokeMiter(float miter) {
+        mArray[mPos] = STROKE_MITER;
+        mPos++;
+        mArray[mPos] = Float.floatToRawIntBits(miter);
+        mPos++;
+    }
+
+    /**
+     * Set the paint's Join.
+     *
+     * @param join set the paint's Join, used whenever the paint's style is
+     *             Stroke or StrokeAndFill.
+     */
+    public void setStrokeJoin(int join) {
+        mArray[mPos] = STROKE_JOIN | (join << 16);
+        mPos++;
+    }
+
+    public void setFilterBitmap(boolean filter) {
+        mArray[mPos] = FILTER_BITMAP | (filter ? (1 << 16) : 0);
+        mPos++;
+    }
+
+    /**
+     * Set or clear the blend mode. A blend mode defines how source pixels
+     * (generated by a drawing command) are composited with the
+     * destination pixels
+     * (content of the render target).
+     *
+     *
+     * @param blendmode The blend mode to be installed in the paint
+     */
+    public void setBlendMode(int blendmode) {
+        mArray[mPos] = BLEND_MODE | (blendmode << 16);
+        mPos++;
+    }
+
+    /**
+     * Helper for setFlags(), setting or clearing the ANTI_ALIAS_FLAG bit
+     * AntiAliasing smooths out the edges of what is being drawn, but is has
+     * no impact on the interior of the shape. See setDither() and
+     * setFilterBitmap() to affect how colors are treated.
+     *
+     * @param aa true to set the antialias bit in the flags, false to clear it
+     */
+    public void setAntiAlias(boolean aa) {
+        mArray[mPos] = ANTI_ALIAS | (((aa) ? 1 : 0) << 16);
+        mPos++;
+    }
+
+    public void clear(long mask) { // unused for now
+    }
+
+    public void reset() {
+        mPos = 0;
+    }
+
+    public static String blendModeString(int mode) {
+        switch (mode) {
+            case PaintBundle.BLEND_MODE_CLEAR:
+                return "CLEAR";
+            case PaintBundle.BLEND_MODE_SRC:
+                return "SRC";
+            case PaintBundle.BLEND_MODE_DST:
+                return "DST";
+            case PaintBundle.BLEND_MODE_SRC_OVER:
+                return "SRC_OVER";
+            case PaintBundle.BLEND_MODE_DST_OVER:
+                return "DST_OVER";
+            case PaintBundle.BLEND_MODE_SRC_IN:
+                return "SRC_IN";
+            case PaintBundle.BLEND_MODE_DST_IN:
+                return "DST_IN";
+            case PaintBundle.BLEND_MODE_SRC_OUT:
+                return "SRC_OUT";
+            case PaintBundle.BLEND_MODE_DST_OUT:
+                return "DST_OUT";
+            case PaintBundle.BLEND_MODE_SRC_ATOP:
+                return "SRC_ATOP";
+            case PaintBundle.BLEND_MODE_DST_ATOP:
+                return "DST_ATOP";
+            case PaintBundle.BLEND_MODE_XOR:
+                return "XOR";
+            case PaintBundle.BLEND_MODE_PLUS:
+                return "PLUS";
+            case PaintBundle.BLEND_MODE_MODULATE:
+                return "MODULATE";
+            case PaintBundle.BLEND_MODE_SCREEN:
+                return "SCREEN";
+            case PaintBundle.BLEND_MODE_OVERLAY:
+                return "OVERLAY";
+            case PaintBundle.BLEND_MODE_DARKEN:
+                return "DARKEN";
+            case PaintBundle.BLEND_MODE_LIGHTEN:
+                return "LIGHTEN";
+            case PaintBundle.BLEND_MODE_COLOR_DODGE:
+                return "COLOR_DODGE";
+            case PaintBundle.BLEND_MODE_COLOR_BURN:
+                return "COLOR_BURN";
+            case PaintBundle.BLEND_MODE_HARD_LIGHT:
+                return "HARD_LIGHT";
+            case PaintBundle.BLEND_MODE_SOFT_LIGHT:
+                return "SOFT_LIGHT";
+            case PaintBundle.BLEND_MODE_DIFFERENCE:
+                return "DIFFERENCE";
+            case PaintBundle.BLEND_MODE_EXCLUSION:
+                return "EXCLUSION";
+            case PaintBundle.BLEND_MODE_MULTIPLY:
+                return "MULTIPLY";
+            case PaintBundle.BLEND_MODE_HUE:
+                return "HUE";
+            case PaintBundle.BLEND_MODE_SATURATION:
+                return "SATURATION";
+            case PaintBundle.BLEND_MODE_COLOR:
+                return "COLOR";
+            case PaintBundle.BLEND_MODE_LUMINOSITY:
+                return "LUMINOSITY";
+            case PaintBundle.BLEND_MODE_NULL:
+                return "null";
+            case PaintBundle.PORTER_MODE_ADD:
+                return "ADD";
+        }
+        return "null";
+    }
+
+}
+
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintChangeAdapter.java b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintChangeAdapter.java
new file mode 100644
index 0000000..994bf6d
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintChangeAdapter.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2024 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.internal.widget.remotecompose.core.operations.paint;
+
+public class PaintChangeAdapter implements PaintChanges {
+
+    @Override
+    public void setTextSize(float size) {
+
+    }
+
+    @Override
+    public void setTypeFace(int fontType, int weight, boolean italic) {
+
+    }
+
+
+    @Override
+    public void setStrokeWidth(float width) {
+
+    }
+
+    @Override
+    public void setColor(int color) {
+
+    }
+
+    @Override
+    public void setStrokeCap(int cap) {
+
+    }
+
+    @Override
+    public void setStyle(int style) {
+
+    }
+
+    @Override
+    public void setShader(int shader, String shaderString) {
+
+    }
+
+    @Override
+    public void setImageFilterQuality(int quality) {
+
+    }
+
+    @Override
+    public void setAlpha(float a) {
+
+    }
+
+    @Override
+    public void setStrokeMiter(float miter) {
+
+    }
+
+    @Override
+    public void setStrokeJoin(int join) {
+
+    }
+
+    @Override
+    public void setFilterBitmap(boolean filter) {
+
+    }
+
+    @Override
+    public void setBlendMode(int blendmode) {
+
+    }
+
+    @Override
+    public void setAntiAlias(boolean aa) {
+
+    }
+
+    @Override
+    public void clear(long mask) {
+
+    }
+
+    @Override
+    public void setLinearGradient(int[] colorsArray,
+                                  float[] stopsArray,
+                                  float startX,
+                                  float startY,
+                                  float endX,
+                                  float endY,
+                                  int tileMode) {
+
+    }
+
+    @Override
+    public void setRadialGradient(int[] colorsArray,
+                                  float[] stopsArray,
+                                  float centerX,
+                                  float centerY,
+                                  float radius,
+                                  int tileMode) {
+
+    }
+
+    @Override
+    public void setSweepGradient(int[] colorsArray,
+                                 float[] stopsArray,
+                                 float centerX,
+                                 float centerY) {
+
+    }
+
+    @Override
+    public void setColorFilter(int color, int mode) {
+
+    }
+
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintChanges.java b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintChanges.java
new file mode 100644
index 0000000..87e58ac
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintChanges.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2024 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.internal.widget.remotecompose.core.operations.paint;
+
+public interface PaintChanges {
+
+
+    int CLEAR_TEXT_STYLE = 1 << (PaintBundle.TYPEFACE - 1);
+    int CLEAR_COLOR = 1 << (PaintBundle.COLOR - 1);
+    int CLEAR_STROKE_WIDTH = 1 << (PaintBundle.STROKE_WIDTH - 1);
+    int CLEAR_STROKE_MITER = 1 << (PaintBundle.STROKE_MITER - 1);
+    int CLEAR_CAP = 1 << (PaintBundle.STROKE_CAP - 1);
+    int CLEAR_STYLE = 1 << (PaintBundle.STYLE - 1);
+    int CLEAR_SHADER = 1 << (PaintBundle.SHADER - 1);
+    int CLEAR_IMAGE_FILTER_QUALITY =
+            1 << (PaintBundle.IMAGE_FILTER_QUALITY - 1);
+    int CLEAR_RADIENT = 1 << (PaintBundle.GRADIENT - 1);
+    int CLEAR_ALPHA = 1 << (PaintBundle.ALPHA - 1);
+    int CLEAR_COLOR_FILTER = 1 << (PaintBundle.COLOR_FILTER - 1);
+    int VALID_BITS = 0x1FFF; // only the first 13 bit are valid now
+
+
+    void setTextSize(float size);
+    void setStrokeWidth(float width);
+    void setColor(int color);
+    void setStrokeCap(int cap);
+    void setStyle(int style);
+    void setShader(int shader, String shaderString);
+    void setImageFilterQuality(int quality);
+    void setAlpha(float a);
+    void setStrokeMiter(float miter);
+    void setStrokeJoin(int join);
+    void setFilterBitmap(boolean filter);
+    void setBlendMode(int mode);
+    void setAntiAlias(boolean aa);
+    void clear(long mask);
+    void setLinearGradient(
+            int[] colorsArray,
+            float[] stopsArray,
+            float startX,
+            float startY,
+            float endX,
+            float endY,
+            int tileMode
+    );
+
+    void setRadialGradient(
+            int[] colorsArray,
+            float[] stopsArray,
+            float centerX,
+            float centerY,
+            float radius,
+            int tileMode
+    );
+
+    void setSweepGradient(
+            int[] colorsArray,
+            float[] stopsArray,
+            float centerX,
+            float centerY
+    );
+
+
+    void setColorFilter(int color, int mode);
+
+    void setTypeFace(int fontType, int weight, boolean italic);
+}
+
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/paint/TextPaint.java b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/TextPaint.java
new file mode 100644
index 0000000..1c0bec7
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/TextPaint.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 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.internal.widget.remotecompose.core.operations.paint;
+
+public interface TextPaint {
+    void setARGB(int a, int r, int g, int b);
+
+    void setDither(boolean dither);
+
+    void setElegantTextHeight(boolean elegant);
+
+    void setEndHyphenEdit(int endHyphen);
+
+    void setFakeBoldText(boolean fakeBoldText);
+
+    void setFlags(int flags);
+
+    void setFontFeatureSettings(String settings);
+
+    void setHinting(int mode);
+
+    void setLetterSpacing(float letterSpacing);
+
+    void setLinearText(boolean linearText);
+
+    void setShadowLayer(float radius, float dx, float dy, int shadowColor);
+
+    void setStartHyphenEdit(int startHyphen);
+
+    void setStrikeThruText(boolean strikeThruText);
+
+    void setStrokeCap(int cap);
+
+    void setSubpixelText(boolean subpixelText);
+
+    void setTextAlign(int align);
+
+    void setTextLocale(int locale);
+
+    void setTextLocales(int localesArray);
+
+    void setTextScaleX(float scaleX);
+
+    void setTextSize(float textSize);
+
+    void setTextSkewX(float skewX);
+
+    void setUnderlineText(boolean underlineText);
+
+    void setWordSpacing(float wordSpacing);
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java
index 3799cf6..d0d6e69 100644
--- a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java
+++ b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java
@@ -16,12 +16,25 @@
 package com.android.internal.widget.remotecompose.player.platform;
 
 import android.graphics.Bitmap;
+import android.graphics.BlendMode;
 import android.graphics.Canvas;
+import android.graphics.LinearGradient;
 import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.RadialGradient;
 import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Shader;
+import android.graphics.SweepGradient;
+import android.graphics.Typeface;
 
 import com.android.internal.widget.remotecompose.core.PaintContext;
 import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.operations.ClipPath;
+import com.android.internal.widget.remotecompose.core.operations.paint.PaintBundle;
+import com.android.internal.widget.remotecompose.core.operations.paint.PaintChanges;
 
 /**
  * An implementation of PaintContext for the Android Canvas.
@@ -71,7 +84,8 @@
                            int cdId) {
         AndroidRemoteContext androidContext = (AndroidRemoteContext) mContext;
         if (androidContext.mRemoteComposeState.containsId(imageId)) {
-            Bitmap bitmap = (Bitmap) androidContext.mRemoteComposeState.getFromId(imageId);
+            Bitmap bitmap = (Bitmap) androidContext.mRemoteComposeState
+                    .getFromId(imageId);
             mCanvas.drawBitmap(
                     bitmap,
                     new Rect(srcLeft, srcTop, srcRight, srcBottom),
@@ -89,5 +103,501 @@
     public void translate(float translateX, float translateY) {
         mCanvas.translate(translateX, translateY);
     }
+
+    @Override
+    public void drawArc(float left,
+                        float top,
+                        float right,
+                        float bottom,
+                        float startAngle,
+                        float sweepAngle) {
+        mCanvas.drawArc(left, top, right, bottom, startAngle,
+                sweepAngle, true, mPaint);
+    }
+
+    @Override
+    public void drawBitmap(int id,
+                           float left,
+                           float top,
+                           float right,
+                           float bottom) {
+        AndroidRemoteContext androidContext = (AndroidRemoteContext) mContext;
+        if (androidContext.mRemoteComposeState.containsId(id)) {
+            Bitmap bitmap =
+                    (Bitmap) androidContext.mRemoteComposeState.getFromId(id);
+            Rect src = new Rect(0, 0,
+                    bitmap.getWidth(), bitmap.getHeight());
+            RectF dst = new RectF(left, top, right, bottom);
+            mCanvas.drawBitmap(bitmap, src, dst, mPaint);
+        }
+    }
+
+    @Override
+    public void drawCircle(float centerX, float centerY, float radius) {
+        mCanvas.drawCircle(centerX, centerY, radius, mPaint);
+    }
+
+    @Override
+    public void drawLine(float x1, float y1, float x2, float y2) {
+        mCanvas.drawLine(x1, y1, x2, y2, mPaint);
+    }
+
+    @Override
+    public void drawOval(float left, float top, float right, float bottom) {
+        mCanvas.drawOval(left, top, right, bottom, mPaint);
+    }
+
+    @Override
+    public void drawPath(int id, float start, float end) {
+        mCanvas.drawPath(getPath(id, start, end), mPaint);
+    }
+
+    @Override
+    public void drawRect(float left, float top, float right, float bottom) {
+        mCanvas.drawRect(left, top, right, bottom, mPaint);
+    }
+
+    @Override
+    public void drawRoundRect(float left,
+                              float top,
+                              float right,
+                              float bottom,
+                              float radiusX,
+                              float radiusY) {
+        mCanvas.drawRoundRect(left, top, right, bottom,
+                radiusX, radiusY, mPaint);
+    }
+
+    @Override
+    public void drawTextOnPath(int textId,
+                               int pathId,
+                               float hOffset,
+                               float vOffset) {
+        mCanvas.drawTextOnPath(getText(textId), getPath(pathId, 0, 1), hOffset, vOffset, mPaint);
+    }
+
+    @Override
+    public void drawTextRun(int textID,
+                            int start,
+                            int end,
+                            int contextStart,
+                            int contextEnd,
+                            float x,
+                            float y,
+                            boolean rtl) {
+        String textToPaint = getText(textID).substring(start, end);
+        mCanvas.drawText(textToPaint, x, y, mPaint);
+    }
+
+    @Override
+    public void drawTweenPath(int path1Id,
+                              int path2Id,
+                              float tween,
+                              float start,
+                              float end) {
+        mCanvas.drawPath(getPath(path1Id, path2Id, tween, start, end), mPaint);
+    }
+
+    private static PorterDuff.Mode origamiToPorterDuffMode(int mode) {
+        switch (mode) {
+            case PaintBundle.BLEND_MODE_CLEAR:
+                return PorterDuff.Mode.CLEAR;
+            case PaintBundle.BLEND_MODE_SRC:
+                return PorterDuff.Mode.SRC;
+            case PaintBundle.BLEND_MODE_DST:
+                return PorterDuff.Mode.DST;
+            case PaintBundle.BLEND_MODE_SRC_OVER:
+                return PorterDuff.Mode.SRC_OVER;
+            case PaintBundle.BLEND_MODE_DST_OVER:
+                return PorterDuff.Mode.DST_OVER;
+            case PaintBundle.BLEND_MODE_SRC_IN:
+                return PorterDuff.Mode.SRC_IN;
+            case PaintBundle.BLEND_MODE_DST_IN:
+                return PorterDuff.Mode.DST_IN;
+            case PaintBundle.BLEND_MODE_SRC_OUT:
+                return PorterDuff.Mode.SRC_OUT;
+            case PaintBundle.BLEND_MODE_DST_OUT:
+                return PorterDuff.Mode.DST_OUT;
+            case PaintBundle.BLEND_MODE_SRC_ATOP:
+                return PorterDuff.Mode.SRC_ATOP;
+            case PaintBundle.BLEND_MODE_DST_ATOP:
+                return PorterDuff.Mode.DST_ATOP;
+            case PaintBundle.BLEND_MODE_XOR:
+                return PorterDuff.Mode.XOR;
+            case PaintBundle.BLEND_MODE_SCREEN:
+                return PorterDuff.Mode.SCREEN;
+            case PaintBundle.BLEND_MODE_OVERLAY:
+                return PorterDuff.Mode.OVERLAY;
+            case PaintBundle.BLEND_MODE_DARKEN:
+                return PorterDuff.Mode.DARKEN;
+            case PaintBundle.BLEND_MODE_LIGHTEN:
+                return PorterDuff.Mode.LIGHTEN;
+            case PaintBundle.BLEND_MODE_MULTIPLY:
+                return PorterDuff.Mode.MULTIPLY;
+            case PaintBundle.PORTER_MODE_ADD:
+                return PorterDuff.Mode.ADD;
+        }
+        return PorterDuff.Mode.SRC_OVER;
+    }
+
+    public static BlendMode origamiToBlendMode(int mode) {
+        switch (mode) {
+            case PaintBundle.BLEND_MODE_CLEAR:
+                return BlendMode.CLEAR;
+            case PaintBundle.BLEND_MODE_SRC:
+                return BlendMode.SRC;
+            case PaintBundle.BLEND_MODE_DST:
+                return BlendMode.DST;
+            case PaintBundle.BLEND_MODE_SRC_OVER:
+                return BlendMode.SRC_OVER;
+            case PaintBundle.BLEND_MODE_DST_OVER:
+                return BlendMode.DST_OVER;
+            case PaintBundle.BLEND_MODE_SRC_IN:
+                return BlendMode.SRC_IN;
+            case PaintBundle.BLEND_MODE_DST_IN:
+                return BlendMode.DST_IN;
+            case PaintBundle.BLEND_MODE_SRC_OUT:
+                return BlendMode.SRC_OUT;
+            case PaintBundle.BLEND_MODE_DST_OUT:
+                return BlendMode.DST_OUT;
+            case PaintBundle.BLEND_MODE_SRC_ATOP:
+                return BlendMode.SRC_ATOP;
+            case PaintBundle.BLEND_MODE_DST_ATOP:
+                return BlendMode.DST_ATOP;
+            case PaintBundle.BLEND_MODE_XOR:
+                return BlendMode.XOR;
+            case PaintBundle.BLEND_MODE_PLUS:
+                return BlendMode.PLUS;
+            case PaintBundle.BLEND_MODE_MODULATE:
+                return BlendMode.MODULATE;
+            case PaintBundle.BLEND_MODE_SCREEN:
+                return BlendMode.SCREEN;
+            case PaintBundle.BLEND_MODE_OVERLAY:
+                return BlendMode.OVERLAY;
+            case PaintBundle.BLEND_MODE_DARKEN:
+                return BlendMode.DARKEN;
+            case PaintBundle.BLEND_MODE_LIGHTEN:
+                return BlendMode.LIGHTEN;
+            case PaintBundle.BLEND_MODE_COLOR_DODGE:
+                return BlendMode.COLOR_DODGE;
+            case PaintBundle.BLEND_MODE_COLOR_BURN:
+                return BlendMode.COLOR_BURN;
+            case PaintBundle.BLEND_MODE_HARD_LIGHT:
+                return BlendMode.HARD_LIGHT;
+            case PaintBundle.BLEND_MODE_SOFT_LIGHT:
+                return BlendMode.SOFT_LIGHT;
+            case PaintBundle.BLEND_MODE_DIFFERENCE:
+                return BlendMode.DIFFERENCE;
+            case PaintBundle.BLEND_MODE_EXCLUSION:
+                return BlendMode.EXCLUSION;
+            case PaintBundle.BLEND_MODE_MULTIPLY:
+                return BlendMode.MULTIPLY;
+            case PaintBundle.BLEND_MODE_HUE:
+                return BlendMode.HUE;
+            case PaintBundle.BLEND_MODE_SATURATION:
+                return BlendMode.SATURATION;
+            case PaintBundle.BLEND_MODE_COLOR:
+                return BlendMode.COLOR;
+            case PaintBundle.BLEND_MODE_LUMINOSITY:
+                return BlendMode.LUMINOSITY;
+            case PaintBundle.BLEND_MODE_NULL:
+                return null;
+        }
+        return null;
+    }
+
+    @Override
+    public void applyPaint(PaintBundle mPaintData) {
+        mPaintData.applyPaintChange(new PaintChanges() {
+            @Override
+            public void setTextSize(float size) {
+                mPaint.setTextSize(size);
+            }
+
+            @Override
+            public void setTypeFace(int fontType, int weight, boolean italic) {
+                int[] type = new int[]{Typeface.NORMAL, Typeface.BOLD,
+                        Typeface.ITALIC, Typeface.BOLD_ITALIC};
+
+                switch (fontType) {
+                    case PaintBundle.FONT_TYPE_DEFAULT: {
+                        if (weight == 400 && !italic) { // for normal case
+                            mPaint.setTypeface(Typeface.DEFAULT);
+                        } else {
+                            mPaint.setTypeface(Typeface.create(Typeface.DEFAULT,
+                                    weight, italic));
+                        }
+                        break;
+                    }
+                    case PaintBundle.FONT_TYPE_SERIF: {
+                        if (weight == 400 && !italic) { // for normal case
+                            mPaint.setTypeface(Typeface.SERIF);
+                        } else {
+                            mPaint.setTypeface(Typeface.create(Typeface.SERIF,
+                                    weight, italic));
+                        }
+                        break;
+                    }
+                    case PaintBundle.FONT_TYPE_SANS_SERIF: {
+                        if (weight == 400 && !italic) { //  for normal case
+                            mPaint.setTypeface(Typeface.SANS_SERIF);
+                        } else {
+                            mPaint.setTypeface(
+                                    Typeface.create(Typeface.SANS_SERIF,
+                                            weight, italic));
+                        }
+                        break;
+                    }
+                    case PaintBundle.FONT_TYPE_MONOSPACE: {
+                        if (weight == 400 && !italic) { //  for normal case
+                            mPaint.setTypeface(Typeface.MONOSPACE);
+                        } else {
+                            mPaint.setTypeface(
+                                    Typeface.create(Typeface.MONOSPACE,
+                                            weight, italic));
+                        }
+
+                        break;
+                    }
+                }
+
+
+            }
+
+
+            @Override
+            public void setStrokeWidth(float width) {
+                mPaint.setStrokeWidth(width);
+            }
+
+            @Override
+            public void setColor(int color) {
+                mPaint.setColor(color);
+            }
+
+            @Override
+            public void setStrokeCap(int cap) {
+                mPaint.setStrokeCap(Paint.Cap.values()[cap]);
+            }
+
+            @Override
+            public void setStyle(int style) {
+                mPaint.setStyle(Paint.Style.values()[style]);
+            }
+
+            @Override
+            public void setShader(int shader, String shaderString) {
+
+            }
+
+            @Override
+            public void setImageFilterQuality(int quality) {
+                System.out.println(">>>>>>>>>>>> ");
+            }
+
+            @Override
+            public void setBlendMode(int mode) {
+                mPaint.setBlendMode(origamiToBlendMode(mode));
+            }
+
+            @Override
+            public void setAlpha(float a) {
+                mPaint.setAlpha((int) (255 * a));
+            }
+
+            @Override
+            public void setStrokeMiter(float miter) {
+                mPaint.setStrokeMiter(miter);
+            }
+
+            @Override
+            public void setStrokeJoin(int join) {
+                mPaint.setStrokeJoin(Paint.Join.values()[join]);
+            }
+
+            @Override
+            public void setFilterBitmap(boolean filter) {
+                mPaint.setFilterBitmap(filter);
+            }
+
+
+            @Override
+            public void setAntiAlias(boolean aa) {
+                mPaint.setAntiAlias(aa);
+            }
+
+            @Override
+            public void clear(long mask) {
+                if (true) return;
+                long m = mask;
+                int k = 1;
+                while (m > 0) {
+                    if ((m & 1) == 1L) {
+                        switch (k) {
+
+                            case PaintBundle.COLOR_FILTER:
+                                mPaint.setColorFilter(null);
+                                System.out.println(">>>>>>>>>>>>> CLEAR!!!!");
+                                break;
+                        }
+                    }
+                    k++;
+                    m = m >> 1;
+                }
+            }
+
+            Shader.TileMode[] mTilesModes = new Shader.TileMode[]{
+                    Shader.TileMode.CLAMP,
+                    Shader.TileMode.REPEAT,
+                    Shader.TileMode.MIRROR};
+
+
+            @Override
+            public void setLinearGradient(int[] colors,
+                                          float[] stops,
+                                          float startX,
+                                          float startY,
+                                          float endX,
+                                          float endY,
+                                          int tileMode) {
+                mPaint.setShader(new LinearGradient(startX,
+                        startY,
+                        endX,
+                        endY, colors, stops, mTilesModes[tileMode]));
+
+            }
+
+            @Override
+            public void setRadialGradient(int[] colors,
+                                          float[] stops,
+                                          float centerX,
+                                          float centerY,
+                                          float radius,
+                                          int tileMode) {
+                mPaint.setShader(new RadialGradient(centerX, centerY, radius,
+                        colors, stops, mTilesModes[tileMode]));
+            }
+
+            @Override
+            public void setSweepGradient(int[] colors,
+                                         float[] stops,
+                                         float centerX,
+                                         float centerY) {
+                mPaint.setShader(new SweepGradient(centerX, centerY, colors, stops));
+
+            }
+
+            @Override
+            public void setColorFilter(int color, int mode) {
+                PorterDuff.Mode pmode = origamiToPorterDuffMode(mode);
+                System.out.println("setting color filter to " + pmode.name());
+                if (pmode != null) {
+                    mPaint.setColorFilter(
+                            new PorterDuffColorFilter(color, pmode));
+                }
+            }
+        });
+    }
+
+    @Override
+    public void mtrixScale(float scaleX,
+                           float scaleY,
+                           float centerX,
+                           float centerY) {
+        if (Float.isNaN(centerX)) {
+            mCanvas.scale(scaleX, scaleY);
+        } else {
+            mCanvas.scale(scaleX, scaleY, centerX, centerY);
+        }
+    }
+
+    @Override
+    public void matrixTranslate(float translateX, float translateY) {
+        mCanvas.translate(translateX, translateY);
+    }
+
+    @Override
+    public void matrixSkew(float skewX, float skewY) {
+        mCanvas.skew(skewX, skewY);
+    }
+
+    @Override
+    public void matrixRotate(float rotate, float pivotX, float pivotY) {
+        if (Float.isNaN(pivotX)) {
+            mCanvas.rotate(rotate);
+        } else {
+            mCanvas.rotate(rotate, pivotX, pivotY);
+
+        }
+    }
+
+    @Override
+    public void matrixSave() {
+        mCanvas.save();
+    }
+
+    @Override
+    public void matrixRestore() {
+        mCanvas.restore();
+    }
+
+    @Override
+    public void clipRect(float left, float top, float right, float bottom) {
+        mCanvas.clipRect(left, top, right, bottom);
+    }
+
+    @Override
+    public void clipPath(int pathId, int regionOp) {
+        Path path = getPath(pathId, 0, 1);
+        if (regionOp == ClipPath.DIFFERENCE) {
+            mCanvas.clipOutPath(path); // DIFFERENCE
+        } else {
+            mCanvas.clipPath(path);  // INTERSECT
+        }
+    }
+
+    private Path getPath(int path1Id,
+                         int path2Id,
+                         float tween,
+                         float start,
+                         float end) {
+        if (tween == 0.0f) {
+            return getPath(path1Id, start, end);
+        }
+        if (tween == 1.0f) {
+            return getPath(path2Id, start, end);
+        }
+        AndroidRemoteContext androidContext = (AndroidRemoteContext) mContext;
+        float[] data1 =
+                (float[]) androidContext.mRemoteComposeState.getFromId(path1Id);
+        float[] data2 =
+                (float[]) androidContext.mRemoteComposeState.getFromId(path2Id);
+        float[] tmp = new float[data2.length];
+        for (int i = 0; i < tmp.length; i++) {
+            if (Float.isNaN(data1[i]) || Float.isNaN(data2[i])) {
+                tmp[i] = data1[i];
+            } else {
+                tmp[i] = (data2[i] - data1[i]) * tween + data1[i];
+            }
+        }
+        Path path = new Path();
+        FloatsToPath.genPath(path, tmp, start, end);
+        return path;
+    }
+
+    private Path getPath(int id, float start, float end) {
+        AndroidRemoteContext androidContext = (AndroidRemoteContext) mContext;
+        Path path = new Path();
+        if (androidContext.mRemoteComposeState.containsId(id)) {
+            float[] data =
+                    (float[]) androidContext.mRemoteComposeState.getFromId(id);
+            FloatsToPath.genPath(path, data, start, end);
+        }
+        return path;
+    }
+
+    private String getText(int id) {
+        return (String) mContext.mRemoteComposeState.getFromId(id);
+    }
 }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java
index ce15855..270e96f 100644
--- a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java
+++ b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java
@@ -43,6 +43,13 @@
     // Data handling
     ///////////////////////////////////////////////////////////////////////////////////////////////
 
+    @Override
+    public void loadPathData(int instanceId, float[] floatPath) {
+        if (!mRemoteComposeState.containsId(instanceId)) {
+            mRemoteComposeState.cache(instanceId, floatPath);
+        }
+    }
+
     /**
      * Decode a byte array into an image and cache it using the given imageId
      *
diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/FloatsToPath.java b/core/java/com/android/internal/widget/remotecompose/player/platform/FloatsToPath.java
new file mode 100644
index 0000000..2d766f8
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/player/platform/FloatsToPath.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2024 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.internal.widget.remotecompose.player.platform;
+
+import static com.android.internal.widget.remotecompose.core.operations.Utils.idFromNan;
+
+import android.graphics.Path;
+import android.graphics.PathMeasure;
+import android.os.Build;
+
+import com.android.internal.widget.remotecompose.core.operations.PathData;
+
+public class FloatsToPath {
+    public static void genPath(Path retPath,
+                               float[] floatPath,
+                               float start,
+                               float stop) {
+        int i = 0;
+        Path path = new Path(); // todo this should be cached for performance
+        while (i < floatPath.length) {
+            switch (idFromNan(floatPath[i])) {
+                case PathData.MOVE: {
+                    i++;
+                    path.moveTo(floatPath[i + 0], floatPath[i + 1]);
+                    i += 2;
+                }
+                break;
+                case PathData.LINE: {
+                    i += 3;
+                    path.lineTo(floatPath[i + 0], floatPath[i + 1]);
+                    i += 2;
+                }
+                break;
+                case PathData.QUADRATIC: {
+                    i += 3;
+                    path.quadTo(
+                            floatPath[i + 0],
+                            floatPath[i + 1],
+                            floatPath[i + 2],
+                            floatPath[i + 3]
+                    );
+                    i += 4;
+
+                }
+                break;
+                case PathData.CONIC: {
+                    i += 3;
+                    if (Build.VERSION.SDK_INT >= 34) {
+                        path.conicTo(
+                                floatPath[i + 0], floatPath[i + 1],
+                                floatPath[i + 2], floatPath[i + 3],
+                                floatPath[i + 4]
+                        );
+                    }
+                    i += 5;
+                }
+                break;
+                case PathData.CUBIC: {
+                    i += 3;
+                    path.cubicTo(
+                            floatPath[i + 0], floatPath[i + 1],
+                            floatPath[i + 2], floatPath[i + 3],
+                            floatPath[i + 4], floatPath[i + 5]
+                    );
+                    i += 6;
+                }
+                break;
+                case PathData.CLOSE: {
+
+                    path.close();
+                    i++;
+                }
+                break;
+                case PathData.DONE: {
+                    i++;
+                }
+                break;
+                default: {
+                    System.err.println(" Odd command "
+                            + idFromNan(floatPath[i]));
+                }
+            }
+        }
+
+        retPath.reset();
+        if (start > 0f || stop < 1f) {
+            if (start < stop) {
+
+                PathMeasure measure = new PathMeasure(); // todo cached
+                measure.setPath(path, false);
+                float len = measure.getLength();
+                float scaleStart = Math.max(start, 0f) * len;
+                float scaleStop = Math.min(stop, 1f) * len;
+                measure.getSegment(scaleStart, scaleStop, retPath,
+                        true);
+            }
+        } else {
+
+            retPath.addPath(path);
+        }
+    }
+}
diff --git a/core/jni/android_hardware_Camera.cpp b/core/jni/android_hardware_Camera.cpp
index 5f3a1b5..72b98a2 100644
--- a/core/jni/android_hardware_Camera.cpp
+++ b/core/jni/android_hardware_Camera.cpp
@@ -523,21 +523,23 @@
     }
 }
 
-static jint android_hardware_Camera_getNumberOfCameras(JNIEnv *env, jobject thiz)
-{
-    return Camera::getNumberOfCameras();
+static jint android_hardware_Camera_getNumberOfCameras(JNIEnv *env, jobject thiz, jint deviceId,
+                                                       jint devicePolicy) {
+    return Camera::getNumberOfCameras(deviceId, devicePolicy);
 }
 
 static void android_hardware_Camera_getCameraInfo(JNIEnv *env, jobject thiz, jint cameraId,
-                                                  jboolean overrideToPortrait, jobject info_obj) {
+                                                  jboolean overrideToPortrait, jint deviceId,
+                                                  jint devicePolicy, jobject info_obj) {
     CameraInfo cameraInfo;
-    if (cameraId >= Camera::getNumberOfCameras() || cameraId < 0) {
+    if (cameraId >= Camera::getNumberOfCameras(deviceId, devicePolicy) || cameraId < 0) {
         ALOGE("%s: Unknown camera ID %d", __FUNCTION__, cameraId);
         jniThrowRuntimeException(env, "Unknown camera ID");
         return;
     }
 
-    status_t rc = Camera::getCameraInfo(cameraId, overrideToPortrait, &cameraInfo);
+    status_t rc = Camera::getCameraInfo(cameraId, overrideToPortrait, deviceId, devicePolicy,
+                                        &cameraInfo);
     if (rc != NO_ERROR) {
         jniThrowRuntimeException(env, "Fail to get camera info");
         return;
@@ -556,7 +558,8 @@
 static jint android_hardware_Camera_native_setup(JNIEnv *env, jobject thiz, jobject weak_this,
                                                  jint cameraId, jstring clientPackageName,
                                                  jboolean overrideToPortrait,
-                                                 jboolean forceSlowJpegMode) {
+                                                 jboolean forceSlowJpegMode, jint deviceId,
+                                                 jint devicePolicy) {
     // Convert jstring to String16
     const char16_t *rawClientName = reinterpret_cast<const char16_t*>(
         env->GetStringChars(clientPackageName, NULL));
@@ -568,7 +571,8 @@
     int targetSdkVersion = android_get_application_target_sdk_version();
     sp<Camera> camera =
             Camera::connect(cameraId, clientName, Camera::USE_CALLING_UID, Camera::USE_CALLING_PID,
-                            targetSdkVersion, overrideToPortrait, forceSlowJpegMode);
+                            targetSdkVersion, overrideToPortrait, forceSlowJpegMode, deviceId,
+                            devicePolicy);
     if (camera == NULL) {
         return -EACCES;
     }
@@ -596,7 +600,8 @@
 
     // Update default display orientation in case the sensor is reverse-landscape
     CameraInfo cameraInfo;
-    status_t rc = Camera::getCameraInfo(cameraId, overrideToPortrait, &cameraInfo);
+    status_t rc = Camera::getCameraInfo(cameraId, overrideToPortrait, deviceId, devicePolicy,
+                                        &cameraInfo);
     if (rc != NO_ERROR) {
         ALOGE("%s: getCameraInfo error: %d", __FUNCTION__, rc);
         return rc;
@@ -1051,10 +1056,10 @@
 //-------------------------------------------------
 
 static const JNINativeMethod camMethods[] = {
-        {"getNumberOfCameras", "()I", (void *)android_hardware_Camera_getNumberOfCameras},
-        {"_getCameraInfo", "(IZLandroid/hardware/Camera$CameraInfo;)V",
+        {"_getNumberOfCameras", "(II)I", (void *)android_hardware_Camera_getNumberOfCameras},
+        {"_getCameraInfo", "(IZIILandroid/hardware/Camera$CameraInfo;)V",
          (void *)android_hardware_Camera_getCameraInfo},
-        {"native_setup", "(Ljava/lang/Object;ILjava/lang/String;ZZ)I",
+        {"native_setup", "(Ljava/lang/Object;ILjava/lang/String;ZZII)I",
          (void *)android_hardware_Camera_native_setup},
         {"native_release", "()V", (void *)android_hardware_Camera_release},
         {"setPreviewSurface", "(Landroid/view/Surface;)V",
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index a3dba48..c6706cb 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -5312,6 +5312,13 @@
     <!-- Zen mode - Condition summary when a rule is deactivated due to a call to setInterruptionFilter(). [CHAR_LIMIT=NONE] -->
     <string name="zen_mode_implicit_deactivated">Off</string>
 
+    <!-- [CHAR LIMIT=40] General divider text when concatenating multiple items in a text summary, used for the trigger description of a Zen mode -->
+    <string name="zen_mode_trigger_summary_divider_text">,\u0020</string>
+    <!-- [CHAR LIMIT=40] General template for a symbolic start - end range in a text summary, used for the trigger description of a Zen mode -->
+    <string name="zen_mode_trigger_summary_range_symbol_combination"><xliff:g id="start" example="Sun">%1$s</xliff:g> - <xliff:g id="end" example="Thu">%2$s</xliff:g></string>
+    <!-- [CHAR LIMIT=40] Event-based rule calendar option value for any calendar, used for the trigger description of a Zen mode -->
+    <string name="zen_mode_trigger_event_calendar_any">Any calendar</string>
+
     <!-- Indication that the current volume and other effects (vibration) are being suppressed by a third party, such as a notification listener. [CHAR LIMIT=30] -->
     <string name="muted_by"><xliff:g id="third_party">%1$s</xliff:g> is muting some sounds</string>
 
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 06c0188..fe4e4f04 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2609,6 +2609,10 @@
   <java-symbol type="string" name="zen_mode_implicit_trigger_description" />
   <java-symbol type="string" name="zen_mode_implicit_activated" />
   <java-symbol type="string" name="zen_mode_implicit_deactivated" />
+  <java-symbol type="string" name="zen_mode_trigger_summary_divider_text" />
+  <java-symbol type="string" name="zen_mode_trigger_summary_range_symbol_combination" />
+  <java-symbol type="string" name="zen_mode_trigger_event_calendar_any" />
+
   <java-symbol type="string" name="display_rotation_camera_compat_toast_after_rotation" />
   <java-symbol type="string" name="display_rotation_camera_compat_toast_in_multi_window" />
   <java-symbol type="array" name="config_system_condition_providers" />
diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml
index 62d58b6..c05ea3d 100644
--- a/core/tests/coretests/AndroidManifest.xml
+++ b/core/tests/coretests/AndroidManifest.xml
@@ -254,6 +254,17 @@
             </intent-filter>
         </activity>
 
+        <activity android:name="android.widget.AbsListViewActivity"
+                android:label="AbsListViewActivity"
+                android:screenOrientation="portrait"
+                android:exported="true"
+                android:theme="@android:style/Theme.Material.Light">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+            </intent-filter>
+        </activity>
+
         <activity android:name="android.widget.DatePickerActivity"
                 android:label="DatePickerActivity"
                 android:screenOrientation="portrait"
diff --git a/core/tests/coretests/res/layout/activity_abslist_view.xml b/core/tests/coretests/res/layout/activity_abslist_view.xml
new file mode 100644
index 0000000..85b4f11
--- /dev/null
+++ b/core/tests/coretests/res/layout/activity_abslist_view.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+  <view
+      class="android.widget.AbsListViewFunctionalTest$MyListView"
+      android:id="@+id/list_view"
+      android:layout_width="match_parent"
+      android:layout_height="match_parent"
+  />
+</LinearLayout>
\ No newline at end of file
diff --git a/core/tests/coretests/src/android/view/ViewFrameRateTest.java b/core/tests/coretests/src/android/view/ViewFrameRateTest.java
index 226629e..dcdb8b0 100644
--- a/core/tests/coretests/src/android/view/ViewFrameRateTest.java
+++ b/core/tests/coretests/src/android/view/ViewFrameRateTest.java
@@ -19,7 +19,6 @@
 import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH;
 import static android.view.Surface.FRAME_RATE_CATEGORY_LOW;
 import static android.view.Surface.FRAME_RATE_CATEGORY_NORMAL;
-import static android.view.flags.Flags.FLAG_TOOLKIT_FRAME_RATE_DEFAULT_NORMAL_READ_ONLY;
 import static android.view.flags.Flags.FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY;
 import static android.view.flags.Flags.FLAG_VIEW_VELOCITY_API;
 import static android.view.flags.Flags.toolkitFrameRateBySizeReadOnly;
@@ -137,8 +136,8 @@
         mActivityRule.runOnUiThread(() -> {
             float density = mActivity.getResources().getDisplayMetrics().density;
             ViewGroup.LayoutParams layoutParams = mMovingView.getLayoutParams();
-            layoutParams.height = (int) (40 * density);
-            layoutParams.width = (int) (40 * density);
+            layoutParams.height = 4 * ((int) (10 * density));
+            layoutParams.width = 4 * ((int) (10 * density));
             mMovingView.setLayoutParams(layoutParams);
             mMovingView.getViewTreeObserver().addOnDrawListener(drawLatch1::countDown);
         });
@@ -212,8 +211,8 @@
         mActivityRule.runOnUiThread(() -> {
             float density = mActivity.getResources().getDisplayMetrics().density;
             ViewGroup.LayoutParams layoutParams = mMovingView.getLayoutParams();
-            layoutParams.height = (int) (40 * density);
-            layoutParams.width = (int) Math.ceil(41 * density);
+            layoutParams.height = 4 * ((int) (10 * density));
+            layoutParams.width = 4 * ((int) Math.ceil(10 * density)) + 1;
             mMovingView.setLayoutParams(layoutParams);
             mMovingView.getViewTreeObserver().addOnDrawListener(drawLatch1::countDown);
         });
@@ -237,8 +236,8 @@
         mActivityRule.runOnUiThread(() -> {
             float density = mActivity.getResources().getDisplayMetrics().density;
             ViewGroup.LayoutParams layoutParams = mMovingView.getLayoutParams();
-            layoutParams.height = (int) Math.ceil(41 * density);
-            layoutParams.width = (int) (40 * density);
+            layoutParams.height = 4 * ((int) Math.ceil(10 * density)) + 1;
+            layoutParams.width = 4 * ((int) (10 * density));
             mMovingView.setLayoutParams(layoutParams);
             mMovingView.getViewTreeObserver().addOnDrawListener(drawLatch1::countDown);
         });
@@ -256,13 +255,14 @@
     }
 
     @Test
-    @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY,
-            FLAG_TOOLKIT_FRAME_RATE_DEFAULT_NORMAL_READ_ONLY})
+    @RequiresFlagsEnabled(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY)
     public void defaultNormal() throws Throwable {
         waitForFrameRateCategoryToSettle();
         mActivityRule.runOnUiThread(() -> {
             mMovingView.invalidate();
-            assertEquals(FRAME_RATE_CATEGORY_NORMAL,
+            int expected = toolkitFrameRateDefaultNormalReadOnly()
+                    ? FRAME_RATE_CATEGORY_NORMAL : FRAME_RATE_CATEGORY_HIGH;
+            assertEquals(expected,
                     mViewRoot.getPreferredFrameRateCategory());
         });
     }
diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java
index d95a039..a034f3b 100644
--- a/core/tests/coretests/src/android/view/ViewRootImplTest.java
+++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java
@@ -667,7 +667,7 @@
     }
 
     /**
-     * Test how values of the frame rate cateogry are aggregated.
+     * Test how values of the frame rate category are aggregated.
      * It should take the max value among all of the voted categories per frame.
      */
     @Test
diff --git a/core/tests/coretests/src/android/widget/AbsListViewActivity.java b/core/tests/coretests/src/android/widget/AbsListViewActivity.java
new file mode 100644
index 0000000..a617fa4
--- /dev/null
+++ b/core/tests/coretests/src/android/widget/AbsListViewActivity.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 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.widget;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+import com.android.frameworks.coretests.R;
+
+/**
+ * An activity for testing the AbsListView widget.
+ */
+public class AbsListViewActivity extends Activity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_abslist_view);
+    }
+}
diff --git a/core/tests/coretests/src/android/widget/AbsListViewFunctionalTest.java b/core/tests/coretests/src/android/widget/AbsListViewFunctionalTest.java
new file mode 100644
index 0000000..ceea6ca
--- /dev/null
+++ b/core/tests/coretests/src/android/widget/AbsListViewFunctionalTest.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2024 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.widget;
+
+import static android.view.flags.Flags.FLAG_VIEW_VELOCITY_API;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.util.AttributeSet;
+import android.util.PollingCheck;
+
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.WidgetTestUtils;
+import com.android.frameworks.coretests.R;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class AbsListViewFunctionalTest {
+    private final String[] mCountryList = new String[] {
+        "Argentina", "Australia", "Belize", "Botswana", "Brazil", "Cameroon", "China", "Cyprus",
+        "Denmark", "Djibouti", "Ethiopia", "Fiji", "Finland", "France", "Gabon", "Germany",
+        "Ghana", "Haiti", "Honduras", "Iceland", "India", "Indonesia", "Ireland", "Italy",
+        "Japan", "Kiribati", "Laos", "Lesotho", "Liberia", "Malaysia", "Mongolia", "Myanmar",
+        "Nauru", "Norway", "Oman", "Pakistan", "Philippines", "Portugal", "Romania", "Russia",
+        "Rwanda", "Singapore", "Slovakia", "Slovenia", "Somalia", "Swaziland", "Togo", "Tuvalu",
+        "Uganda", "Ukraine", "United States", "Vanuatu", "Venezuela", "Zimbabwe"
+    };
+    private AbsListViewActivity mActivity;
+    private MyListView mMyListView;
+
+    @Rule
+    public ActivityTestRule<AbsListViewActivity> mActivityRule = new ActivityTestRule<>(
+            AbsListViewActivity.class);
+
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    @Before
+    public void setUp() throws Exception {
+        mActivity = mActivityRule.getActivity();
+        mMyListView = (MyListView) mActivity.findViewById(R.id.list_view);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_VIEW_VELOCITY_API)
+    public void testLsitViewSetVelocity() throws Throwable {
+        final ArrayList<String> items = new ArrayList<>(Arrays.asList(mCountryList));
+        final ArrayAdapter<String> adapter = new ArrayAdapter<String>(mActivity,
+                android.R.layout.simple_list_item_1, items);
+
+        WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mMyListView,
+                () -> mMyListView.setAdapter(adapter));
+        mActivityRule.runOnUiThread(() -> {
+            // Create an adapter to display the list
+            mMyListView.setFrameContentVelocity(0);
+        });
+        // set setFrameContentVelocity shouldn't do anything.
+        assertEquals(mMyListView.isSetVelocityCalled, false);
+
+        mActivityRule.runOnUiThread(() -> {
+            mMyListView.fling(100);
+        });
+        PollingCheck.waitFor(100, () -> mMyListView.isSetVelocityCalled);
+        // set setFrameContentVelocity should be called when fling.
+        assertTrue(mMyListView.isSetVelocityCalled);
+    }
+
+    public static class MyListView extends ListView {
+
+        public boolean isSetVelocityCalled;
+
+        public MyListView(Context context) {
+            super(context);
+        }
+
+        public MyListView(Context context, AttributeSet attrs) {
+            super(context, attrs);
+        }
+
+        public MyListView(Context context, AttributeSet attrs, int defStyle) {
+            super(context, attrs, defStyle);
+        }
+
+        public MyListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+            super(context, attrs, defStyleAttr, defStyleRes);
+        }
+
+        @Override
+        public void setFrameContentVelocity(float pixelsPerSecond) {
+            if (pixelsPerSecond != 0) {
+                isSetVelocityCalled = true;
+            }
+        }
+    }
+}
diff --git a/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateInfoTest.java b/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateInfoTest.java
index 0897726..cf7c549 100644
--- a/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateInfoTest.java
+++ b/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateInfoTest.java
@@ -38,6 +38,7 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
 
@@ -73,8 +74,8 @@
 
     @Test
     public void create() {
-        final List<DeviceState> supportedStates = List.of(DEVICE_STATE_0, DEVICE_STATE_1,
-                DEVICE_STATE_2);
+        final ArrayList<DeviceState> supportedStates = new ArrayList<>(
+                List.of(DEVICE_STATE_0, DEVICE_STATE_1, DEVICE_STATE_2));
         final DeviceState baseState = DEVICE_STATE_0;
         final DeviceState currentState = DEVICE_STATE_2;
 
@@ -87,8 +88,8 @@
 
     @Test
     public void equals() {
-        final List<DeviceState> supportedStates = List.of(DEVICE_STATE_0, DEVICE_STATE_1,
-                DEVICE_STATE_2);
+        final ArrayList<DeviceState> supportedStates = new ArrayList<>(
+                List.of(DEVICE_STATE_0, DEVICE_STATE_1, DEVICE_STATE_2));
         final DeviceState baseState = DEVICE_STATE_0;
         final DeviceState currentState = DEVICE_STATE_2;
 
@@ -100,15 +101,14 @@
         Assert.assertEquals(info, sameInfo);
 
         final DeviceStateInfo differentInfo = new DeviceStateInfo(
-                List.of(DEVICE_STATE_0, DEVICE_STATE_2), baseState,
-                currentState);
+                new ArrayList<>(List.of(DEVICE_STATE_0, DEVICE_STATE_2)), baseState, currentState);
         assertNotEquals(info, differentInfo);
     }
 
     @Test
     public void diff_sameObject() {
-        final List<DeviceState> supportedStates = List.of(DEVICE_STATE_0, DEVICE_STATE_1,
-                DEVICE_STATE_2);
+        final ArrayList<DeviceState> supportedStates = new ArrayList<>(
+                List.of(DEVICE_STATE_0, DEVICE_STATE_1, DEVICE_STATE_2));
         final DeviceState baseState = DEVICE_STATE_0;
         final DeviceState currentState = DEVICE_STATE_2;
 
@@ -118,10 +118,10 @@
 
     @Test
     public void diff_differentSupportedStates() {
-        final DeviceStateInfo info = new DeviceStateInfo(List.of(DEVICE_STATE_1), DEVICE_STATE_0,
-                DEVICE_STATE_0);
-        final DeviceStateInfo otherInfo = new DeviceStateInfo(List.of(DEVICE_STATE_2),
+        final DeviceStateInfo info = new DeviceStateInfo(new ArrayList<>(List.of(DEVICE_STATE_1)),
                 DEVICE_STATE_0, DEVICE_STATE_0);
+        final DeviceStateInfo otherInfo = new DeviceStateInfo(
+                new ArrayList<>(List.of(DEVICE_STATE_2)), DEVICE_STATE_0, DEVICE_STATE_0);
         final int diff = info.diff(otherInfo);
         assertTrue((diff & DeviceStateInfo.CHANGED_SUPPORTED_STATES) > 0);
         assertFalse((diff & DeviceStateInfo.CHANGED_BASE_STATE) > 0);
@@ -130,10 +130,10 @@
 
     @Test
     public void diff_differentNonOverrideState() {
-        final DeviceStateInfo info = new DeviceStateInfo(List.of(DEVICE_STATE_1), DEVICE_STATE_1,
-                DEVICE_STATE_0);
-        final DeviceStateInfo otherInfo = new DeviceStateInfo(List.of(DEVICE_STATE_1),
-                DEVICE_STATE_2, DEVICE_STATE_0);
+        final DeviceStateInfo info = new DeviceStateInfo(new ArrayList<>(List.of(DEVICE_STATE_1)),
+                DEVICE_STATE_1, DEVICE_STATE_0);
+        final DeviceStateInfo otherInfo = new DeviceStateInfo(
+                new ArrayList<>(List.of(DEVICE_STATE_1)), DEVICE_STATE_2, DEVICE_STATE_0);
         final int diff = info.diff(otherInfo);
         assertFalse((diff & DeviceStateInfo.CHANGED_SUPPORTED_STATES) > 0);
         assertTrue((diff & DeviceStateInfo.CHANGED_BASE_STATE) > 0);
@@ -142,10 +142,10 @@
 
     @Test
     public void diff_differentState() {
-        final DeviceStateInfo info = new DeviceStateInfo(List.of(DEVICE_STATE_1), DEVICE_STATE_0,
-                DEVICE_STATE_1);
-        final DeviceStateInfo otherInfo = new DeviceStateInfo(List.of(DEVICE_STATE_1),
-                DEVICE_STATE_0, DEVICE_STATE_2);
+        final DeviceStateInfo info = new DeviceStateInfo(new ArrayList<>(List.of(DEVICE_STATE_1)),
+                DEVICE_STATE_0, DEVICE_STATE_1);
+        final DeviceStateInfo otherInfo = new DeviceStateInfo(
+                new ArrayList<>(List.of(DEVICE_STATE_1)), DEVICE_STATE_0, DEVICE_STATE_2);
         final int diff = info.diff(otherInfo);
         assertFalse((diff & DeviceStateInfo.CHANGED_SUPPORTED_STATES) > 0);
         assertFalse((diff & DeviceStateInfo.CHANGED_BASE_STATE) > 0);
@@ -154,8 +154,8 @@
 
     @Test
     public void writeToParcel() {
-        final List<DeviceState> supportedStates = List.of(DEVICE_STATE_0, DEVICE_STATE_1,
-                DEVICE_STATE_2);
+        final ArrayList<DeviceState> supportedStates = new ArrayList<>(
+                List.of(DEVICE_STATE_0, DEVICE_STATE_1, DEVICE_STATE_2));
         final DeviceState nonOverrideState = DEVICE_STATE_0;
         final DeviceState state = DEVICE_STATE_2;
         final DeviceStateInfo originalInfo =
diff --git a/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateManagerGlobalTest.java b/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateManagerGlobalTest.java
index ee238c0..f4d3631 100644
--- a/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateManagerGlobalTest.java
+++ b/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateManagerGlobalTest.java
@@ -40,6 +40,7 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
 
+import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -276,7 +277,7 @@
                     new DeviceState.Configuration.Builder(mergedBaseState, "" /* name */).build());
             final DeviceState state = new DeviceState(
                     new DeviceState.Configuration.Builder(mergedState, "" /* name */).build());
-            return new DeviceStateInfo(mSupportedDeviceStates, baseState, state);
+            return new DeviceStateInfo(new ArrayList<>(mSupportedDeviceStates), baseState, state);
         }
 
         private void notifyDeviceStateInfoChanged() {
diff --git a/graphics/java/android/graphics/fonts/FontFamily.java b/graphics/java/android/graphics/fonts/FontFamily.java
index e441c53..685fd82 100644
--- a/graphics/java/android/graphics/fonts/FontFamily.java
+++ b/graphics/java/android/graphics/fonts/FontFamily.java
@@ -120,24 +120,6 @@
         }
 
         /**
-         * Returns true if the passed font files can be used for building a variable font family
-         * that automatically adjust the `wght` and `ital` axes value for the requested
-         * weight/italic style values.
-         *
-         * This method can be used for checking that the provided font files can be used for
-         * building a variable font family created with {@link #buildVariableFamily()}.
-         * If this function returns false, the {@link #buildVariableFamily()} will fail and
-         * return null.
-         *
-         * @return true if a variable font can be built from the given fonts. Otherwise, false.
-         */
-        @FlaggedApi(FLAG_NEW_FONTS_FALLBACK_XML)
-        public boolean canBuildVariableFamily() {
-            int variableFamilyType = analyzeAndResolveVariableType(mFonts);
-            return variableFamilyType != VARIABLE_FONT_FAMILY_TYPE_UNKNOWN;
-        }
-
-        /**
          * Build a variable font family that automatically adjust the `wght` and `ital` axes value
          * for the requested weight/italic style values.
          *
@@ -158,9 +140,7 @@
          * value of the supported `wght`axis, the maximum supported `wght` value is used. The weight
          * value of the font is ignored.
          *
-         * If none of the above conditions are met, this function return {@code null}. Please check
-         * that your font files meet the above requirements or consider using the {@link #build()}
-         * method.
+         * If none of the above conditions are met, this function return {@code null}.
          *
          * @return A variable font family. null if a variable font cannot be built from the given
          *         fonts.
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp
index 8829d1b..9b14ce4 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -45,7 +45,6 @@
     name: "wm_shell_util-sources",
     srcs: [
         "src/com/android/wm/shell/animation/Interpolators.java",
-        "src/com/android/wm/shell/animation/PhysicsAnimator.kt",
         "src/com/android/wm/shell/common/bubbles/*.kt",
         "src/com/android/wm/shell/common/bubbles/*.java",
         "src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt",
@@ -169,7 +168,13 @@
 java_library {
     name: "WindowManager-Shell-shared",
 
-    srcs: ["shared/**/*.java"],
+    srcs: [
+        "shared/**/*.java",
+        "shared/**/*.kt",
+    ],
+    static_libs: [
+        "androidx.dynamicanimation_dynamicanimation",
+    ],
 }
 
 android_library {
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt
index f3d70f7..35a4a62 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt
@@ -34,11 +34,11 @@
 import com.android.internal.protolog.common.ProtoLog
 import com.android.launcher3.icons.BubbleIconFactory
 import com.android.wm.shell.R
-import com.android.wm.shell.animation.PhysicsAnimatorTestUtils
 import com.android.wm.shell.bubbles.Bubbles.SysuiProxy
 import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix
 import com.android.wm.shell.common.FloatingContentCoordinator
 import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils
 import com.android.wm.shell.taskview.TaskView
 import com.android.wm.shell.taskview.TaskViewTaskController
 import com.google.common.truth.Truth.assertThat
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimator.kt
similarity index 99%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt
rename to libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimator.kt
index b7f0890..9d3b56d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimator.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.animation
+package com.android.wm.shell.shared.animation
 
 import android.util.ArrayMap
 import android.util.Log
@@ -25,7 +25,7 @@
 import androidx.dynamicanimation.animation.SpringAnimation
 import androidx.dynamicanimation.animation.SpringForce
 
-import com.android.wm.shell.animation.PhysicsAnimator.Companion.getInstance
+import com.android.wm.shell.shared.animation.PhysicsAnimator.Companion.getInstance
 import java.lang.ref.WeakReference
 import java.util.WeakHashMap
 import kotlin.math.abs
@@ -874,7 +874,7 @@
      *
      * @param <T> The type of the object being animated.
     </T> */
-    interface UpdateListener<T> {
+    fun interface UpdateListener<T> {
 
         /**
          * Called on each animation frame with the target object, and a map of FloatPropertyCompat
@@ -904,7 +904,7 @@
      *
      * @param <T> The type of the object being animated.
     </T> */
-    interface EndListener<T> {
+    fun interface EndListener<T> {
 
         /**
          * Called with the final animation values as each property animation ends. This can be used
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimatorTestUtils.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTestUtils.kt
similarity index 98%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimatorTestUtils.kt
rename to libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTestUtils.kt
index 7defc26..235b9bf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimatorTestUtils.kt
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTestUtils.kt
@@ -13,13 +13,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.wm.shell.animation
+package com.android.wm.shell.shared.animation
 
 import android.os.Handler
 import android.os.Looper
 import android.util.ArrayMap
 import androidx.dynamicanimation.animation.FloatPropertyCompat
-import com.android.wm.shell.animation.PhysicsAnimatorTestUtils.prepareForTest
+import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils.prepareForTest
 import java.util.*
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
index 6908682..8da85d2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java
@@ -82,7 +82,6 @@
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.wm.shell.R;
 import com.android.wm.shell.animation.Interpolators;
-import com.android.wm.shell.animation.PhysicsAnimator;
 import com.android.wm.shell.bubbles.BubblesNavBarMotionEventHandler.MotionEventListener;
 import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix;
 import com.android.wm.shell.bubbles.animation.ExpandedAnimationController;
@@ -95,6 +94,7 @@
 import com.android.wm.shell.common.bubbles.DismissView;
 import com.android.wm.shell.common.bubbles.RelativeTouchListener;
 import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
+import com.android.wm.shell.shared.animation.PhysicsAnimator;
 
 import java.io.PrintWriter;
 import java.math.BigDecimal;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
index 512c9d1..1fb966f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/ExpandedAnimationController.java
@@ -34,12 +34,12 @@
 
 import com.android.wm.shell.R;
 import com.android.wm.shell.animation.Interpolators;
-import com.android.wm.shell.animation.PhysicsAnimator;
 import com.android.wm.shell.bubbles.BadgedImageView;
 import com.android.wm.shell.bubbles.BubbleOverflow;
 import com.android.wm.shell.bubbles.BubblePositioner;
 import com.android.wm.shell.bubbles.BubbleStackView;
 import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
+import com.android.wm.shell.shared.animation.PhysicsAnimator;
 
 import com.google.android.collect.Sets;
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java
index bb0dd95..47d4d07 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/StackAnimationController.java
@@ -38,12 +38,12 @@
 import androidx.dynamicanimation.animation.SpringForce;
 
 import com.android.wm.shell.R;
-import com.android.wm.shell.animation.PhysicsAnimator;
 import com.android.wm.shell.bubbles.BadgedImageView;
 import com.android.wm.shell.bubbles.BubblePositioner;
 import com.android.wm.shell.bubbles.BubbleStackView;
 import com.android.wm.shell.common.FloatingContentCoordinator;
 import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
+import com.android.wm.shell.shared.animation.PhysicsAnimator;
 
 import com.google.android.collect.Sets;
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
index 9eb9632..8af4c75 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
@@ -43,12 +43,12 @@
 import androidx.annotation.Nullable;
 
 import com.android.wm.shell.animation.Interpolators;
-import com.android.wm.shell.animation.PhysicsAnimator;
 import com.android.wm.shell.bubbles.BubbleOverflow;
 import com.android.wm.shell.bubbles.BubblePositioner;
 import com.android.wm.shell.bubbles.BubbleViewProvider;
 import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix;
 import com.android.wm.shell.common.magnetictarget.MagnetizedObject.MagneticTarget;
+import com.android.wm.shell.shared.animation.PhysicsAnimator;
 
 /**
  * Helper class to animate a {@link BubbleBarExpandedView} on a bubble.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java
index 81e7582..02918db 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java
@@ -29,8 +29,8 @@
 import androidx.dynamicanimation.animation.SpringForce;
 
 import com.android.wm.shell.R;
-import com.android.wm.shell.animation.PhysicsAnimator;
 import com.android.wm.shell.bubbles.Bubble;
+import com.android.wm.shell.shared.animation.PhysicsAnimator;
 
 import java.util.ArrayList;
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleEducationViewController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleEducationViewController.kt
index ee552ae..e108f7b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleEducationViewController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleEducationViewController.kt
@@ -28,7 +28,6 @@
 import androidx.dynamicanimation.animation.DynamicAnimation
 import androidx.dynamicanimation.animation.SpringForce
 import com.android.wm.shell.R
-import com.android.wm.shell.animation.PhysicsAnimator
 import com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_USER_EDUCATION
 import com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES
 import com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME
@@ -37,6 +36,7 @@
 import com.android.wm.shell.bubbles.setup
 import com.android.wm.shell.common.bubbles.BubblePopupDrawable
 import com.android.wm.shell.common.bubbles.BubblePopupView
+import com.android.wm.shell.shared.animation.PhysicsAnimator
 import kotlin.math.roundToInt
 
 /** Manages bubble education presentation and animation */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/DismissView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/DismissView.kt
index 9094739..e06de9e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/DismissView.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/DismissView.kt
@@ -35,7 +35,7 @@
 import androidx.dynamicanimation.animation.DynamicAnimation
 import androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_LOW_BOUNCY
 import androidx.dynamicanimation.animation.SpringForce.STIFFNESS_LOW
-import com.android.wm.shell.animation.PhysicsAnimator
+import com.android.wm.shell.shared.animation.PhysicsAnimator
 
 /**
  * View that handles interactions between DismissCircleView and BubbleStackView.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt
index 11e4777..123d4dc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/magnetictarget/MagnetizedObject.kt
@@ -28,7 +28,7 @@
 import androidx.dynamicanimation.animation.DynamicAnimation
 import androidx.dynamicanimation.animation.FloatPropertyCompat
 import androidx.dynamicanimation.animation.SpringForce
-import com.android.wm.shell.animation.PhysicsAnimator
+import com.android.wm.shell.shared.animation.PhysicsAnimator
 import kotlin.math.abs
 import kotlin.math.hypot
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
index df67707..ef46843 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
@@ -37,7 +37,6 @@
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.R;
 import com.android.wm.shell.animation.FloatProperties;
-import com.android.wm.shell.animation.PhysicsAnimator;
 import com.android.wm.shell.common.FloatingContentCoordinator;
 import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
 import com.android.wm.shell.common.pip.PipAppOpsListener;
@@ -47,6 +46,7 @@
 import com.android.wm.shell.pip.PipTaskOrganizer;
 import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.animation.PhysicsAnimator;
 
 import kotlin.Unit;
 import kotlin.jvm.functions.Function0;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt
index a4fb350..8bb182d 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/magnetictarget/MagnetizedObjectTest.kt
@@ -22,7 +22,7 @@
 import androidx.dynamicanimation.animation.FloatPropertyCompat
 import androidx.test.filters.SmallTest
 import com.android.wm.shell.ShellTestCase
-import com.android.wm.shell.animation.PhysicsAnimatorTestUtils
+import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/animation/PhysicsAnimatorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTest.kt
similarity index 97%
rename from libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/animation/PhysicsAnimatorTest.kt
rename to libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTest.kt
index e727491..3fb66be 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/animation/PhysicsAnimatorTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/animation/PhysicsAnimatorTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.animation
+package com.android.wm.shell.shared.animation
 
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
@@ -27,11 +27,11 @@
 import androidx.dynamicanimation.animation.SpringForce
 import androidx.test.filters.SmallTest
 import com.android.wm.shell.ShellTestCase
-import com.android.wm.shell.animation.PhysicsAnimator.EndListener
-import com.android.wm.shell.animation.PhysicsAnimator.UpdateListener
-import com.android.wm.shell.animation.PhysicsAnimatorTestUtils.clearAnimationUpdateFrames
-import com.android.wm.shell.animation.PhysicsAnimatorTestUtils.getAnimationUpdateFrames
-import com.android.wm.shell.animation.PhysicsAnimatorTestUtils.verifyAnimationUpdateFrames
+import com.android.wm.shell.shared.animation.PhysicsAnimator.EndListener
+import com.android.wm.shell.shared.animation.PhysicsAnimator.UpdateListener
+import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils.clearAnimationUpdateFrames
+import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils.getAnimationUpdateFrames
+import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils.verifyAnimationUpdateFrames
 import org.junit.After
 import org.junit.Assert
 import org.junit.Assert.assertEquals
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java
index e89becd..fc96896 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraBinderTest.java
@@ -16,6 +16,9 @@
 
 package com.android.mediaframeworktest.integration;
 
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
+import static android.content.Context.DEVICE_ID_DEFAULT;
+
 import android.hardware.CameraInfo;
 import android.hardware.ICamera;
 import android.hardware.ICameraClient;
@@ -75,8 +78,8 @@
 
     @SmallTest
     public void testNumberOfCameras() throws Exception {
-
-        int numCameras = mUtils.getCameraService().getNumberOfCameras(CAMERA_TYPE_ALL);
+        int numCameras = mUtils.getCameraService().getNumberOfCameras(CAMERA_TYPE_ALL,
+                DEVICE_ID_DEFAULT, DEVICE_POLICY_DEFAULT);
         assertTrue("At least this many cameras: " + mUtils.getGuessedNumCameras(),
                 numCameras >= mUtils.getGuessedNumCameras());
         Log.v(TAG, "Number of cameras " + numCameras);
@@ -85,9 +88,8 @@
     @SmallTest
     public void testCameraInfo() throws Exception {
         for (int cameraId = 0; cameraId < mUtils.getGuessedNumCameras(); ++cameraId) {
-
             CameraInfo info = mUtils.getCameraService().getCameraInfo(cameraId,
-                    /*overrideToPortrait*/false);
+                    /*overrideToPortrait*/false, DEVICE_ID_DEFAULT, DEVICE_POLICY_DEFAULT);
             assertTrue("Facing was not set for camera " + cameraId, info.info.facing != -1);
             assertTrue("Orientation was not set for camera " + cameraId,
                     info.info.orientation != -1);
@@ -163,7 +165,8 @@
                             ICameraService.USE_CALLING_PID,
                             getContext().getApplicationInfo().targetSdkVersion,
                             /*overrideToPortrait*/false,
-                            /*forceSlowJpegMode*/false);
+                            /*forceSlowJpegMode*/false,
+                            DEVICE_ID_DEFAULT, DEVICE_POLICY_DEFAULT);
             assertNotNull(String.format("Camera %s was null", cameraId), cameraUser);
 
             Log.v(TAG, String.format("Camera %s connected", cameraId));
@@ -184,7 +187,6 @@
         public void onDeviceError(int errorCode, CaptureResultExtras resultExtras)
                 throws RemoteException {
             // TODO Auto-generated method stub
-
         }
 
         /*
@@ -197,7 +199,6 @@
         public void onCaptureStarted(CaptureResultExtras resultExtras, long timestamp)
                 throws RemoteException {
             // TODO Auto-generated method stub
-
         }
 
         /*
@@ -211,7 +212,6 @@
         public void onResultReceived(CameraMetadataNative result, CaptureResultExtras resultExtras,
                 PhysicalCaptureResultInfo physicalResults[]) throws RemoteException {
             // TODO Auto-generated method stub
-
         }
 
         /*
@@ -221,7 +221,6 @@
         @Override
         public void onDeviceIdle() throws RemoteException {
             // TODO Auto-generated method stub
-
         }
 
         /*
@@ -231,7 +230,6 @@
         @Override
         public void onPrepared(int streamId) throws RemoteException {
             // TODO Auto-generated method stub
-
         }
 
         /*
@@ -241,7 +239,6 @@
         @Override
         public void onRequestQueueEmpty() throws RemoteException {
             // TODO Auto-generated method stub
-
         }
 
         /*
@@ -269,7 +266,7 @@
                         clientPackageName, clientAttributionTag,
                         ICameraService.USE_CALLING_UID, 0 /*oomScoreOffset*/,
                         getContext().getApplicationInfo().targetSdkVersion,
-                        /*overrideToPortrait*/false);
+                        /*overrideToPortrait*/false, DEVICE_ID_DEFAULT, DEVICE_POLICY_DEFAULT);
             assertNotNull(String.format("Camera %s was null", cameraId), cameraUser);
 
             Log.v(TAG, String.format("Camera %s connected", cameraId));
@@ -280,18 +277,18 @@
 
     static class DummyCameraServiceListener extends ICameraServiceListener.Stub {
         @Override
-        public void onStatusChanged(int status, String cameraId)
+        public void onStatusChanged(int status, String cameraId, int deviceId)
                 throws RemoteException {
             Log.v(TAG, String.format("Camera %s has status changed to 0x%x", cameraId, status));
         }
-        public void onTorchStatusChanged(int status, String cameraId)
+        public void onTorchStatusChanged(int status, String cameraId, int deviceId)
                 throws RemoteException {
             Log.v(TAG, String.format("Camera %s has torch status changed to 0x%x",
                     cameraId, status));
         }
         @Override
         public void onPhysicalCameraStatusChanged(int status, String cameraId,
-                String physicalCameraId) throws RemoteException {
+                String physicalCameraId, int deviceId) throws RemoteException {
             Log.v(TAG, String.format("Camera %s : %s has status changed to 0x%x",
                     cameraId, physicalCameraId, status));
         }
@@ -300,16 +297,16 @@
             Log.v(TAG, "Camera access permission change");
         }
         @Override
-        public void onCameraOpened(String cameraId, String clientPackageName) {
+        public void onCameraOpened(String cameraId, String clientPackageName, int deviceId) {
             Log.v(TAG, String.format("Camera %s is opened by client package %s",
                     cameraId, clientPackageName));
         }
         @Override
-        public void onCameraClosed(String cameraId) {
+        public void onCameraClosed(String cameraId, int deviceId) {
             Log.v(TAG, String.format("Camera %s is closed", cameraId));
         }
         @Override
-        public void onTorchStrengthLevelChanged(String cameraId, int torchStrength) {
+        public void onTorchStrengthLevelChanged(String cameraId, int torchStrength, int deviceId) {
             Log.v(TAG, String.format("Camera " + cameraId + " torch strength level changed to "
                     + torchStrength ));
         }
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java
index eaa5a85..dc8647f 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/integration/CameraDeviceBinderTest.java
@@ -16,6 +16,8 @@
 
 package com.android.mediaframeworktest.integration;
 
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
+import static android.content.Context.DEVICE_ID_DEFAULT;
 import static android.hardware.camera2.CameraDevice.TEMPLATE_PREVIEW;
 
 import static org.mockito.Mockito.any;
@@ -246,7 +248,7 @@
         mCameraUser = mUtils.getCameraService().connectDevice(mMockCb, mCameraId,
                 clientPackageName, clientAttributionTag, ICameraService.USE_CALLING_UID,
                 /*oomScoreOffset*/0, getContext().getApplicationInfo().targetSdkVersion,
-                /*overrideToPortrait*/false);
+                /*overrideToPortrait*/false, DEVICE_ID_DEFAULT, DEVICE_POLICY_DEFAULT);
         assertNotNull(String.format("Camera %s was null", mCameraId), mCameraUser);
         mHandlerThread = new HandlerThread(TAG);
         mHandlerThread.start();
@@ -272,7 +274,6 @@
 
         metadata = mCameraUser.createDefaultRequest(TEMPLATE_PREVIEW);
         assertFalse(metadata.isEmpty());
-
     }
 
     @SmallTest
@@ -306,7 +307,6 @@
 
     @SmallTest
     public void testCreateStreamTwo() throws Exception {
-
         // Create first stream
         int streamId = mCameraUser.createStream(mOutputConfiguration);
         assertEquals(0, streamId);
@@ -335,7 +335,6 @@
 
     @SmallTest
     public void testSubmitBadRequest() throws Exception {
-
         CaptureRequest.Builder builder = createDefaultBuilder(/* needStream */false);
         CaptureRequest request1 = builder.build();
         try {
@@ -361,7 +360,6 @@
 
     @SmallTest
     public void testSubmitGoodRequest() throws Exception {
-
         CaptureRequest.Builder builder = createDefaultBuilder(/* needStream */true);
         CaptureRequest request = builder.build();
 
@@ -370,12 +368,10 @@
         SubmitInfo requestInfo2 = submitCameraRequest(request, /* streaming */false);
         assertNotSame("Request IDs should be unique for multiple requests",
                 requestInfo1.getRequestId(), requestInfo2.getRequestId());
-
     }
 
     @SmallTest
     public void testSubmitStreamingRequest() throws Exception {
-
         CaptureRequest.Builder builder = createDefaultBuilder(/* needStream */true);
 
         CaptureRequest request = builder.build();
@@ -419,7 +415,8 @@
     @SmallTest
     public void testCameraCharacteristics() throws RemoteException {
         CameraMetadataNative info = mUtils.getCameraService().getCameraCharacteristics(mCameraId,
-                getContext().getApplicationInfo().targetSdkVersion, /*overrideToPortrait*/false);
+                getContext().getApplicationInfo().targetSdkVersion, /*overrideToPortrait*/false,
+                DEVICE_ID_DEFAULT, DEVICE_POLICY_DEFAULT);
 
         assertFalse(info.isEmpty());
         assertNotNull(info.get(CameraCharacteristics.SCALER_AVAILABLE_FORMATS));
@@ -512,7 +509,6 @@
 
         // And wait for more idle
         verify(mMockCb, timeout(WAIT_FOR_IDLE_TIMEOUT_MS).times(2)).onDeviceIdle();
-
     }
 
     @SmallTest
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index 8a5dfef..69e4bd7 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1030,13 +1030,6 @@
     <!-- Settings item title to select whether to disable cache for transcoding. [CHAR LIMIT=85] -->
     <string name="transcode_disable_cache">Disable transcoding cache</string>
 
-    <!-- Developer settings title: widevine settings screen. [CHAR LIMIT=50] -->
-    <string name="widevine_settings_title">Widevine settings</string>
-     <!-- Developer settings title: select whether to enable Force L3 fallback. [CHAR LIMIT=50] -->
-    <string name="force_l3_fallback_title">Force L3 fallback</string>
-     <!-- Developer settings summary: select whether to enable Force L3 fallback.[CHAR LIMIT=NONE] -->
-    <string name="force_l3_fallback_summary">Select to force L3 fallback</string>
-
     <!-- Services settings screen, setting option name for the user to go to the screen to view running services -->
     <string name="runningservices_settings_title">Running services</string>
     <!-- Services settings screen, setting option summary for the user to go to the screen to view running services  -->
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
index 2889ce2..56118da 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
@@ -51,9 +51,11 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.text.format.Formatter;
+import android.util.ArrayMap;
 import android.util.Log;
 import android.util.SparseArray;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 import androidx.lifecycle.Lifecycle;
 import androidx.lifecycle.LifecycleObserver;
@@ -990,11 +992,22 @@
                 apps = new ArrayList<>(mAppEntries);
             }
 
+            ArrayMap<UserHandle, Boolean> profileHideInQuietModeStatus = new ArrayMap<>();
             ArrayList<AppEntry> filteredApps = new ArrayList<>();
             if (DEBUG) {
                 Log.i(TAG, "Rebuilding...");
             }
             for (AppEntry entry : apps) {
+                if (android.multiuser.Flags.enablePrivateSpaceFeatures()
+                        && android.multiuser.Flags.handleInterleavedSettingsForPrivateSpace()) {
+                    UserHandle userHandle = UserHandle.of(UserHandle.getUserId(entry.info.uid));
+                    if (!profileHideInQuietModeStatus.containsKey(userHandle)) {
+                        profileHideInQuietModeStatus.put(
+                                userHandle, isHideInQuietEnabledForProfile(mUm, userHandle));
+                    }
+                    filter.refreshAppEntryOnRebuild(
+                            entry, profileHideInQuietModeStatus.get(userHandle));
+                }
                 if (entry != null && (filter == null || filter.filterApp(entry))) {
                     synchronized (mEntriesMap) {
                         if (DEBUG_LOCKING) {
@@ -1648,6 +1661,11 @@
          */
         public boolean isHomeApp;
 
+        /**
+         * Whether the app should be hidden for user when quiet mode is enabled.
+         */
+        public boolean hideInQuietMode;
+
         public String getNormalizedLabel() {
             if (normalizedLabel != null) {
                 return normalizedLabel;
@@ -1691,6 +1709,7 @@
             UserInfo userInfo = um.getUserInfo(UserHandle.getUserId(info.uid));
             mProfileType = userInfo.userType;
             this.showInPersonalTab = shouldShowInPersonalTab(um, info.uid);
+            hideInQuietMode = shouldHideInQuietMode(um, info.uid);
         }
 
         public boolean isClonedProfile() {
@@ -1800,12 +1819,32 @@
                 this.labelDescription = this.label;
             }
         }
+
+        /**
+         * Returns true if profile is in quiet mode and the profile should not be visible when the
+         * quiet mode is enabled, false otherwise.
+         */
+        private boolean shouldHideInQuietMode(@NonNull UserManager userManager, int uid) {
+            if (android.multiuser.Flags.enablePrivateSpaceFeatures()
+                    && android.multiuser.Flags.handleInterleavedSettingsForPrivateSpace()) {
+                UserHandle userHandle = UserHandle.of(UserHandle.getUserId(uid));
+                return isHideInQuietEnabledForProfile(userManager, userHandle);
+            }
+            return false;
+        }
     }
 
     private static boolean hasFlag(int flags, int flag) {
         return (flags & flag) != 0;
     }
 
+    private static boolean isHideInQuietEnabledForProfile(
+            UserManager userManager, UserHandle userHandle) {
+        return userManager.isQuietModeEnabled(userHandle)
+                && userManager.getUserProperties(userHandle).getShowInQuietMode()
+                == UserProperties.SHOW_IN_QUIET_MODE_HIDDEN;
+    }
+
     /**
      * Compare by label, then package name, then uid.
      */
@@ -1868,6 +1907,15 @@
         }
 
         boolean filterApp(AppEntry info);
+
+        /**
+         * Updates AppEntry based on whether quiet mode is enabled and should not be
+         * visible for the corresponding profile.
+         */
+        default void refreshAppEntryOnRebuild(
+                @NonNull AppEntry appEntry,
+                boolean hideInQuietMode) {
+        }
     }
 
     public static final AppFilter FILTER_PERSONAL = new AppFilter() {
@@ -2010,6 +2058,25 @@
         }
     };
 
+    public static final AppFilter FILTER_ENABLED_NOT_QUIET = new AppFilter() {
+        @Override
+        public void init() {
+        }
+
+        @Override
+        public boolean filterApp(@NonNull AppEntry entry) {
+            return entry.info.enabled && !AppUtils.isInstant(entry.info)
+                    && !entry.hideInQuietMode;
+        }
+
+        @Override
+        public void refreshAppEntryOnRebuild(
+                @NonNull AppEntry appEntry,
+                boolean hideInQuietMode) {
+            appEntry.hideInQuietMode = hideInQuietMode;
+        }
+    };
+
     public static final AppFilter FILTER_EVERYTHING = new AppFilter() {
         @Override
         public void init() {
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java
index fe83ffb..b974888 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java
@@ -267,6 +267,16 @@
     }
 
     @Test
+    public void testEnabledFilterNotQuietRejectsInstantApp() {
+        mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE,
+                android.multiuser.Flags.FLAG_HANDLE_INTERLEAVED_SETTINGS_FOR_PRIVATE_SPACE);
+        mEntry.info.enabled = true;
+        assertThat(ApplicationsState.FILTER_ENABLED_NOT_QUIET.filterApp(mEntry)).isTrue();
+        when(mEntry.info.isInstantApp()).thenReturn(true);
+        assertThat(ApplicationsState.FILTER_ENABLED_NOT_QUIET.filterApp(mEntry)).isFalse();
+    }
+
+    @Test
     public void testFilterWithDomainUrls() {
         mEntry.info.privateFlags |= ApplicationInfo.PRIVATE_FLAG_HAS_DOMAIN_URLS;
         // should included updated system apps
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index f911269..dc7850f 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -289,7 +289,7 @@
         "WindowManager-Shell",
         "LowLightDreamLib",
         "motion_tool_lib",
-        "androidx.core_core-animation-testing-nodeps",
+        "androidx.core_core-animation-testing",
         "androidx.compose.ui_ui",
         "flag-junit",
         "ravenwood-junit",
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java
index f70ad9e..5f3d1ea 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java
+++ b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java
@@ -62,6 +62,7 @@
 import org.junit.Assume;
 import org.junit.Before;
 import org.junit.BeforeClass;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -452,6 +453,8 @@
     }
 
     @Test
+    @Ignore("Test failure in pre/postsubmit cannot be replicated on local devices. "
+            + "Coverage is low-impact.")
     public void testOnScreenLock_cannotOpenMenu() throws Throwable {
         closeScreen();
         wakeUpScreen();
diff --git a/packages/SystemUI/animation/Android.bp b/packages/SystemUI/animation/Android.bp
index 2268d16..a6d750f 100644
--- a/packages/SystemUI/animation/Android.bp
+++ b/packages/SystemUI/animation/Android.bp
@@ -41,7 +41,7 @@
     ],
 
     static_libs: [
-        "androidx.core_core-animation-nodeps",
+        "androidx.core_core-animation",
         "androidx.core_core-ktx",
         "androidx.annotation_annotation",
         "com_android_systemui_flags_lib",
@@ -64,7 +64,7 @@
     ],
 
     static_libs: [
-        "androidx.core_core-animation-nodeps",
+        "androidx.core_core-animation",
         "androidx.core_core-ktx",
         "androidx.annotation_annotation",
     ],
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpecForSysUi.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpecForSysUi.kt
index c6b7073..b8d4469 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpecForSysUi.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpecForSysUi.kt
@@ -71,5 +71,5 @@
         displayMetrics = displayMetrics,
         maxMarginXdp = 8f,
         maxMarginYdp = 8f,
-        minScale = 0.8f,
+        minScale = 0.9f,
     )
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index 503779c..ed80277 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -482,7 +482,7 @@
     Card(
         modifier = Modifier.height(Dimensions.GridHeight).padding(contentPadding),
         colors = CardDefaults.cardColors(containerColor = Color.Transparent),
-        border = BorderStroke(3.dp, colors.primaryFixedDim),
+        border = BorderStroke(3.dp, colors.secondary),
         shape = RoundedCornerShape(size = 80.dp)
     ) {
         Column(
@@ -495,7 +495,7 @@
                 text = stringResource(R.string.title_for_empty_state_cta),
                 style = MaterialTheme.typography.displaySmall,
                 textAlign = TextAlign.Center,
-                color = colors.secondaryFixed,
+                color = colors.secondary,
             )
             Row(
                 modifier = Modifier.fillMaxWidth(),
@@ -505,8 +505,8 @@
                     modifier = Modifier.height(56.dp),
                     colors =
                         ButtonDefaults.buttonColors(
-                            containerColor = colors.primaryFixed,
-                            contentColor = colors.onPrimaryFixed,
+                            containerColor = colors.primary,
+                            contentColor = colors.onPrimary,
                         ),
                     onClick = {
                         viewModel.onOpenWidgetEditor(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
index c5c48e6..fcd7760 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
@@ -114,6 +114,11 @@
     statusBarIconController: StatusBarIconController,
     modifier: Modifier = Modifier,
 ) {
+    val isDisabled by viewModel.isDisabled.collectAsState()
+    if (isDisabled) {
+        return
+    }
+
     val formatProgress =
         animateSceneFloatAsState(0f, ShadeHeader.Keys.transitionProgress)
             .unsafeCompositionState(initialValue = 0f)
@@ -251,6 +256,11 @@
     statusBarIconController: StatusBarIconController,
     modifier: Modifier = Modifier,
 ) {
+    val isDisabled by viewModel.isDisabled.collectAsState()
+    if (isDisabled) {
+        return
+    }
+
     val formatProgress =
         animateSceneFloatAsState(1f, ShadeHeader.Keys.transitionProgress)
             .unsafeCompositionState(initialValue = 1f)
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index 25c649a..e086cda 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -27,7 +27,9 @@
 import android.text.format.DateFormat
 import android.util.AttributeSet
 import android.util.MathUtils.constrainedMap
+import android.util.TypedValue
 import android.view.View
+import android.view.View.MeasureSpec.EXACTLY
 import android.widget.TextView
 import com.android.app.animation.Interpolators
 import com.android.internal.annotations.VisibleForTesting
@@ -42,6 +44,7 @@
 import java.util.Calendar
 import java.util.Locale
 import java.util.TimeZone
+import kotlin.math.min
 
 /**
  * Displays the time with the hour positioned above the minutes. (ie: 09 above 30 is 9:30)
@@ -85,6 +88,8 @@
     private var textAnimator: TextAnimator? = null
     private var onTextAnimatorInitialized: Runnable? = null
 
+    // last text size which is not constrained by view height
+    private var lastUnconstrainedTextSize: Float = Float.MAX_VALUE
     @VisibleForTesting var textAnimatorFactory: (Layout, () -> Unit) -> TextAnimator =
         { layout, invalidateCb ->
             TextAnimator(layout, NUM_CLOCK_FONT_ANIMATION_STEPS, invalidateCb) }
@@ -188,6 +193,11 @@
     @SuppressLint("DrawAllocation")
     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
         logger.d("onMeasure")
+        if (migratedClocks && !isSingleLineInternal &&
+                MeasureSpec.getMode(heightMeasureSpec) == EXACTLY) {
+            setTextSize(TypedValue.COMPLEX_UNIT_PX,
+                    min(lastUnconstrainedTextSize, MeasureSpec.getSize(heightMeasureSpec) / 2F))
+        }
         super.onMeasure(widthMeasureSpec, heightMeasureSpec)
         val animator = textAnimator
         if (animator == null) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
index 36458ed..15c9cf7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
@@ -22,6 +22,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
 import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING
 import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
@@ -37,6 +38,7 @@
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import junit.framework.Assert.assertEquals
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
@@ -242,7 +244,8 @@
     @Test
     fun transitionValue() =
         testScope.runTest {
-            val startedSteps by collectValues(underTest.transitionValue(state = DOZING))
+            resetTransitionValueReplayCache(setOf(AOD, DOZING, LOCKSCREEN))
+            val transitionValues by collectValues(underTest.transitionValue(state = DOZING))
 
             val toSteps =
                 listOf(
@@ -266,12 +269,14 @@
                 runCurrent()
             }
 
-            assertThat(startedSteps).isEqualTo(listOf(0f, 0.5f, 1f, 1f, 0.5f, 0f))
+            assertThat(transitionValues).isEqualTo(listOf(0f, 0.5f, 1f, 1f, 0.5f, 0f))
         }
 
     @Test
     fun transitionValue_canceled_toAnotherState() =
         testScope.runTest {
+            resetTransitionValueReplayCache(setOf(AOD, GONE, LOCKSCREEN))
+
             val transitionValuesGone by collectValues(underTest.transitionValue(state = GONE))
             val transitionValuesAod by collectValues(underTest.transitionValue(state = AOD))
             val transitionValuesLs by collectValues(underTest.transitionValue(state = LOCKSCREEN))
@@ -297,6 +302,8 @@
     @Test
     fun transitionValue_canceled_backToOriginalState() =
         testScope.runTest {
+            resetTransitionValueReplayCache(setOf(AOD, GONE))
+
             val transitionValuesGone by collectValues(underTest.transitionValue(state = GONE))
             val transitionValuesAod by collectValues(underTest.transitionValue(state = AOD))
 
@@ -1395,4 +1402,8 @@
             testScope.runCurrent()
         }
     }
+
+    private fun resetTransitionValueReplayCache(states: Set<KeyguardState>) {
+        states.forEach { (underTest.transitionValue(it) as MutableSharedFlow).resetReplayCache() }
+    }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
index 6108904..ef38567 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
@@ -34,6 +34,7 @@
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.shade.domain.interactor.privacyChipInteractor
 import com.android.systemui.shade.domain.interactor.shadeHeaderClockInteractor
+import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel
 import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
@@ -97,6 +98,7 @@
             ShadeHeaderViewModel(
                 applicationScope = testScope.backgroundScope,
                 context = context,
+                shadeInteractor = kosmos.shadeInteractor,
                 mobileIconsInteractor = mobileIconsInteractor,
                 mobileIconsViewModel = mobileIconsViewModel,
                 privacyChipInteractor = kosmos.privacyChipInteractor,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index af9abcd..c80835d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -86,7 +86,6 @@
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
 import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
 import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
-import com.android.systemui.statusbar.policy.data.repository.fakeDeviceProvisioningRepository
 import com.android.systemui.statusbar.policy.domain.interactor.deviceProvisioningInteractor
 import com.android.systemui.telephony.data.repository.fakeTelephonyRepository
 import com.android.systemui.testKosmos
@@ -242,6 +241,7 @@
             ShadeHeaderViewModel(
                 applicationScope = testScope.backgroundScope,
                 context = context,
+                shadeInteractor = kosmos.shadeInteractor,
                 mobileIconsInteractor = mobileIconsInteractor,
                 mobileIconsViewModel = mobileIconsViewModel,
                 privacyChipInteractor = kosmos.privacyChipInteractor,
@@ -549,21 +549,6 @@
             assertCurrentScene(Scenes.Lockscreen)
         }
 
-    @Test
-    fun deviceProvisioningAndFactoryResetProtection() =
-        testScope.runTest {
-            val isVisible by collectLastValue(sceneContainerViewModel.isVisible)
-            kosmos.fakeDeviceProvisioningRepository.setDeviceProvisioned(false)
-            kosmos.fakeDeviceProvisioningRepository.setFactoryResetProtectionActive(true)
-            assertThat(isVisible).isFalse()
-
-            kosmos.fakeDeviceProvisioningRepository.setFactoryResetProtectionActive(false)
-            assertThat(isVisible).isFalse()
-
-            kosmos.fakeDeviceProvisioningRepository.setDeviceProvisioned(true)
-            assertThat(isVisible).isTrue()
-        }
-
     /**
      * Asserts that the current scene in the view-model matches what's expected.
      *
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index f018cc1..594543b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -187,27 +187,20 @@
         }
 
     @Test
-    fun hydrateVisibility_basedOnDeviceProvisioningAndFactoryResetProtection() =
+    fun hydrateVisibility_basedOnDeviceProvisioning() =
         testScope.runTest {
             val isVisible by collectLastValue(sceneInteractor.isVisible)
             prepareState(
                 isDeviceUnlocked = true,
                 initialSceneKey = Scenes.Lockscreen,
                 isDeviceProvisioned = false,
-                isFrpActive = true,
             )
 
             underTest.start()
             assertThat(isVisible).isFalse()
 
-            kosmos.fakeDeviceProvisioningRepository.setFactoryResetProtectionActive(false)
-            assertThat(isVisible).isFalse()
-
             kosmos.fakeDeviceProvisioningRepository.setDeviceProvisioned(true)
             assertThat(isVisible).isTrue()
-
-            kosmos.fakeDeviceProvisioningRepository.setFactoryResetProtectionActive(true)
-            assertThat(isVisible).isFalse()
         }
 
     @Test
@@ -1030,7 +1023,6 @@
         isLockscreenEnabled: Boolean = true,
         startsAwake: Boolean = true,
         isDeviceProvisioned: Boolean = true,
-        isFrpActive: Boolean = false,
     ): MutableStateFlow<ObservableTransitionState> {
         if (authenticationMethod?.isSecure == true) {
             assert(isLockscreenEnabled) {
@@ -1068,7 +1060,6 @@
         }
 
         kosmos.fakeDeviceProvisioningRepository.setDeviceProvisioned(isDeviceProvisioned)
-        kosmos.fakeDeviceProvisioningRepository.setFactoryResetProtectionActive(isFrpActive)
 
         runCurrent()
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
index 4e82feb..757a6c9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImplTest.kt
@@ -326,7 +326,7 @@
         }
 
     @Test
-    fun shadeExpansionWhenNotInSplitShadeAndQsExpanded() =
+    fun shadeExpansionWhenNotInSplitShadeAndQsPartiallyExpanded() =
         testScope.runTest {
             val actual by collectLastValue(underTest.shadeExpansion)
 
@@ -338,6 +338,22 @@
             runCurrent()
 
             // THEN shade expansion is zero
+            assertThat(actual).isEqualTo(.5f)
+        }
+
+    @Test
+    fun shadeExpansionWhenNotInSplitShadeAndQsFullyExpanded() =
+        testScope.runTest {
+            val actual by collectLastValue(underTest.shadeExpansion)
+
+            // WHEN split shade is not enabled and QS is expanded
+            keyguardRepository.setStatusBarState(StatusBarState.SHADE)
+            overrideResource(R.bool.config_use_split_notification_shade, false)
+            shadeRepository.setQsExpansion(1f)
+            shadeRepository.setLegacyShadeExpansion(1f)
+            runCurrent()
+
+            // THEN shade expansion is zero
             assertThat(actual).isEqualTo(0f)
         }
 
@@ -603,20 +619,6 @@
         }
 
     @Test
-    fun isShadeTouchable_isFalse_whenFrpIsActive() =
-        testScope.runTest {
-            deviceProvisioningRepository.setFactoryResetProtectionActive(true)
-            keyguardTransitionRepository.sendTransitionStep(
-                TransitionStep(
-                    transitionState = TransitionState.STARTED,
-                )
-            )
-            val isShadeTouchable by collectLastValue(underTest.isShadeTouchable)
-            runCurrent()
-            assertThat(isShadeTouchable).isFalse()
-        }
-
-    @Test
     fun isShadeTouchable_isFalse_whenDeviceAsleepAndNotPulsing() =
         testScope.runTest {
             powerRepository.updateWakefulness(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImplTest.kt
index 682c4ef..0ae95e7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImplTest.kt
@@ -95,14 +95,14 @@
         }
 
     @Test
-    fun shadeExpansionWhenNotInSplitShadeAndQsExpanded() =
+    fun shadeExpansionWhenNotInSplitShadeAndQsFullyExpanded() =
         testScope.runTest {
             val actual by collectLastValue(underTest.shadeExpansion)
 
             // WHEN split shade is not enabled and QS is expanded
             keyguardRepository.setStatusBarState(StatusBarState.SHADE)
             overrideResource(R.bool.config_use_split_notification_shade, false)
-            shadeRepository.setQsExpansion(.5f)
+            shadeRepository.setQsExpansion(1f)
             shadeRepository.setLegacyShadeExpansion(1f)
             runCurrent()
 
@@ -111,16 +111,34 @@
         }
 
     @Test
+    fun shadeExpansionWhenNotInSplitShadeAndQsPartlyExpanded() =
+        testScope.runTest {
+            val actual by collectLastValue(underTest.shadeExpansion)
+
+            // WHEN split shade is not enabled and QS partly expanded
+            keyguardRepository.setStatusBarState(StatusBarState.SHADE)
+            overrideResource(R.bool.config_use_split_notification_shade, false)
+            shadeRepository.setQsExpansion(.4f)
+            shadeRepository.setLegacyShadeExpansion(1f)
+            runCurrent()
+
+            // THEN shade expansion is the difference
+            assertThat(actual).isEqualTo(.6f)
+        }
+
+    @Test
     fun shadeExpansionWhenNotInSplitShadeAndQsCollapsed() =
         testScope.runTest {
             val actual by collectLastValue(underTest.shadeExpansion)
 
-            // WHEN split shade is not enabled and QS is expanded
+            // WHEN split shade is not enabled and QS collapsed
             keyguardRepository.setStatusBarState(StatusBarState.SHADE)
+            overrideResource(R.bool.config_use_split_notification_shade, false)
             shadeRepository.setQsExpansion(0f)
             shadeRepository.setLegacyShadeExpansion(.6f)
+            runCurrent()
 
-            // THEN shade expansion is zero
+            // THEN shade expansion is the legacy one
             assertThat(actual).isEqualTo(.6f)
         }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
index 062741d..4c573d3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
@@ -13,6 +13,7 @@
 import com.android.systemui.plugins.activityStarter
 import com.android.systemui.shade.domain.interactor.privacyChipInteractor
 import com.android.systemui.shade.domain.interactor.shadeHeaderClockInteractor
+import com.android.systemui.shade.domain.interactor.shadeInteractor
 import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
 import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
 import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
@@ -69,6 +70,7 @@
             ShadeHeaderViewModel(
                 applicationScope = testScope.backgroundScope,
                 context = context,
+                shadeInteractor = kosmos.shadeInteractor,
                 mobileIconsInteractor = mobileIconsInteractor,
                 mobileIconsViewModel = mobileIconsViewModel,
                 privacyChipInteractor = kosmos.privacyChipInteractor,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
index d1c4ec3..f90a3b1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
@@ -110,6 +110,7 @@
             ShadeHeaderViewModel(
                 applicationScope = testScope.backgroundScope,
                 context = context,
+                shadeInteractor = kosmos.shadeInteractor,
                 mobileIconsInteractor = mobileIconsInteractor,
                 mobileIconsViewModel = mobileIconsViewModel,
                 privacyChipInteractor = kosmos.privacyChipInteractor,
@@ -280,17 +281,17 @@
 
     @Test
     fun upTransitionSceneKey_customizing_noTransition() =
-            testScope.runTest {
-                val destinationScenes by collectLastValue(underTest.destinationScenes)
+        testScope.runTest {
+            val destinationScenes by collectLastValue(underTest.destinationScenes)
 
-                qsSceneAdapter.setCustomizing(true)
-                assertThat(
-                        destinationScenes!!
-                                .keys
-                                .filterIsInstance<Swipe>()
-                                .filter { it.direction == SwipeDirection.Up }
-                ).isEmpty()
-            }
+            qsSceneAdapter.setCustomizing(true)
+            assertThat(
+                    destinationScenes!!.keys.filterIsInstance<Swipe>().filter {
+                        it.direction == SwipeDirection.Up
+                    }
+                )
+                .isEmpty()
+        }
 
     @Test
     fun shadeMode() =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
index 2a2b2f1..7ac549a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModelTest.kt
@@ -483,6 +483,21 @@
         }
 
     @Test
+    fun shouldHideFooterView_falseWhenQSPartiallyOpen() =
+        testScope.runTest {
+            val shouldHide by collectLastValue(underTest.shouldHideFooterView)
+
+            // WHEN QS partially open
+            fakeKeyguardRepository.setStatusBarState(StatusBarState.SHADE)
+            fakeShadeRepository.setQsExpansion(0.5f)
+            fakeShadeRepository.setLegacyShadeExpansion(0.5f)
+            runCurrent()
+
+            // THEN footer is hidden
+            assertThat(shouldHide).isFalse()
+        }
+
+    @Test
     @EnableFlags(NotificationsHeadsUpRefactor.FLAG_NAME)
     fun pinnedHeadsUpRows_filtersForPinnedItems() =
         testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index cc7ebe9..509a82d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -585,6 +585,12 @@
             notificationCount = 25
             sharedNotificationContainerInteractor.notificationStackChanged()
             assertThat(maxNotifications).isEqualTo(25)
+
+            // Also ensure another collection starts with the same value. As an example, folding
+            // then unfolding will restart the coroutine and it must get the last value immediately.
+            val newMaxNotifications by
+                collectLastValue(underTest.getMaxNotifications(calculateSpace))
+            assertThat(newMaxNotifications).isEqualTo(25)
         }
 
     @Test
@@ -760,6 +766,41 @@
         }
 
     @Test
+    fun alphaDoesNotUpdateWhileOcclusionTransitionIsRunning() =
+        testScope.runTest {
+            val viewState = ViewStateAccessor()
+            val alpha by collectLastValue(underTest.keyguardAlpha(viewState))
+
+            showLockscreen()
+            // OCCLUDED transition gets to 90% complete
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.OCCLUDED,
+                    transitionState = TransitionState.STARTED,
+                    value = 0f,
+                )
+            )
+            runCurrent()
+            keyguardTransitionRepository.sendTransitionStep(
+                TransitionStep(
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.OCCLUDED,
+                    transitionState = TransitionState.RUNNING,
+                    value = 0.9f,
+                )
+            )
+            runCurrent()
+
+            // At this point, alpha should be zero
+            assertThat(alpha).isEqualTo(0f)
+
+            // An attempt to override by the shade should be ignored
+            shadeRepository.setQsExpansion(0.5f)
+            assertThat(alpha).isEqualTo(0f)
+        }
+
+    @Test
     fun alphaWhenGoneIsSetToOne() =
         testScope.runTest {
             val viewState = ViewStateAccessor()
diff --git a/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceDataPlugin.java b/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceDataPlugin.java
index c99cb39..83658d3 100644
--- a/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceDataPlugin.java
+++ b/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceDataPlugin.java
@@ -129,6 +129,11 @@
         void setDozeAmount(float amount);
 
         /**
+         * Set if the screen is on.
+         */
+        default void setScreenOn(boolean screenOn) {}
+
+        /**
          * Set if dozing is true or false
          */
         default void setDozing(boolean dozing) {}
diff --git a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
index 22bd207..27b2b92 100644
--- a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
@@ -18,6 +18,7 @@
 
 import static androidx.dynamicanimation.animation.DynamicAnimation.TRANSLATION_X;
 import static androidx.dynamicanimation.animation.FloatPropertyCompat.createFloatPropertyCompat;
+
 import static com.android.systemui.classifier.Classifier.NOTIFICATION_DISMISS;
 import static com.android.systemui.flags.Flags.SWIPE_UNCLEARED_TRANSIENT_VIEW_FIX;
 import static com.android.systemui.statusbar.notification.NotificationUtils.logKey;
@@ -54,8 +55,8 @@
 import com.android.systemui.res.R;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.wm.shell.animation.FlingAnimationUtils;
-import com.android.wm.shell.animation.PhysicsAnimator;
-import com.android.wm.shell.animation.PhysicsAnimator.SpringConfig;
+import com.android.wm.shell.shared.animation.PhysicsAnimator;
+import com.android.wm.shell.shared.animation.PhysicsAnimator.SpringConfig;
 
 import java.io.PrintWriter;
 import java.util.function.Consumer;
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractView.kt b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractView.kt
index a90d4b2..c1b3962 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractView.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractView.kt
@@ -39,9 +39,9 @@
 import androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_LOW_BOUNCY
 import androidx.dynamicanimation.animation.SpringForce.STIFFNESS_LOW
 import com.android.wm.shell.R
-import com.android.wm.shell.animation.PhysicsAnimator
 import com.android.wm.shell.common.bubbles.DismissCircleView
 import com.android.wm.shell.common.bubbles.DismissView
+import com.android.wm.shell.shared.animation.PhysicsAnimator
 
 /**
  * View that handles interactions between DismissCircleView and BubbleStackView.
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 19a44cc..d33e7ff 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -394,10 +394,6 @@
     // TODO(b/251205791): Tracking Bug
     @JvmField val SCREENSHOT_APP_CLIPS = releasedFlag("screenshot_app_clips")
 
-    /** TODO(b/295143676): Tracking bug. When enable, captures a screenshot for each display. */
-    @JvmField
-    val MULTI_DISPLAY_SCREENSHOT = releasedFlag("multi_display_screenshot")
-
     // 1400 - columbus
     // TODO(b/254512756): Tracking Bug
     val QUICK_TAP_IN_PCC = releasedFlag("quick_tap_in_pcc")
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
index f10327e..a0ab869 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
@@ -218,5 +218,6 @@
         private val DEFAULT_DURATION = 500.milliseconds
         val TO_LOCKSCREEN_DURATION = 933.milliseconds
         val TO_AOD_DURATION = DEFAULT_DURATION
+        val TO_DOZING_DURATION = DEFAULT_DURATION
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index 0cd7d18..804b630 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -85,9 +85,11 @@
     private fun getTransitionValueFlow(state: KeyguardState): MutableSharedFlow<Float> {
         return transitionValueCache.getOrPut(state) {
             MutableSharedFlow<Float>(
-                extraBufferCapacity = 2,
-                onBufferOverflow = BufferOverflow.DROP_OLDEST
-            )
+                    replay = 1,
+                    extraBufferCapacity = 2,
+                    onBufferOverflow = BufferOverflow.DROP_OLDEST
+                )
+                .also { it.tryEmit(0f) }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
index 881467f..4a09939 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
@@ -25,6 +25,7 @@
 import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
 import androidx.constraintlayout.widget.ConstraintSet.END
 import androidx.constraintlayout.widget.ConstraintSet.GONE
+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
@@ -160,7 +161,7 @@
         constraints.apply {
             connect(R.id.lockscreen_clock_view_large, START, PARENT_ID, START)
             connect(R.id.lockscreen_clock_view_large, END, guideline, END)
-            connect(R.id.lockscreen_clock_view_large, BOTTOM, R.id.lock_icon_view, TOP)
+            connect(R.id.lockscreen_clock_view_large, BOTTOM, R.id.device_entry_icon_view, TOP)
             var largeClockTopMargin =
                 context.resources.getDimensionPixelSize(R.dimen.status_bar_height) +
                     context.resources.getDimensionPixelSize(
@@ -172,7 +173,7 @@
 
             connect(R.id.lockscreen_clock_view_large, TOP, PARENT_ID, TOP, largeClockTopMargin)
             constrainWidth(R.id.lockscreen_clock_view_large, WRAP_CONTENT)
-            constrainHeight(R.id.lockscreen_clock_view_large, WRAP_CONTENT)
+            constrainHeight(R.id.lockscreen_clock_view_large, MATCH_CONSTRAINT)
             constrainWidth(R.id.lockscreen_clock_view, WRAP_CONTENT)
             constrainHeight(
                 R.id.lockscreen_clock_view,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index 5337ca3..e8313a9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -102,6 +102,7 @@
     private val lockscreenToPrimaryBouncerTransitionViewModel:
         LockscreenToPrimaryBouncerTransitionViewModel,
     private val occludedToAodTransitionViewModel: OccludedToAodTransitionViewModel,
+    private val occludedToDozingTransitionViewModel: OccludedToDozingTransitionViewModel,
     private val occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel,
     private val primaryBouncerToAodTransitionViewModel: PrimaryBouncerToAodTransitionViewModel,
     private val primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel,
@@ -228,6 +229,7 @@
                         lockscreenToOccludedTransitionViewModel.lockscreenAlpha,
                         lockscreenToPrimaryBouncerTransitionViewModel.lockscreenAlpha,
                         occludedToAodTransitionViewModel.lockscreenAlpha,
+                        occludedToDozingTransitionViewModel.lockscreenAlpha,
                         occludedToLockscreenTransitionViewModel.lockscreenAlpha,
                         primaryBouncerToAodTransitionViewModel.lockscreenAlpha,
                         primaryBouncerToGoneTransitionViewModel.lockscreenAlpha,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToDozingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToDozingTransitionViewModel.kt
new file mode 100644
index 0000000..91554e3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToDozingTransitionViewModel.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2024 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.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import javax.inject.Inject
+import kotlin.time.Duration.Companion.milliseconds
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Breaks down OCCLUDED->DOZING transition into discrete steps for corresponding views to consume.
+ */
+@ExperimentalCoroutinesApi
+@SysUISingleton
+class OccludedToDozingTransitionViewModel
+@Inject
+constructor(
+    animationFlow: KeyguardTransitionAnimationFlow,
+) {
+    private val transitionAnimation =
+        animationFlow.setup(
+            duration = FromOccludedTransitionInteractor.TO_DOZING_DURATION,
+            from = KeyguardState.OCCLUDED,
+            to = KeyguardState.DOZING,
+        )
+
+    /** Lockscreen views alpha */
+    val lockscreenAlpha: Flow<Float> =
+        transitionAnimation.sharedFlow(
+            startTime = 233.milliseconds,
+            duration = 250.milliseconds,
+            onStep = { it },
+        )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandler.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandler.kt
index c033e46..b531ecf 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaCarouselScrollHandler.kt
@@ -37,7 +37,7 @@
 import com.android.systemui.qs.PageIndicator
 import com.android.systemui.res.R
 import com.android.systemui.util.concurrency.DelayableExecutor
-import com.android.wm.shell.animation.PhysicsAnimator
+import com.android.wm.shell.shared.animation.PhysicsAnimator
 
 private const val FLING_SLOP = 1000000
 private const val DISMISS_DELAY = 100L
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaScrollView.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaScrollView.kt
index b625908..b08ee16 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaScrollView.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/view/MediaScrollView.kt
@@ -24,7 +24,7 @@
 import android.view.ViewGroup
 import android.widget.HorizontalScrollView
 import com.android.systemui.Gefingerpoken
-import com.android.wm.shell.animation.physicsAnimator
+import com.android.wm.shell.shared.animation.physicsAnimator
 
 /**
  * A ScrollView used in Media that doesn't limit itself to the childs bounds. This is useful when
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index 32e8f55..42b41f8 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
@@ -126,12 +126,7 @@
     private fun hydrateVisibility() {
         applicationScope.launch {
             // TODO(b/296114544): Combine with some global hun state to make it visible!
-            combine(
-                    deviceProvisioningInteractor.isDeviceProvisioned,
-                    deviceProvisioningInteractor.isFactoryResetProtectionActive,
-                ) { isDeviceProvisioned, isFrpActive ->
-                    isDeviceProvisioned && !isFrpActive
-                }
+            deviceProvisioningInteractor.isDeviceProvisioned
                 .distinctUntilChanged()
                 .flatMapLatest { isAllowedToBeVisible ->
                     if (isAllowedToBeVisible) {
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt
index 3081f89..922997d 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt
@@ -18,32 +18,11 @@
 
 import android.util.Log
 import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.app.tracing.coroutines.launch
-import kotlinx.coroutines.CoroutineScope
-import java.util.function.Consumer
-import javax.inject.Inject
-
-/** Processes a screenshot request sent from [ScreenshotHelper]. */
-interface ScreenshotRequestProcessor {
-    /**
-     * Inspects the incoming ScreenshotData, potentially modifying it based upon policy.
-     *
-     * @param screenshot the screenshot to process
-     */
-    suspend fun process(screenshot: ScreenshotData): ScreenshotData
-}
 
 /** Implementation of [ScreenshotRequestProcessor] */
-@SysUISingleton
-class RequestProcessor
-@Inject
-constructor(
+class RequestProcessor(
     private val capture: ImageCapture,
     private val policy: ScreenshotPolicy,
-    /** For the Java Async version, to invoke the callback. */
-    @Application private val mainScope: CoroutineScope
 ) : ScreenshotRequestProcessor {
 
     override suspend fun process(screenshot: ScreenshotData): ScreenshotData {
@@ -78,24 +57,6 @@
 
         return result
     }
-
-    /**
-     * Note: This is for compatibility with existing Java. Prefer the suspending function when
-     * calling from a Coroutine context.
-     *
-     * @param screenshot the screenshot to process
-     * @param callback the callback to provide the processed screenshot, invoked from the main
-     *                 thread
-     */
-    fun processAsync(screenshot: ScreenshotData, callback: Consumer<ScreenshotData>) {
-        mainScope.launch({ "$TAG#processAsync" }) {
-            val result = process(screenshot)
-            callback.accept(result)
-        }
-    }
 }
 
 private const val TAG = "RequestProcessor"
-
-/** Exception thrown by [RequestProcessor] if something goes wrong. */
-class RequestProcessorException(message: String) : IllegalStateException(message)
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationsController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationsController.kt
index d874eb6..4079abe 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationsController.kt
@@ -105,7 +105,7 @@
 
     /** Factory for [ScreenshotNotificationsController]. */
     @AssistedFactory
-    interface Factory {
-        fun create(displayId: Int = Display.DEFAULT_DISPLAY): ScreenshotNotificationsController
+    fun interface Factory {
+        fun create(displayId: Int): ScreenshotNotificationsController
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt
index d5ab306..c8067df 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt
@@ -30,7 +30,7 @@
 import kotlinx.coroutines.withContext
 
 /** Provides state from the main SystemUI process on behalf of the Screenshot process. */
-internal class ScreenshotProxyService
+class ScreenshotProxyService
 @Inject
 constructor(
     private val mExpansionMgr: ShadeExpansionStateManager,
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotRequestProcessor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotRequestProcessor.kt
new file mode 100644
index 0000000..796457d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotRequestProcessor.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 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.screenshot
+
+/** Processes a screenshot request sent from [ScreenshotHelper]. */
+interface ScreenshotRequestProcessor {
+    /**
+     * Inspects the incoming ScreenshotData, potentially modifying it based upon policy.
+     *
+     * @param screenshot the screenshot to process
+     */
+    suspend fun process(screenshot: ScreenshotData): ScreenshotData
+}
+
+/** Exception thrown by [RequestProcessor] if something goes wrong. */
+class RequestProcessorException(message: String) : IllegalStateException(message)
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
index bc33755..92d3e55 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
@@ -5,7 +5,6 @@
 import android.util.Log
 import android.view.Display
 import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
-import com.android.app.tracing.coroutines.launch
 import com.android.internal.logging.UiEventLogger
 import com.android.internal.util.ScreenshotRequest
 import com.android.systemui.dagger.SysUISingleton
@@ -18,6 +17,23 @@
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+
+interface TakeScreenshotExecutor {
+    suspend fun executeScreenshots(
+        screenshotRequest: ScreenshotRequest,
+        onSaved: (Uri) -> Unit,
+        requestCallback: RequestCallback
+    )
+    fun onCloseSystemDialogsReceived()
+    fun removeWindows()
+    fun onDestroy()
+    fun executeScreenshotsAsync(
+        screenshotRequest: ScreenshotRequest,
+        onSaved: Consumer<Uri>,
+        requestCallback: RequestCallback
+    )
+}
 
 /**
  * Receives the signal to take a screenshot from [TakeScreenshotService], and calls back with the
@@ -26,7 +42,7 @@
  * Captures a screenshot for each [Display] available.
  */
 @SysUISingleton
-class TakeScreenshotExecutor
+class TakeScreenshotExecutorImpl
 @Inject
 constructor(
     private val screenshotControllerFactory: ScreenshotController.Factory,
@@ -35,7 +51,7 @@
     private val screenshotRequestProcessor: ScreenshotRequestProcessor,
     private val uiEventLogger: UiEventLogger,
     private val screenshotNotificationControllerFactory: ScreenshotNotificationsController.Factory,
-) {
+) : TakeScreenshotExecutor {
 
     private val displays = displayRepository.displays
     private val screenshotControllers = mutableMapOf<Int, ScreenshotController>()
@@ -47,7 +63,7 @@
      * [onSaved] is invoked only on the default display result. [RequestCallback.onFinish] is
      * invoked only when both screenshot UIs are removed.
      */
-    suspend fun executeScreenshots(
+    override suspend fun executeScreenshots(
         screenshotRequest: ScreenshotRequest,
         onSaved: (Uri) -> Unit,
         requestCallback: RequestCallback
@@ -128,12 +144,8 @@
         }
     }
 
-    /**
-     * Propagates the close system dialog signal to all controllers.
-     *
-     * TODO(b/295143676): Move the receiver in this class once the flag is flipped.
-     */
-    fun onCloseSystemDialogsReceived() {
+    /** Propagates the close system dialog signal to all controllers. */
+    override fun onCloseSystemDialogsReceived() {
         screenshotControllers.forEach { (_, screenshotController) ->
             if (!screenshotController.isPendingSharedTransition) {
                 screenshotController.requestDismissal(ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER)
@@ -142,7 +154,7 @@
     }
 
     /** Removes all screenshot related windows. */
-    fun removeWindows() {
+    override fun removeWindows() {
         screenshotControllers.forEach { (_, screenshotController) ->
             screenshotController.removeWindow()
         }
@@ -151,7 +163,7 @@
     /**
      * Destroys the executor. Afterwards, this class is not expected to work as intended anymore.
      */
-    fun onDestroy() {
+    override fun onDestroy() {
         screenshotControllers.forEach { (_, screenshotController) ->
             screenshotController.onDestroy()
         }
@@ -171,12 +183,12 @@
     }
 
     /** For java compatibility only. see [executeScreenshots] */
-    fun executeScreenshotsAsync(
+    override fun executeScreenshotsAsync(
         screenshotRequest: ScreenshotRequest,
         onSaved: Consumer<Uri>,
         requestCallback: RequestCallback
     ) {
-        mainScope.launch("TakeScreenshotService#executeScreenshotsAsync") {
+        mainScope.launch {
             executeScreenshots(screenshotRequest, { uri -> onSaved.accept(uri) }, requestCallback)
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
index 9cf347b..2187b51 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
@@ -21,13 +21,11 @@
 
 import static com.android.internal.util.ScreenshotHelper.SCREENSHOT_MSG_PROCESS_COMPLETE;
 import static com.android.internal.util.ScreenshotHelper.SCREENSHOT_MSG_URI;
-import static com.android.systemui.flags.Flags.MULTI_DISPLAY_SCREENSHOT;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_SERVICE;
 import static com.android.systemui.screenshot.LogConfig.logTag;
 import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_CAPTURE_FAILED;
-import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER;
 
 import android.annotation.MainThread;
 import android.app.Service;
@@ -50,24 +48,19 @@
 import android.view.Display;
 import android.widget.Toast;
 
-import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.util.ScreenshotRequest;
 import com.android.systemui.dagger.qualifiers.Background;
-import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.res.R;
 
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
 import javax.inject.Inject;
-import javax.inject.Provider;
 
 public class TakeScreenshotService extends Service {
     private static final String TAG = logTag(TakeScreenshotService.class);
 
-    private final ScreenshotController mScreenshot;
-
     private final UserManager mUserManager;
     private final DevicePolicyManager mDevicePolicyManager;
     private final UiEventLogger mUiEventLogger;
@@ -75,27 +68,20 @@
     private final Handler mHandler;
     private final Context mContext;
     private final @Background Executor mBgExecutor;
-    private final RequestProcessor mProcessor;
-    private final FeatureFlags mFeatureFlags;
+    private final TakeScreenshotExecutor mTakeScreenshotExecutor;
 
+    @SuppressWarnings("deprecation")
     private final BroadcastReceiver mCloseSystemDialogs = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
-            if (ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction()) && mScreenshot != null) {
+            if (ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) {
                 if (DEBUG_DISMISS) {
                     Log.d(TAG, "Received ACTION_CLOSE_SYSTEM_DIALOGS");
                 }
-                if (mFeatureFlags.isEnabled(MULTI_DISPLAY_SCREENSHOT)) {
-                    // TODO(b/295143676): move receiver inside executor when the flag is enabled.
-                    mTakeScreenshotExecutor.get().onCloseSystemDialogsReceived();
-                } else if (!mScreenshot.isPendingSharedTransition()) {
-                    mScreenshot.requestDismissal(SCREENSHOT_DISMISSED_OTHER);
-                }
+                mTakeScreenshotExecutor.onCloseSystemDialogsReceived();
             }
         }
     };
-    private final Provider<TakeScreenshotExecutor> mTakeScreenshotExecutor;
-
 
     /** Informs about coarse grained state of the Controller. */
     public interface RequestCallback {
@@ -111,12 +97,14 @@
     }
 
     @Inject
-    public TakeScreenshotService(ScreenshotController.Factory screenshotControllerFactory,
-            UserManager userManager, DevicePolicyManager devicePolicyManager,
+    public TakeScreenshotService(
+            UserManager userManager,
+            DevicePolicyManager devicePolicyManager,
             UiEventLogger uiEventLogger,
             ScreenshotNotificationsController.Factory notificationsControllerFactory,
-            Context context, @Background Executor bgExecutor, FeatureFlags featureFlags,
-            RequestProcessor processor, Provider<TakeScreenshotExecutor> takeScreenshotExecutor) {
+            Context context,
+            @Background Executor bgExecutor,
+            TakeScreenshotExecutor takeScreenshotExecutor) {
         if (DEBUG_SERVICE) {
             Log.d(TAG, "new " + this);
         }
@@ -127,15 +115,7 @@
         mNotificationsController = notificationsControllerFactory.create(Display.DEFAULT_DISPLAY);
         mContext = context;
         mBgExecutor = bgExecutor;
-        mFeatureFlags = featureFlags;
-        mProcessor = processor;
         mTakeScreenshotExecutor = takeScreenshotExecutor;
-        if (mFeatureFlags.isEnabled(MULTI_DISPLAY_SCREENSHOT)) {
-            mScreenshot = null;
-        } else {
-            mScreenshot = screenshotControllerFactory.create(
-                    Display.DEFAULT_DISPLAY, /* showUIOnExternalDisplay= */ false);
-        }
     }
 
     @Override
@@ -161,11 +141,7 @@
         if (DEBUG_SERVICE) {
             Log.d(TAG, "onUnbind");
         }
-        if (mFeatureFlags.isEnabled(MULTI_DISPLAY_SCREENSHOT)) {
-            mTakeScreenshotExecutor.get().removeWindows();
-        } else {
-            mScreenshot.removeWindow();
-        }
+        mTakeScreenshotExecutor.removeWindows();
         unregisterReceiver(mCloseSystemDialogs);
         return false;
     }
@@ -173,11 +149,7 @@
     @Override
     public void onDestroy() {
         super.onDestroy();
-        if (mFeatureFlags.isEnabled(MULTI_DISPLAY_SCREENSHOT)) {
-            mTakeScreenshotExecutor.get().onDestroy();
-        } else {
-            mScreenshot.onDestroy();
-        }
+        mTakeScreenshotExecutor.onDestroy();
         if (DEBUG_SERVICE) {
             Log.d(TAG, "onDestroy");
         }
@@ -190,6 +162,7 @@
             mReplyTo = replyTo;
         }
 
+        @Override
         public void reportError() {
             reportUri(mReplyTo, null);
             sendComplete(mReplyTo);
@@ -214,7 +187,6 @@
     }
 
     @MainThread
-    @VisibleForTesting
     void handleRequest(ScreenshotRequest request, Consumer<Uri> onSaved,
             RequestCallback callback) {
         // If the storage for this user is locked, we have no place to store
@@ -245,36 +217,9 @@
         }
 
         Log.d(TAG, "Processing screenshot data");
-
-
-        if (mFeatureFlags.isEnabled(MULTI_DISPLAY_SCREENSHOT)) {
-            mTakeScreenshotExecutor.get().executeScreenshotsAsync(request, onSaved, callback);
-            return;
-        }
-        // TODO(b/295143676): Delete the following after the flag is released.
-        try {
-            ScreenshotData screenshotData = ScreenshotData.fromRequest(
-                    request, Display.DEFAULT_DISPLAY);
-            mProcessor.processAsync(screenshotData, (data) ->
-                    dispatchToController(data, onSaved, callback));
-
-        } catch (IllegalStateException e) {
-            Log.e(TAG, "Failed to process screenshot request!", e);
-            logFailedRequest(request);
-            mNotificationsController.notifyScreenshotError(
-                    R.string.screenshot_failed_to_capture_text);
-            callback.reportError();
-        }
+        mTakeScreenshotExecutor.executeScreenshotsAsync(request, onSaved, callback);
     }
 
-    // TODO(b/295143676): Delete this.
-    private void dispatchToController(ScreenshotData screenshot,
-            Consumer<Uri> uriConsumer, RequestCallback callback) {
-        mUiEventLogger.log(ScreenshotEvent.getScreenshotSource(screenshot.getSource()), 0,
-                screenshot.getPackageNameString());
-        Log.d(TAG, "Screenshot request: " + screenshot);
-        mScreenshot.handleScreenshot(screenshot, uriConsumer, callback);
-    }
 
     private void logFailedRequest(ScreenshotRequest request) {
         ComponentName topComponent = request.getTopComponent();
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
index 2ce6d83..6ff0fda 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
@@ -26,21 +26,22 @@
 import com.android.systemui.screenshot.ImageCapture;
 import com.android.systemui.screenshot.ImageCaptureImpl;
 import com.android.systemui.screenshot.LegacyScreenshotViewProxy;
-import com.android.systemui.screenshot.RequestProcessor;
 import com.android.systemui.screenshot.ScreenshotActionsProvider;
 import com.android.systemui.screenshot.ScreenshotPolicy;
 import com.android.systemui.screenshot.ScreenshotPolicyImpl;
-import com.android.systemui.screenshot.ScreenshotProxyService;
-import com.android.systemui.screenshot.ScreenshotRequestProcessor;
 import com.android.systemui.screenshot.ScreenshotShelfViewProxy;
 import com.android.systemui.screenshot.ScreenshotSoundController;
 import com.android.systemui.screenshot.ScreenshotSoundControllerImpl;
 import com.android.systemui.screenshot.ScreenshotSoundProvider;
 import com.android.systemui.screenshot.ScreenshotSoundProviderImpl;
 import com.android.systemui.screenshot.ScreenshotViewProxy;
+import com.android.systemui.screenshot.TakeScreenshotExecutor;
+import com.android.systemui.screenshot.TakeScreenshotExecutorImpl;
 import com.android.systemui.screenshot.TakeScreenshotService;
 import com.android.systemui.screenshot.appclips.AppClipsScreenshotHelperService;
 import com.android.systemui.screenshot.appclips.AppClipsService;
+import com.android.systemui.screenshot.policy.ScreenshotPolicyModule;
+import com.android.systemui.screenshot.proxy.SystemUiProxyModule;
 import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel;
 
 import dagger.Binds;
@@ -52,7 +53,7 @@
 /**
  * Defines injectable resources for Screenshots
  */
-@Module
+@Module(includes = {ScreenshotPolicyModule.class, SystemUiProxyModule.class})
 public abstract class ScreenshotModule {
 
     @Binds
@@ -61,9 +62,9 @@
     abstract Service bindTakeScreenshotService(TakeScreenshotService service);
 
     @Binds
-    @IntoMap
-    @ClassKey(ScreenshotProxyService.class)
-    abstract Service bindScreenshotProxyService(ScreenshotProxyService service);
+    @SysUISingleton
+    abstract TakeScreenshotExecutor bindTakeScreenshotExecutor(
+            TakeScreenshotExecutorImpl impl);
 
     @Binds
     abstract ScreenshotPolicy bindScreenshotPolicyImpl(ScreenshotPolicyImpl impl);
@@ -82,10 +83,6 @@
     abstract Service bindAppClipsService(AppClipsService service);
 
     @Binds
-    abstract ScreenshotRequestProcessor bindScreenshotRequestProcessor(
-            RequestProcessor requestProcessor);
-
-    @Binds
     abstract ScreenshotSoundProvider bindScreenshotSoundProvider(
             ScreenshotSoundProviderImpl screenshotSoundProviderImpl);
 
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/data/model/ProfileType.kt b/packages/SystemUI/src/com/android/systemui/screenshot/data/model/ProfileType.kt
new file mode 100644
index 0000000..38016ad
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/data/model/ProfileType.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 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.screenshot.data.model
+
+/** The profile type of a user. */
+enum class ProfileType {
+    /** The user is not a profile. */
+    NONE,
+    /** Private space user */
+    PRIVATE,
+    /** Managed (work) profile. */
+    WORK,
+    /** Cloned apps user */
+    CLONE,
+    /** Communal profile */
+    COMMUNAL,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/data/repository/ProfileTypeRepository.kt b/packages/SystemUI/src/com/android/systemui/screenshot/data/repository/ProfileTypeRepository.kt
new file mode 100644
index 0000000..2c6e4fe
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/data/repository/ProfileTypeRepository.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 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.screenshot.data.repository
+
+import android.annotation.UserIdInt
+import com.android.systemui.screenshot.data.model.ProfileType
+
+/** A facility for checking user profile types. */
+fun interface ProfileTypeRepository {
+    /**
+     * Returns the profile type when [userId] refers to a profile user. If the profile type is
+     * unknown or not a profile user, [ProfileType.NONE] is returned.
+     */
+    suspend fun getProfileType(@UserIdInt userId: Int): ProfileType
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/data/repository/ProfileTypeRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/screenshot/data/repository/ProfileTypeRepositoryImpl.kt
new file mode 100644
index 0000000..42ad21bd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/data/repository/ProfileTypeRepositoryImpl.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2024 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:SuppressLint("MissingPermission")
+
+package com.android.systemui.screenshot.data.repository
+
+import android.annotation.SuppressLint
+import android.annotation.UserIdInt
+import android.os.UserManager
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.screenshot.data.model.ProfileType
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withContext
+
+/** Fetches profile types from [UserManager] as needed, caching results for a given user. */
+class ProfileTypeRepositoryImpl
+@Inject
+constructor(
+    private val userManager: UserManager,
+    @Background private val background: CoroutineDispatcher
+) : ProfileTypeRepository {
+    /** Cache to avoid repeated requests to IActivityTaskManager for the same userId */
+    private val cache = mutableMapOf<Int, ProfileType>()
+    private val mutex = Mutex()
+
+    override suspend fun getProfileType(@UserIdInt userId: Int): ProfileType {
+        return mutex.withLock {
+            cache[userId]
+                ?: withContext(background) {
+                        val userType = userManager.getUserInfo(userId).userType
+                        when (userType) {
+                            UserManager.USER_TYPE_PROFILE_MANAGED -> ProfileType.WORK
+                            UserManager.USER_TYPE_PROFILE_PRIVATE -> ProfileType.PRIVATE
+                            UserManager.USER_TYPE_PROFILE_CLONE -> ProfileType.CLONE
+                            UserManager.USER_TYPE_PROFILE_COMMUNAL -> ProfileType.COMMUNAL
+                            else -> ProfileType.NONE
+                        }
+                    }
+                    .also { cache[userId] = it }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/ScreenshotPolicyModule.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/ScreenshotPolicyModule.kt
new file mode 100644
index 0000000..39b07e3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/ScreenshotPolicyModule.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2024 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.screenshot.policy
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.screenshot.ImageCapture
+import com.android.systemui.screenshot.RequestProcessor
+import com.android.systemui.screenshot.ScreenshotPolicy
+import com.android.systemui.screenshot.ScreenshotRequestProcessor
+import com.android.systemui.screenshot.data.repository.ProfileTypeRepository
+import com.android.systemui.screenshot.data.repository.ProfileTypeRepositoryImpl
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import javax.inject.Provider
+
+@Module
+interface ScreenshotPolicyModule {
+
+    @Binds
+    @SysUISingleton
+    fun bindProfileTypeRepository(impl: ProfileTypeRepositoryImpl): ProfileTypeRepository
+
+    companion object {
+        @Provides
+        @SysUISingleton
+        fun bindScreenshotRequestProcessor(
+            imageCapture: ImageCapture,
+            policyProvider: Provider<ScreenshotPolicy>,
+        ): ScreenshotRequestProcessor {
+            return RequestProcessor(imageCapture, policyProvider.get())
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/proxy/SystemUiProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/proxy/SystemUiProxy.kt
new file mode 100644
index 0000000..e3eb3c4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/proxy/SystemUiProxy.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 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.screenshot.proxy
+
+/**
+ * Provides a mechanism to interact with the main SystemUI process.
+ *
+ * ScreenshotService runs in an isolated process. Because of this, interactions with an outside
+ * component with shared state must be accessed through this proxy to reach the correct instance.
+ *
+ * TODO: Rename and relocate 'ScreenshotProxyService' to this package and remove duplicate clients.
+ */
+interface SystemUiProxy {
+    /** Indicate if the notification shade is "open"... (not in the fully collapsed position) */
+    suspend fun isNotificationShadeExpanded(): Boolean
+
+    /**
+     * Request keyguard dismissal, raising keyguard credential entry if required and waits for
+     * completion.
+     */
+    suspend fun dismissKeyguard()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/proxy/SystemUiProxyClient.kt b/packages/SystemUI/src/com/android/systemui/screenshot/proxy/SystemUiProxyClient.kt
new file mode 100644
index 0000000..dcf58bd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/proxy/SystemUiProxyClient.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2024 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.screenshot.proxy
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+import com.android.internal.infra.ServiceConnector
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.screenshot.IOnDoneCallback
+import com.android.systemui.screenshot.IScreenshotProxy
+import com.android.systemui.screenshot.ScreenshotProxyService
+import javax.inject.Inject
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+import kotlinx.coroutines.CompletableDeferred
+
+private const val TAG = "SystemUiProxy"
+
+/** An implementation of [SystemUiProxy] using [ScreenshotProxyService]. */
+class SystemUiProxyClient @Inject constructor(@Application context: Context) : SystemUiProxy {
+    @SuppressLint("ImplicitSamInstance")
+    private val proxyConnector: ServiceConnector<IScreenshotProxy> =
+        ServiceConnector.Impl(
+            context,
+            Intent(context, ScreenshotProxyService::class.java),
+            Context.BIND_AUTO_CREATE or Context.BIND_WAIVE_PRIORITY or Context.BIND_NOT_VISIBLE,
+            context.userId,
+            IScreenshotProxy.Stub::asInterface
+        )
+
+    override suspend fun isNotificationShadeExpanded(): Boolean = suspendCoroutine { k ->
+        proxyConnector
+            .postForResult { it.isNotificationShadeExpanded }
+            .whenComplete { expanded, error ->
+                error?.also { Log.wtf(TAG, "isNotificationShadeExpanded", it) }
+                k.resume(expanded ?: false)
+            }
+    }
+
+    override suspend fun dismissKeyguard() {
+        val completion = CompletableDeferred<Unit>()
+        val onDoneBinder =
+            object : IOnDoneCallback.Stub() {
+                override fun onDone(success: Boolean) {
+                    completion.complete(Unit)
+                }
+            }
+        if (proxyConnector.run { it.dismissKeyguard(onDoneBinder) }) {
+            completion.await()
+        } else {
+            Log.wtf(TAG, "Keyguard dismissal request failed")
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/proxy/SystemUiProxyModule.kt b/packages/SystemUI/src/com/android/systemui/screenshot/proxy/SystemUiProxyModule.kt
new file mode 100644
index 0000000..4dd5cc4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/proxy/SystemUiProxyModule.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 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.screenshot.proxy
+
+import android.app.Service
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.screenshot.ScreenshotProxyService
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+
+@Module
+interface SystemUiProxyModule {
+
+    @Binds
+    @IntoMap
+    @ClassKey(ScreenshotProxyService::class)
+    fun bindScreenshotProxyService(service: ScreenshotProxyService): Service
+
+    @Binds
+    @SysUISingleton
+    fun bindSystemUiProxy(systemUiProxyClient: SystemUiProxyClient): SystemUiProxy
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index c93ef65..8d66fa7 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -334,6 +334,7 @@
     private final ScrimController mScrimController;
     private final LockscreenShadeTransitionController mLockscreenShadeTransitionController;
     private final TapAgainViewController mTapAgainViewController;
+    private final ShadeHeaderController mShadeHeaderController;
     private final boolean mVibrateOnOpening;
     private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
     private final FlingAnimationUtils mFlingAnimationUtilsClosing;
@@ -730,6 +731,7 @@
             FragmentService fragmentService,
             IStatusBarService statusBarService,
             ContentResolver contentResolver,
+            ShadeHeaderController shadeHeaderController,
             ScreenOffAnimationController screenOffAnimationController,
             LockscreenGestureLogger lockscreenGestureLogger,
             ShadeExpansionStateManager shadeExpansionStateManager,
@@ -874,6 +876,7 @@
         mSplitShadeEnabled =
                 mSplitShadeStateController.shouldUseSplitNotificationShade(mResources);
         mView.setWillNotDraw(!DEBUG_DRAWABLE);
+        mShadeHeaderController = shadeHeaderController;
         mLayoutInflater = layoutInflater;
         mFeatureFlags = featureFlags;
         mAnimateBack = predictiveBackAnimateShade();
@@ -1107,6 +1110,9 @@
         }
 
         mTapAgainViewController.init();
+        mShadeHeaderController.init();
+        mShadeHeaderController.setShadeCollapseAction(
+                () -> collapse(/* delayed= */ false , /* speedUpFactor= */ 1.0f));
         mKeyguardUnfoldTransition.ifPresent(u -> u.setup(mView));
         mNotificationPanelUnfoldAnimationController.ifPresent(controller ->
                 controller.setup(mNotificationContainerParent));
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
index 7525184..243ea68 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
@@ -965,14 +965,21 @@
         }
     }
 
-    void updateQsState() {
-        boolean qsFullScreen = getExpanded() && !mSplitShadeEnabled;
+    private void setQsFullScreen(boolean qsFullScreen) {
         mShadeRepository.setLegacyQsFullscreen(qsFullScreen);
         mNotificationStackScrollLayoutController.setQsFullScreen(qsFullScreen);
         if (!SceneContainerFlag.isEnabled()) {
             mNotificationStackScrollLayoutController.setScrollingEnabled(
                     mBarState != KEYGUARD && (!qsFullScreen || mExpansionFromOverscroll));
         }
+    }
+
+    void updateQsState() {
+        if (!FooterViewRefactor.isEnabled()) {
+            // Update full screen state; note that this will be true if the QS panel is only
+            // partially expanded, and that is fixed with the footer view refactor.
+            setQsFullScreen(/* qsFullScreen = */ getExpanded() && !mSplitShadeEnabled);
+        }
 
         if (mQsStateUpdateListener != null) {
             mQsStateUpdateListener.onQsStateUpdated(getExpanded(), mStackScrollerOverscrolling);
@@ -1035,6 +1042,11 @@
 
         // Update the light bar
         mLightBarController.setQsExpanded(mFullyExpanded);
+
+        if (FooterViewRefactor.isEnabled()) {
+            // Update full screen state
+            setQsFullScreen(/* qsFullScreen = */ mFullyExpanded && !mSplitShadeEnabled);
+        }
     }
 
     float getLockscreenShadeDragProgress() {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
index 8d23f5d..b5b46f1 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
@@ -17,9 +17,9 @@
 package com.android.systemui.shade
 
 import android.view.MotionEvent
-import com.android.systemui.log.dagger.ShadeLog
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.dagger.ShadeLog
 import com.android.systemui.shade.ShadeViewController.Companion.FLING_COLLAPSE
 import com.android.systemui.shade.ShadeViewController.Companion.FLING_EXPAND
 import com.android.systemui.shade.ShadeViewController.Companion.FLING_HIDE
@@ -304,8 +304,7 @@
         msg: String,
         forceCancel: Boolean,
         expand: Boolean,
-    )
-    {
+    ) {
         buffer.log(
             TAG,
             LogLevel.VERBOSE,
@@ -322,8 +321,7 @@
         msg: String,
         panelClosedOnDown: Boolean,
         expandFraction: Float,
-    )
-    {
+    ) {
         buffer.log(
             TAG,
             LogLevel.VERBOSE,
@@ -381,7 +379,6 @@
         shouldControlScreenOff: Boolean,
         deviceInteractive: Boolean,
         isPulsing: Boolean,
-        isFrpActive: Boolean,
     ) {
         buffer.log(
             TAG,
@@ -392,12 +389,11 @@
                 bool3 = shouldControlScreenOff
                 bool4 = deviceInteractive
                 str1 = isPulsing.toString()
-                str2 = isFrpActive.toString()
             },
             {
                 "CentralSurfaces updateNotificationPanelTouchState set disabled to: $bool1\n" +
                         "isGoingToSleep: $bool2, !shouldControlScreenOff: $bool3," +
-                        "!mDeviceInteractive: $bool4, !isPulsing: $str1, isFrpActive: $str2"
+                        "!mDeviceInteractive: $bool4, !isPulsing: $str1"
             }
         )
     }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
index cde45f2..0de3c10 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
@@ -29,6 +29,9 @@
     /** Emits true if the shade is currently allowed and false otherwise. */
     val isShadeEnabled: StateFlow<Boolean>
 
+    /** Emits true if QS is currently allowed and false otherwise. */
+    val isQsEnabled: StateFlow<Boolean>
+
     /** Whether either the shade or QS is fully expanded. */
     val isAnyFullyExpanded: StateFlow<Boolean>
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
index 5fbd2cf..883ef97 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt
@@ -29,6 +29,7 @@
     private val inactiveFlowBoolean = MutableStateFlow(false)
     private val inactiveFlowFloat = MutableStateFlow(0f)
     override val isShadeEnabled: StateFlow<Boolean> = inactiveFlowBoolean
+    override val isQsEnabled: StateFlow<Boolean> = inactiveFlowBoolean
     override val shadeExpansion: StateFlow<Float> = inactiveFlowFloat
     override val qsExpansion: StateFlow<Float> = inactiveFlowFloat
     override val isQsExpanded: StateFlow<Boolean> = inactiveFlowBoolean
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
index e619806..d68e28c 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
@@ -28,7 +28,6 @@
 import com.android.systemui.statusbar.policy.data.repository.UserSetupRepository
 import com.android.systemui.statusbar.policy.domain.interactor.DeviceProvisioningInteractor
 import com.android.systemui.user.domain.interactor.UserSwitcherInteractor
-import com.android.systemui.util.kotlin.combine
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
@@ -56,15 +55,16 @@
     private val baseShadeInteractor: BaseShadeInteractor,
 ) : ShadeInteractor, BaseShadeInteractor by baseShadeInteractor {
     override val isShadeEnabled: StateFlow<Boolean> =
-        combine(
-                deviceProvisioningInteractor.isFactoryResetProtectionActive,
-                disableFlagsRepository.disableFlags,
-            ) { isFrpActive, isDisabledByFlags ->
-                isDisabledByFlags.isShadeEnabled() && !isFrpActive
-            }
+        disableFlagsRepository.disableFlags
+            .map { isDisabledByFlags -> isDisabledByFlags.isShadeEnabled() }
             .distinctUntilChanged()
             .stateIn(scope, SharingStarted.Eagerly, initialValue = false)
 
+    override val isQsEnabled: StateFlow<Boolean> =
+        disableFlagsRepository.disableFlags
+            .map { it.isQuickSettingsEnabled() }
+            .stateIn(scope, SharingStarted.Eagerly, initialValue = false)
+
     override val isAnyFullyExpanded: StateFlow<Boolean> =
         anyExpansion
             .map { it >= 1f }
@@ -84,11 +84,8 @@
             powerInteractor.isAsleep,
             keyguardTransitionInteractor.isInTransitionToStateWhere { it == KeyguardState.AOD },
             keyguardRepository.dozeTransitionModel.map { it.to == DozeStateModel.DOZE_PULSING },
-            deviceProvisioningInteractor.isFactoryResetProtectionActive,
-        ) { isAsleep, goingToSleep, isPulsing, isFrpActive ->
+        ) { isAsleep, goingToSleep, isPulsing ->
             when {
-                // Touches are disabled when Factory Reset Protection is active
-                isFrpActive -> false
                 // If the device is going to sleep, only accept touches if we're still
                 // animating
                 goingToSleep -> dozeParams.shouldControlScreenOff()
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt
index ac881b5..7d46d2b 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt
@@ -66,7 +66,8 @@
                 when (statusBarState) {
                     // legacyShadeExpansion is 1 instead of 0 when QS is expanded
                     StatusBarState.SHADE ->
-                        if (!splitShadeEnabled && qsExpansion > 0f) 0f else legacyShadeExpansion
+                        if (!splitShadeEnabled && qsExpansion > 0f) 1f - qsExpansion
+                        else legacyShadeExpansion
                     StatusBarState.KEYGUARD -> lockscreenShadeExpansion
                     // dragDownAmount, which drives lockscreenShadeExpansion resets to 0f when
                     // the pointer is lifted and the lockscreen shade is fully expanded
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt
index 60810a0..d8216dc 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/startable/ShadeStartable.kt
@@ -23,8 +23,6 @@
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.dagger.ShadeTouchLog
-import com.android.systemui.shade.ShadeController
-import com.android.systemui.shade.ShadeHeaderController
 import com.android.systemui.shade.TouchLogger.Companion.logTouchesTo
 import com.android.systemui.shade.data.repository.ShadeRepository
 import com.android.systemui.shade.shared.model.ShadeMode
@@ -46,25 +44,15 @@
     private val configurationRepository: ConfigurationRepository,
     private val shadeRepository: ShadeRepository,
     private val controller: SplitShadeStateController,
-    private val shadeController: ShadeController,
-    private val shadeHeaderController: ShadeHeaderController,
     private val scrimShadeTransitionController: ScrimShadeTransitionController,
 ) : CoreStartable {
 
     override fun start() {
         hydrateShadeMode()
         logTouchesTo(touchLog)
-        initHeaderController()
         scrimShadeTransitionController.init()
     }
 
-    private fun initHeaderController() {
-        shadeHeaderController.init()
-        shadeHeaderController.shadeCollapseAction = Runnable {
-            shadeController.animateCollapseShade()
-        }
-    }
-
     private fun hydrateShadeMode() {
         applicationScope.launch {
             configurationRepository.onAnyConfigurationChange
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
index 1191c0f..72a9c8d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.res.R
 import com.android.systemui.shade.domain.interactor.PrivacyChipInteractor
 import com.android.systemui.shade.domain.interactor.ShadeHeaderClockInteractor
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
 import java.util.Date
@@ -53,6 +54,7 @@
 constructor(
     @Application private val applicationScope: CoroutineScope,
     context: Context,
+    shadeInteractor: ShadeInteractor,
     mobileIconsInteractor: MobileIconsInteractor,
     val mobileIconsViewModel: MobileIconsViewModel,
     private val privacyChipInteractor: PrivacyChipInteractor,
@@ -85,6 +87,12 @@
     /** Whether or not the privacy chip is enabled in the device privacy config. */
     val isPrivacyChipEnabled: StateFlow<Boolean> = privacyChipInteractor.isChipEnabled
 
+    /** Whether or not the Shade Header should be disabled based on disableFlags. */
+    val isDisabled: StateFlow<Boolean> =
+        shadeInteractor.isQsEnabled
+            .map { !it }
+            .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false)
+
     private val longerPattern = context.getString(R.string.abbrev_wday_month_day_no_year_alarm)
     private val shorterPattern = context.getString(R.string.abbrev_month_day_no_year)
     private val longerDateFormat = MutableStateFlow(getFormatFromPattern(longerPattern))
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
index 0fd0555..c29a64e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
@@ -36,6 +36,7 @@
 import android.view.ContextThemeWrapper
 import android.view.View
 import android.view.ViewGroup
+import androidx.annotation.VisibleForTesting
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.settingslib.Utils
 import com.android.systemui.Dumpable
@@ -45,6 +46,7 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.BcSmartspaceConfigPlugin
 import com.android.systemui.plugins.BcSmartspaceDataPlugin
@@ -95,6 +97,7 @@
         private val deviceProvisionedController: DeviceProvisionedController,
         private val bypassController: KeyguardBypassController,
         private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+        private val wakefulnessLifecycle: WakefulnessLifecycle,
         private val dumpManager: DumpManager,
         private val execution: Execution,
         @Main private val uiExecutor: Executor,
@@ -123,7 +126,7 @@
     private val recentSmartspaceData: Deque<List<SmartspaceTarget>> = LinkedList()
 
     // Smartspace can be used on multiple displays, such as when the user casts their screen
-    private var smartspaceViews = mutableSetOf<SmartspaceView>()
+    @VisibleForTesting var smartspaceViews = mutableSetOf<SmartspaceView>()
     private var regionSamplers =
             mutableMapOf<SmartspaceView, RegionSampler>()
 
@@ -272,6 +275,18 @@
             }
         }
 
+    // TODO(b/331451011): Refactor to viewmodel and use interactor pattern.
+    private val wakefulnessLifecycleObserver =
+        object : WakefulnessLifecycle.Observer {
+            override fun onStartedWakingUp() {
+                smartspaceViews.forEach { it.setScreenOn(true) }
+            }
+
+            override fun onFinishedGoingToSleep() {
+                smartspaceViews.forEach { it.setScreenOn(false) }
+            }
+        }
+
     init {
         deviceProvisionedController.addCallback(deviceProvisionedListener)
         dumpManager.registerDumpable(this)
@@ -451,6 +466,7 @@
         configurationController.addCallback(configChangeListener)
         statusBarStateController.addCallback(statusBarStateListener)
         bypassController.registerOnBypassStateChangedListener(bypassStateChangedListener)
+        wakefulnessLifecycle.addObserver(wakefulnessLifecycleObserver)
 
         datePlugin?.registerSmartspaceEventNotifier { e -> session?.notifySmartspaceEvent(e) }
         weatherPlugin?.registerSmartspaceEventNotifier { e -> session?.notifySmartspaceEvent(e) }
@@ -493,6 +509,7 @@
         configurationController.removeCallback(configChangeListener)
         statusBarStateController.removeCallback(statusBarStateListener)
         bypassController.unregisterOnBypassStateChangedListener(bypassStateChangedListener)
+        wakefulnessLifecycle.removeObserver(wakefulnessLifecycleObserver)
         session = null
 
         datePlugin?.registerSmartspaceEventNotifier(null)
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 3944c3a..3367dc4 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
@@ -4734,6 +4734,11 @@
     }
 
     public void setQsFullScreen(boolean qsFullScreen) {
+        if (FooterViewRefactor.isEnabled()) {
+            if (qsFullScreen == mQsFullScreen) {
+                return;  // no change
+            }
+        }
         mQsFullScreen = qsFullScreen;
         updateAlgorithmLayoutMinHeight();
         updateScrollability();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index f767b99..9df6f93 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -33,6 +33,7 @@
 import com.android.systemui.keyguard.shared.model.KeyguardState.GLANCEABLE_HUB
 import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
 import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
+import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED
 import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER
 import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE
 import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED
@@ -61,6 +62,8 @@
 import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
+import com.android.systemui.util.kotlin.BooleanFlowOperators.and
+import com.android.systemui.util.kotlin.BooleanFlowOperators.or
 import com.android.systemui.util.kotlin.FlowDumperImpl
 import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine
 import javax.inject.Inject
@@ -79,7 +82,6 @@
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.merge
 import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.flow.shareIn
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.flow.transformWhile
 import kotlinx.coroutines.isActive
@@ -123,6 +125,7 @@
 ) : FlowDumperImpl(dumpManager) {
     private val statesForConstrainedNotifications: Set<KeyguardState> =
         setOf(AOD, LOCKSCREEN, DOZING, ALTERNATE_BOUNCER, PRIMARY_BOUNCER)
+    private val statesForHiddenKeyguard: Set<KeyguardState> = setOf(GONE, OCCLUDED)
 
     /**
      * Is either shade/qs expanded? This intentionally does not use the [ShadeInteractor] version,
@@ -194,8 +197,12 @@
             ) { constrainedNotificationState, transitioningToOrFromLockscreen ->
                 constrainedNotificationState || transitioningToOrFromLockscreen
             }
-            .shareIn(scope = applicationScope, started = SharingStarted.Eagerly)
-            .dumpWhileCollecting("isOnLockscreen")
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.Eagerly,
+                initialValue = false
+            )
+            .dumpValue("isOnLockscreen")
 
     /** Are we purely on the keyguard without the shade/qs? */
     val isOnLockscreenWithoutShade: Flow<Boolean> =
@@ -385,31 +392,54 @@
             .onStart { emit(1f) }
             .dumpWhileCollecting("alphaForShadeAndQsExpansion")
 
-    private val isGoneTransitionRunning: Flow<Boolean> =
+    private fun toFlowArray(
+        states: Set<KeyguardState>,
+        flow: (KeyguardState) -> Flow<Boolean>
+    ): Array<Flow<Boolean>> {
+        return states.map { flow(it) }.toTypedArray()
+    }
+
+    private val isTransitioningToHiddenKeyguard: Flow<Boolean> =
         flow {
                 while (currentCoroutineContext().isActive) {
                     emit(false)
-                    // Ensure start where GONE is inactive
-                    keyguardTransitionInteractor.transitionValue(GONE).first { it == 0f }
-                    // Wait for a GONE transition to begin
-                    keyguardTransitionInteractor.transitionStepsToState(GONE).first {
-                        it.value > 0f && it.transitionState == RUNNING
-                    }
+                    // Ensure states are inactive to start
+                    and(
+                            *toFlowArray(statesForHiddenKeyguard) { state ->
+                                keyguardTransitionInteractor.transitionValue(state).map { it == 0f }
+                            }
+                        )
+                        .first { it }
+                    // Wait for a qualifying transition to begin
+                    or(
+                            *toFlowArray(statesForHiddenKeyguard) { state ->
+                                keyguardTransitionInteractor
+                                    .transitionStepsToState(state)
+                                    .map { it.value > 0f && it.transitionState == RUNNING }
+                                    .onStart { emit(false) }
+                            }
+                        )
+                        .first { it }
                     emit(true)
-                    // Now await the signal that SHADE state has been reached or the GONE transition
-                    // was reversed. Until SHADE state has been replaced and merged with GONE, it is
-                    // the only source of when it is considered safe to reset alpha to 1f for HUNs.
+                    // Now await the signal that SHADE state has been reached or the transition was
+                    // reversed. Until SHADE state has been replaced it is the only source of when
+                    // it is considered safe to reset alpha to 1f for HUNs.
                     combine(
                             keyguardInteractor.statusBarState,
-                            // Emit -1f on start to make sure the flow runs
-                            keyguardTransitionInteractor.transitionValue(GONE).onStart { emit(-1f) }
-                        ) { statusBarState, goneValue ->
-                            statusBarState == SHADE || goneValue == 0f
+                            and(
+                                *toFlowArray(statesForHiddenKeyguard) { state ->
+                                    keyguardTransitionInteractor.transitionValue(state).map {
+                                        it == 0f
+                                    }
+                                }
+                            )
+                        ) { statusBarState, stateIsReversed ->
+                            statusBarState == SHADE || stateIsReversed
                         }
                         .first { it }
                 }
             }
-            .dumpWhileCollecting("goneTransitionInProgress")
+            .dumpWhileCollecting("isTransitioningToHiddenKeyguard")
 
     fun keyguardAlpha(viewState: ViewStateAccessor): Flow<Float> {
         // All transition view models are mututally exclusive, and safe to merge
@@ -440,7 +470,7 @@
                 // shade expansion or swipe to dismiss
                 combineTransform(
                     isOnLockscreenWithoutShade,
-                    isGoneTransitionRunning,
+                    isTransitioningToHiddenKeyguard,
                     shadeCollapseFadeIn,
                     alphaForShadeAndQsExpansion,
                     keyguardInteractor.dismissAlpha.dumpWhileCollecting(
@@ -448,7 +478,7 @@
                     ),
                 ) {
                     isOnLockscreenWithoutShade,
-                    isGoneTransitionRunning,
+                    isTransitioningToHiddenKeyguard,
                     shadeCollapseFadeIn,
                     alphaForShadeAndQsExpansion,
                     dismissAlpha ->
@@ -456,7 +486,7 @@
                         if (!shadeCollapseFadeIn && dismissAlpha != null) {
                             emit(dismissAlpha)
                         }
-                    } else if (!isGoneTransitionRunning) {
+                    } else if (!isTransitioningToHiddenKeyguard) {
                         emit(alphaForShadeAndQsExpansion)
                     }
                 },
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
index ad3012e5..e93c0f6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
@@ -52,6 +52,7 @@
 import com.android.systemui.qs.QSPanelController;
 import com.android.systemui.recents.ScreenPinningRequest;
 import com.android.systemui.res.R;
+import com.android.systemui.scene.shared.flag.SceneContainerFlag;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shade.CameraLauncher;
 import com.android.systemui.shade.QuickSettingsController;
@@ -279,7 +280,9 @@
             }
         }
 
-        mShadeHeaderController.disable(state1, state2, animate);
+        if (!SceneContainerFlag.isEnabled()) {
+            mShadeHeaderController.disable(state1, state2, animate);
+        }
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 710cdcd..b6f653f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -2628,11 +2628,10 @@
         boolean goingToSleepWithoutAnimation = isGoingToSleep()
                 && !mDozeParameters.shouldControlScreenOff();
         boolean disabled = (!mDeviceInteractive && !mDozeServiceHost.isPulsing())
-                || goingToSleepWithoutAnimation
-                || mDeviceProvisionedController.isFrpActive();
+                || goingToSleepWithoutAnimation;
         mShadeLogger.logUpdateNotificationPanelTouchState(disabled, isGoingToSleep(),
                 !mDozeParameters.shouldControlScreenOff(), !mDeviceInteractive,
-                !mDozeServiceHost.isPulsing(), mDeviceProvisionedController.isFrpActive());
+                !mDozeServiceHost.isPulsing());
 
         mShadeSurface.setTouchAndAnimationDisabled(disabled);
         if (!NotificationIconContainerRefactor.isEnabled()) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceProvisionedController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceProvisionedController.java
index 0d09fc1..e432158 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceProvisionedController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceProvisionedController.java
@@ -50,9 +50,6 @@
      */
     boolean isCurrentUserSetup();
 
-    /** Returns true when Factory Reset Protection is locking the device. */
-    boolean isFrpActive();
-
     /**
      * Interface to provide calls when the values tracked change
      */
@@ -73,10 +70,5 @@
          * Call when some user changes from not provisioned to provisioned
          */
         default void onUserSetupChanged() { }
-
-        /**
-         * Called when the state of FRP changes.
-         */
-        default void onFrpActiveChanged() {}
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImpl.kt
index 8b20283..8b63dfe 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImpl.kt
@@ -36,7 +36,6 @@
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.util.settings.GlobalSettings
 import com.android.systemui.util.settings.SecureSettings
-import com.android.systemui.util.wrapper.BuildInfo
 import java.io.PrintWriter
 import java.util.concurrent.Executor
 import java.util.concurrent.atomic.AtomicBoolean
@@ -48,7 +47,6 @@
     private val globalSettings: GlobalSettings,
     private val userTracker: UserTracker,
     private val dumpManager: DumpManager,
-    private val buildInfo: BuildInfo,
     @Background private val backgroundHandler: Handler,
     @Main private val mainExecutor: Executor
 ) : DeviceProvisionedController,
@@ -62,11 +60,9 @@
     }
 
     private val deviceProvisionedUri = globalSettings.getUriFor(Settings.Global.DEVICE_PROVISIONED)
-    private val frpActiveUri = globalSettings.getUriFor(Settings.Global.SECURE_FRP_MODE)
     private val userSetupUri = secureSettings.getUriFor(Settings.Secure.USER_SETUP_COMPLETE)
 
     private val deviceProvisioned = AtomicBoolean(false)
-    private val frpActive = AtomicBoolean(false)
     @GuardedBy("lock")
     private val userSetupComplete = SparseBooleanArray()
     @GuardedBy("lock")
@@ -93,15 +89,11 @@
             userId: Int
         ) {
             val updateDeviceProvisioned = deviceProvisionedUri in uris
-            val updateFrp = frpActiveUri in uris
             val updateUser = if (userSetupUri in uris) userId else NO_USERS
-            updateValues(updateDeviceProvisioned, updateFrp, updateUser)
+            updateValues(updateDeviceProvisioned, updateUser)
             if (updateDeviceProvisioned) {
                 onDeviceProvisionedChanged()
             }
-            if (updateFrp) {
-                onFrpActiveChanged()
-            }
             if (updateUser != NO_USERS) {
                 onUserSetupChanged()
             }
@@ -111,7 +103,7 @@
     private val userChangedCallback = object : UserTracker.Callback {
         @WorkerThread
         override fun onUserChanged(newUser: Int, userContext: Context) {
-            updateValues(updateDeviceProvisioned = false, updateFrp = false, updateUser = newUser)
+            updateValues(updateDeviceProvisioned = false, updateUser = newUser)
             onUserSwitched()
         }
 
@@ -133,23 +125,18 @@
         updateValues()
         userTracker.addCallback(userChangedCallback, backgroundExecutor)
         globalSettings.registerContentObserver(deviceProvisionedUri, observer)
-        globalSettings.registerContentObserver(frpActiveUri, observer)
         secureSettings.registerContentObserverForUser(userSetupUri, observer, UserHandle.USER_ALL)
     }
 
     @WorkerThread
     private fun updateValues(
-        updateDeviceProvisioned: Boolean = true,
-        updateFrp: Boolean = true,
-        updateUser: Int = ALL_USERS
+            updateDeviceProvisioned: Boolean = true,
+            updateUser: Int = ALL_USERS
     ) {
         if (updateDeviceProvisioned) {
             deviceProvisioned
                     .set(globalSettings.getInt(Settings.Global.DEVICE_PROVISIONED, 0) != 0)
         }
-        if (updateFrp) {
-            frpActive.set(globalSettings.getInt(Settings.Global.SECURE_FRP_MODE, 0) != 0)
-        }
         synchronized(lock) {
             if (updateUser == ALL_USERS) {
                 val n = userSetupComplete.size()
@@ -188,10 +175,6 @@
         return deviceProvisioned.get()
     }
 
-    override fun isFrpActive(): Boolean {
-        return frpActive.get() && !buildInfo.isDebuggable
-    }
-
     override fun isUserSetup(user: Int): Boolean {
         val index = synchronized(lock) {
             userSetupComplete.indexOfKey(user)
@@ -220,12 +203,6 @@
         )
     }
 
-    override fun onFrpActiveChanged() {
-        dispatchChange(
-            DeviceProvisionedController.DeviceProvisionedListener::onFrpActiveChanged
-        )
-    }
-
     override fun onUserSetupChanged() {
         dispatchChange(DeviceProvisionedController.DeviceProvisionedListener::onUserSetupChanged)
     }
@@ -247,7 +224,6 @@
 
     override fun dump(pw: PrintWriter, args: Array<out String>) {
         pw.println("Device provisioned: ${deviceProvisioned.get()}")
-        pw.println("Factory Reset Protection active: ${frpActive.get()}")
         synchronized(lock) {
             pw.println("User setup complete: $userSetupComplete")
             pw.println("Listeners: $listeners")
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
index 9633cb0..579d43b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
@@ -453,6 +453,7 @@
                         setTopMargin(0);
                         if (grandParent != null) grandParent.setClipChildren(true);
                         setVisibility(GONE);
+                        setAlpha(1f);
                         if (mWrapper != null) {
                             mWrapper.setRemoteInputVisible(false);
                         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt
index 1160d65..4838554 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepository.kt
@@ -31,9 +31,6 @@
      * @see android.provider.Settings.Global.DEVICE_PROVISIONED
      */
     val isDeviceProvisioned: Flow<Boolean>
-
-    /** Whether Factory Reset Protection (FRP) is currently active, locking the device. */
-    val isFactoryResetProtectionActive: Flow<Boolean>
 }
 
 @Module
@@ -58,16 +55,4 @@
         trySend(deviceProvisionedController.isDeviceProvisioned)
         awaitClose { deviceProvisionedController.removeCallback(listener) }
     }
-
-    override val isFactoryResetProtectionActive: Flow<Boolean> = conflatedCallbackFlow {
-        val listener =
-            object : DeviceProvisionedController.DeviceProvisionedListener {
-                override fun onFrpActiveChanged() {
-                    trySend(deviceProvisionedController.isFrpActive)
-                }
-            }
-        deviceProvisionedController.addCallback(listener)
-        trySend(deviceProvisionedController.isFrpActive)
-        awaitClose { deviceProvisionedController.removeCallback(listener) }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt
index 32cf86d..66ed092 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/domain/interactor/DeviceProvisioningInteractor.kt
@@ -34,7 +34,4 @@
      * @see android.provider.Settings.Global.DEVICE_PROVISIONED
      */
     val isDeviceProvisioned: Flow<Boolean> = repository.isDeviceProvisioned
-
-    /** Whether Factory Reset Protection (FRP) is currently active, locking the device. */
-    val isFactoryResetProtectionActive: Flow<Boolean> = repository.isFactoryResetProtectionActive
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackAnimationSpecTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackAnimationSpecTest.kt
index 3bdbf97..58011eb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackAnimationSpecTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackAnimationSpecTest.kt
@@ -23,9 +23,9 @@
 
     @Test
     fun sysUi_floatingSystemSurfaces_animationValues() {
-        val maxX = 14.0f
-        val maxY = 4.0f
-        val minScale = 0.8f
+        val maxX = 19.0f
+        val maxY = 14.0f
+        val minScale = 0.9f
 
         val backAnimationSpec = BackAnimationSpec.floatingSystemSurfacesForSysUi(displayMetrics)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtensionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtensionTest.kt
index 2b95973..f5c9bef 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtensionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtensionTest.kt
@@ -4,7 +4,10 @@
 import android.window.BackEvent
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
@@ -48,7 +51,14 @@
 
         onBackAnimationCallback.onBackProgressed(backEvent)
 
-        verify(onBackProgress).invoke(BackTransformation(0f, 0f, 1f))
+        val argumentCaptor = argumentCaptor<BackTransformation>()
+        verify(onBackProgress).invoke(capture(argumentCaptor))
+
+        val actual = argumentCaptor.value
+        val tolerance = 0.0001f
+        assertThat(actual.translateX).isWithin(tolerance).of(0f)
+        assertThat(actual.translateY).isWithin(tolerance).of(0f)
+        assertThat(actual.scale).isWithin(tolerance).of(1f)
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
index 7e41745..0847f01 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
@@ -30,8 +30,6 @@
 import com.android.internal.util.ScreenshotRequest
 import com.android.systemui.screenshot.ScreenshotPolicy.DisplayContentInfo
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.runBlocking
 import org.junit.Assert
 import org.junit.Test
@@ -44,35 +42,8 @@
     private val component = ComponentName("android.test", "android.test.Component")
     private val bounds = Rect(25, 25, 75, 75)
 
-    private val scope = CoroutineScope(Dispatchers.Unconfined)
     private val policy = FakeScreenshotPolicy()
 
-    /** Tests the Java-compatible function wrapper, ensures callback is invoked. */
-    @Test
-    fun testProcessAsync_ScreenshotData() {
-        val request =
-            ScreenshotData.fromRequest(
-                ScreenshotRequest.Builder(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_KEY_OTHER)
-                    .setBitmap(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))
-                    .build()
-            )
-        val processor = RequestProcessor(imageCapture, policy, scope)
-
-        var result: ScreenshotData? = null
-        var callbackCount = 0
-        val callback: (ScreenshotData) -> Unit = { processedRequest: ScreenshotData ->
-            result = processedRequest
-            callbackCount++
-        }
-
-        // runs synchronously, using Unconfined Dispatcher
-        processor.processAsync(request, callback)
-
-        // Callback invoked once returning the same request (no changes)
-        assertThat(callbackCount).isEqualTo(1)
-        assertThat(result).isEqualTo(request)
-    }
-
     @Test
     fun testFullScreenshot() = runBlocking {
         // Indicate that the primary content belongs to a normal user
@@ -84,7 +55,7 @@
 
         val request =
             ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_OTHER).build()
-        val processor = RequestProcessor(imageCapture, policy, scope)
+        val processor = RequestProcessor(imageCapture, policy)
 
         val processedData = processor.process(ScreenshotData.fromRequest(request))
 
@@ -109,7 +80,7 @@
 
         val request =
             ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER).build()
-        val processor = RequestProcessor(imageCapture, policy, scope)
+        val processor = RequestProcessor(imageCapture, policy)
 
         val processedData = processor.process(ScreenshotData.fromRequest(request))
 
@@ -136,7 +107,7 @@
 
         val request =
             ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER).build()
-        val processor = RequestProcessor(imageCapture, policy, scope)
+        val processor = RequestProcessor(imageCapture, policy)
 
         Assert.assertThrows(IllegalStateException::class.java) {
             runBlocking { processor.process(ScreenshotData.fromRequest(request)) }
@@ -146,7 +117,7 @@
     @Test
     fun testProvidedImageScreenshot() = runBlocking {
         val bounds = Rect(50, 50, 150, 150)
-        val processor = RequestProcessor(imageCapture, policy, scope)
+        val processor = RequestProcessor(imageCapture, policy)
 
         policy.setManagedProfile(USER_ID, false)
 
@@ -171,7 +142,7 @@
     @Test
     fun testProvidedImageScreenshot_managedProfile() = runBlocking {
         val bounds = Rect(50, 50, 150, 150)
-        val processor = RequestProcessor(imageCapture, policy, scope)
+        val processor = RequestProcessor(imageCapture, policy)
 
         // Indicate that the screenshot belongs to a manged profile
         policy.setManagedProfile(USER_ID, true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
index 0baee5d..c900463 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
@@ -57,7 +57,7 @@
     private val eventLogger = UiEventLoggerFake()
 
     private val screenshotExecutor =
-        TakeScreenshotExecutor(
+        TakeScreenshotExecutorImpl(
             controllerFactory,
             fakeDisplayRepository,
             testScope,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt
index f3809aa..0776aa7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt
@@ -20,134 +20,97 @@
 import android.app.admin.DevicePolicyResources.Strings.SystemUi.SCREENSHOT_BLOCKED_BY_ADMIN
 import android.app.admin.DevicePolicyResourcesManager
 import android.content.ComponentName
+import android.net.Uri
 import android.os.UserHandle
 import android.os.UserManager
 import android.testing.AndroidTestingRunner
-import android.view.Display
 import android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_OTHER
 import android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN
-import androidx.test.filters.SmallTest
 import com.android.internal.logging.testing.UiEventLoggerFake
 import com.android.internal.util.ScreenshotRequest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.flags.Flags.MULTI_DISPLAY_SCREENSHOT
 import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_CAPTURE_FAILED
 import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_REQUESTED_KEY_OTHER
 import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
 import java.util.function.Consumer
+import kotlinx.coroutines.runBlocking
 import org.junit.Assert.assertEquals
-import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.ArgumentMatchers.isNull
-import org.mockito.Mockito.clearInvocations
-import org.mockito.Mockito.doAnswer
-import org.mockito.Mockito.doThrow
-import org.mockito.Mockito.times
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyZeroInteractions
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
 
 @RunWith(AndroidTestingRunner::class)
-@SmallTest
 class TakeScreenshotServiceTest : SysuiTestCase() {
 
-    private val application = mock<Application>()
-    private val controller = mock<ScreenshotController>()
-    private val controllerFactory = mock<ScreenshotController.Factory>()
-    private val takeScreenshotExecutor = mock<TakeScreenshotExecutor>()
-    private val userManager = mock<UserManager>()
-    private val requestProcessor = mock<RequestProcessor>()
-    private val devicePolicyManager = mock<DevicePolicyManager>()
-    private val devicePolicyResourcesManager = mock<DevicePolicyResourcesManager>()
-    private val notificationsControllerFactory = mock<ScreenshotNotificationsController.Factory>()
+    private val userManager = mock<UserManager> { on { isUserUnlocked } doReturn (true) }
+
+    private val devicePolicyResourcesManager =
+        mock<DevicePolicyResourcesManager> {
+            on { getString(eq(SCREENSHOT_BLOCKED_BY_ADMIN), /* defaultStringLoader= */ any()) }
+                .doReturn("SCREENSHOT_BLOCKED_BY_ADMIN")
+        }
+
+    private val devicePolicyManager =
+        mock<DevicePolicyManager> {
+            on { resources } doReturn (devicePolicyResourcesManager)
+            on { getScreenCaptureDisabled(/* admin= */ isNull(), eq(UserHandle.USER_ALL)) }
+                .doReturn(false)
+        }
+
     private val notificationsController = mock<ScreenshotNotificationsController>()
-    private val callback = mock<RequestCallback>()
+    private val notificationsControllerFactory =
+        ScreenshotNotificationsController.Factory { notificationsController }
 
+    private val executor = FakeScreenshotExecutor()
+    private val callback = FakeRequestCallback()
     private val eventLogger = UiEventLoggerFake()
-    private val flags = FakeFeatureFlags()
     private val topComponent = ComponentName(mContext, TakeScreenshotServiceTest::class.java)
 
-    private lateinit var service: TakeScreenshotService
-
-    @Before
-    fun setUp() {
-        flags.set(MULTI_DISPLAY_SCREENSHOT, false)
-        whenever(devicePolicyManager.resources).thenReturn(devicePolicyResourcesManager)
-        whenever(
-                devicePolicyManager.getScreenCaptureDisabled(
-                    /* admin component (null: any admin) */ isNull(),
-                    eq(UserHandle.USER_ALL)
-                )
-            )
-            .thenReturn(false)
-        whenever(userManager.isUserUnlocked).thenReturn(true)
-        whenever(controllerFactory.create(any(), any())).thenReturn(controller)
-        whenever(notificationsControllerFactory.create(any())).thenReturn(notificationsController)
-
-        // Stub request processor as a synchronous no-op for tests with the flag enabled
-        doAnswer {
-                val request: ScreenshotData = it.getArgument(0) as ScreenshotData
-                val consumer: Consumer<ScreenshotData> = it.getArgument(1)
-                consumer.accept(request)
-            }
-            .whenever(requestProcessor)
-            .processAsync(/* screenshot= */ any(ScreenshotData::class.java), /* callback= */ any())
-
-        service = createService()
-    }
-
     @Test
     fun testServiceLifecycle() {
+        val service = createService()
         service.onCreate()
         service.onBind(null /* unused: Intent */)
+        assertThat(executor.windowsPresent).isTrue()
 
         service.onUnbind(null /* unused: Intent */)
-        verify(controller, times(1)).removeWindow()
+        assertThat(executor.windowsPresent).isFalse()
 
         service.onDestroy()
-        verify(controller, times(1)).onDestroy()
+        assertThat(executor.destroyed).isTrue()
     }
 
     @Test
     fun takeScreenshotFullscreen() {
+        val service = createService()
+
         val request =
             ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER)
                 .setTopComponent(topComponent)
                 .build()
 
         service.handleRequest(request, { /* onSaved */}, callback)
+        assertWithMessage("request received by executor").that(executor.requestReceived).isNotNull()
 
-        verify(controller, times(1))
-            .handleScreenshot(
-                eq(ScreenshotData.fromRequest(request, Display.DEFAULT_DISPLAY)),
-                /* onSavedListener = */ any(),
-                /* requestCallback = */ any()
-            )
-
-        assertEquals("Expected one UiEvent", eventLogger.numLogs(), 1)
-        val logEvent = eventLogger.get(0)
-
-        assertEquals(
-            "Expected SCREENSHOT_REQUESTED UiEvent",
-            logEvent.eventId,
-            SCREENSHOT_REQUESTED_KEY_OTHER.id
-        )
-        assertEquals(
-            "Expected supplied package name",
-            topComponent.packageName,
-            eventLogger.get(0).packageName
-        )
+        assertWithMessage("request received by executor")
+            .that(ScreenshotData.fromRequest(executor.requestReceived!!))
+            .isEqualTo(ScreenshotData.fromRequest(request))
     }
 
     @Test
     fun takeScreenshotFullscreen_userLocked() {
-        whenever(userManager.isUserUnlocked).thenReturn(false)
+        val service = createService()
+        whenever(userManager.isUserUnlocked).doReturn(false)
 
         val request =
             ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER)
@@ -157,47 +120,41 @@
         service.handleRequest(request, { /* onSaved */}, callback)
 
         verify(notificationsController, times(1)).notifyScreenshotError(anyInt())
-        verify(callback, times(1)).reportError()
-        verifyZeroInteractions(controller)
 
-        assertEquals("Expected two UiEvents", 2, eventLogger.numLogs())
+        assertWithMessage("callback errorReported").that(callback.errorReported).isTrue()
+
+        assertWithMessage("UiEvent count").that(eventLogger.numLogs()).isEqualTo(2)
+
         val requestEvent = eventLogger.get(0)
-        assertEquals(
-            "Expected SCREENSHOT_REQUESTED_* UiEvent",
-            SCREENSHOT_REQUESTED_KEY_OTHER.id,
-            requestEvent.eventId
-        )
-        assertEquals(
-            "Expected supplied package name",
-            topComponent.packageName,
-            requestEvent.packageName
-        )
+        assertWithMessage("request UiEvent id")
+            .that(requestEvent.eventId)
+            .isEqualTo(SCREENSHOT_REQUESTED_KEY_OTHER.id)
+
+        assertWithMessage("topComponent package name")
+            .that(requestEvent.packageName)
+            .isEqualTo(topComponent.packageName)
+
         val failureEvent = eventLogger.get(1)
-        assertEquals(
-            "Expected SCREENSHOT_CAPTURE_FAILED UiEvent",
-            SCREENSHOT_CAPTURE_FAILED.id,
-            failureEvent.eventId
-        )
-        assertEquals(
-            "Expected supplied package name",
-            topComponent.packageName,
-            failureEvent.packageName
-        )
+        assertWithMessage("failure UiEvent id")
+            .that(failureEvent.eventId)
+            .isEqualTo(SCREENSHOT_CAPTURE_FAILED.id)
+
+        assertWithMessage("Supplied package name")
+            .that(failureEvent.packageName)
+            .isEqualTo(topComponent.packageName)
     }
 
     @Test
     fun takeScreenshotFullscreen_screenCaptureDisabled_allUsers() {
-        whenever(devicePolicyManager.getScreenCaptureDisabled(isNull(), eq(UserHandle.USER_ALL)))
-            .thenReturn(true)
+        val service = createService()
 
         whenever(
-                devicePolicyResourcesManager.getString(
-                    eq(SCREENSHOT_BLOCKED_BY_ADMIN),
-                    /* Supplier<String> */
-                    any(),
+                devicePolicyManager.getScreenCaptureDisabled(
+                    /* admin= */ isNull(),
+                    eq(UserHandle.USER_ALL)
                 )
             )
-            .thenReturn("SCREENSHOT_BLOCKED_BY_ADMIN")
+            .doReturn(true)
 
         val request =
             ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER)
@@ -205,11 +162,9 @@
                 .build()
 
         service.handleRequest(request, { /* onSaved */}, callback)
+        assertThat(callback.errorReported).isTrue()
+        assertWithMessage("Expected two UiEvents").that(eventLogger.numLogs()).isEqualTo(2)
 
-        // error shown: Toast.makeText(...).show(), untestable
-        verify(callback, times(1)).reportError()
-        verifyZeroInteractions(controller)
-        assertEquals("Expected two UiEvents", 2, eventLogger.numLogs())
         val requestEvent = eventLogger.get(0)
         assertEquals(
             "Expected SCREENSHOT_REQUESTED_* UiEvent",
@@ -234,112 +189,70 @@
         )
     }
 
-    @Test
-    fun takeScreenshot_workProfile_nullBitmap() {
-        val request =
-            ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER)
-                .setTopComponent(topComponent)
-                .build()
-
-        doThrow(IllegalStateException::class.java)
-            .whenever(requestProcessor)
-            .processAsync(any(ScreenshotData::class.java), any())
-
-        service.handleRequest(request, { /* onSaved */}, callback)
-
-        verify(callback, times(1)).reportError()
-        verify(notificationsController, times(1)).notifyScreenshotError(anyInt())
-        verifyZeroInteractions(controller)
-        assertEquals("Expected two UiEvents", 2, eventLogger.numLogs())
-        val requestEvent = eventLogger.get(0)
-        assertEquals(
-            "Expected SCREENSHOT_REQUESTED_* UiEvent",
-            SCREENSHOT_REQUESTED_KEY_OTHER.id,
-            requestEvent.eventId
-        )
-        assertEquals(
-            "Expected supplied package name",
-            topComponent.packageName,
-            requestEvent.packageName
-        )
-        val failureEvent = eventLogger.get(1)
-        assertEquals(
-            "Expected SCREENSHOT_CAPTURE_FAILED UiEvent",
-            SCREENSHOT_CAPTURE_FAILED.id,
-            failureEvent.eventId
-        )
-        assertEquals(
-            "Expected supplied package name",
-            topComponent.packageName,
-            failureEvent.packageName
-        )
-    }
-
-    @Test
-    fun takeScreenshotFullScreen_multiDisplayFlagEnabled_takeScreenshotExecutor() {
-        flags.set(MULTI_DISPLAY_SCREENSHOT, true)
-        service = createService()
-
-        val request =
-            ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER)
-                .setTopComponent(topComponent)
-                .build()
-
-        service.handleRequest(request, { /* onSaved */}, callback)
-
-        verifyZeroInteractions(controller)
-        verify(takeScreenshotExecutor, times(1)).executeScreenshotsAsync(any(), any(), any())
-
-        assertEquals("Expected one UiEvent", 0, eventLogger.numLogs())
-    }
-
-    @Test
-    fun testServiceLifecycle_multiDisplayScreenshotFlagEnabled() {
-        flags.set(MULTI_DISPLAY_SCREENSHOT, true)
-        service = createService()
-
-        service.onCreate()
-        service.onBind(null /* unused: Intent */)
-
-        service.onUnbind(null /* unused: Intent */)
-        verify(takeScreenshotExecutor, times(1)).removeWindows()
-
-        service.onDestroy()
-        verify(takeScreenshotExecutor, times(1)).onDestroy()
-    }
-
-    @Test
-    fun constructor_MultiDisplayFlagOn_screenshotControllerNotCreated() {
-        flags.set(MULTI_DISPLAY_SCREENSHOT, true)
-        clearInvocations(controllerFactory)
-
-        service = createService()
-
-        verifyZeroInteractions(controllerFactory)
-    }
-
     private fun createService(): TakeScreenshotService {
         val service =
             TakeScreenshotService(
-                controllerFactory,
                 userManager,
                 devicePolicyManager,
                 eventLogger,
                 notificationsControllerFactory,
                 mContext,
                 Runnable::run,
-                flags,
-                requestProcessor,
-                { takeScreenshotExecutor },
+                executor
             )
+
         service.attach(
             mContext,
             /* thread = */ null,
             /* className = */ null,
             /* token = */ null,
-            application,
+            mock<Application>(),
             /* activityManager = */ null
         )
         return service
     }
 }
+
+internal class FakeRequestCallback : RequestCallback {
+    var errorReported = false
+    var finished = false
+    override fun reportError() {
+        errorReported = true
+    }
+
+    override fun onFinish() {
+        finished = true
+    }
+}
+
+internal class FakeScreenshotExecutor : TakeScreenshotExecutor {
+    var requestReceived: ScreenshotRequest? = null
+    var windowsPresent = true
+    var destroyed = false
+    override fun onCloseSystemDialogsReceived() {}
+    override suspend fun executeScreenshots(
+        screenshotRequest: ScreenshotRequest,
+        onSaved: (Uri) -> Unit,
+        requestCallback: RequestCallback,
+    ) {
+        requestReceived = screenshotRequest
+    }
+
+    override fun removeWindows() {
+        windowsPresent = false
+    }
+
+    override fun onDestroy() {
+        destroyed = true
+    }
+
+    override fun executeScreenshotsAsync(
+        screenshotRequest: ScreenshotRequest,
+        onSaved: Consumer<Uri>,
+        requestCallback: RequestCallback,
+    ) {
+        runBlocking {
+            executeScreenshots(screenshotRequest, { onSaved.accept(it) }, requestCallback)
+        }
+    }
+}
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 56e61e4..5b6da0e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -711,6 +711,7 @@
                 mFragmentService,
                 mStatusBarService,
                 mContentResolver,
+                mShadeHeaderController,
                 mScreenOffAnimationController,
                 mLockscreenGestureLogger,
                 mShadeExpansionStateManager,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java
index b16f412..ad4b4fd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java
@@ -35,6 +35,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.platform.test.annotations.EnableFlags;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.MotionEvent;
@@ -43,6 +44,7 @@
 
 import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.res.R;
+import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -285,16 +287,43 @@
     }
 
     @Test
-    public void updateQsState_fullscreenTrue() {
+    @EnableFlags(FooterViewRefactor.FLAG_NAME)
+    public void updateExpansion_partiallyExpanded_fullscreenFalse() {
+        // WHEN QS are only partially expanded
         mQsController.setExpanded(true);
-        mQsController.updateQsState();
+        when(mQs.getDesiredHeight()).thenReturn(123);
+        mQsController.setQs(mQs);
+        mQsController.onHeightChanged();
+        mQsController.setExpansionHeight(100);
+
+        // THEN they are not full screen
+        mQsController.updateExpansion();
+        assertThat(mShadeRepository.getLegacyQsFullscreen().getValue()).isFalse();
+    }
+
+    @Test
+    public void updateExpansion_fullyExpanded_fullscreenTrue() {
+        // WHEN QS are fully expanded
+        mQsController.setExpanded(true);
+        when(mQs.getDesiredHeight()).thenReturn(123);
+        mQsController.setQs(mQs);
+        mQsController.onHeightChanged();
+        mQsController.setExpansionHeight(123);
+
+        // THEN they are full screen
         assertThat(mShadeRepository.getLegacyQsFullscreen().getValue()).isTrue();
     }
 
     @Test
-    public void updateQsState_fullscreenFalse() {
+    public void updateExpansion_notExpanded_fullscreenFalse() {
+        // WHEN QS are not expanded
         mQsController.setExpanded(false);
-        mQsController.updateQsState();
+        when(mQs.getDesiredHeight()).thenReturn(123);
+        mQsController.setQs(mQs);
+        mQsController.onHeightChanged();
+        mQsController.setExpansionHeight(0);
+
+        // THEN they are not full screen
         assertThat(mShadeRepository.getLegacyQsFullscreen().getValue()).isFalse();
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
index a5f3f57..5abad61 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
@@ -38,6 +38,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.BcSmartspaceConfigPlugin
 import com.android.systemui.plugins.BcSmartspaceDataPlugin
@@ -180,6 +181,7 @@
     private lateinit var dateSmartspaceView: SmartspaceView
     private lateinit var weatherSmartspaceView: SmartspaceView
     private lateinit var smartspaceView: SmartspaceView
+    private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
 
     private val clock = FakeSystemClock()
     private val executor = FakeExecutor(clock)
@@ -225,6 +227,14 @@
         setAllowPrivateNotifications(userHandleSecondary, true)
         setShowNotifications(userHandlePrimary, true)
 
+        // Use the real wakefulness lifecycle instead of a mock
+        wakefulnessLifecycle = WakefulnessLifecycle(
+            context,
+            /* wallpaper= */ null,
+            clock,
+            dumpManager
+        )
+
         controller = LockscreenSmartspaceController(
                 context,
                 featureFlags,
@@ -240,6 +250,7 @@
                 deviceProvisionedController,
                 keyguardBypassController,
                 keyguardUpdateMonitor,
+                wakefulnessLifecycle,
                 dumpManager,
                 execution,
                 executor,
@@ -773,6 +784,38 @@
         verify(configurationController, never()).addCallback(any())
     }
 
+    @Test
+    fun testWakefulnessLifecycleDispatch_wake_setsSmartspaceScreenOnTrue() {
+        // Connect session
+        connectSession()
+
+        // Add mock views
+        val mockSmartspaceView = mock(SmartspaceView::class.java)
+        controller.smartspaceViews.add(mockSmartspaceView)
+
+        // Initiate wakefulness change
+        wakefulnessLifecycle.dispatchStartedWakingUp(0)
+
+        // Verify smartspace views receive screen on
+        verify(mockSmartspaceView).setScreenOn(true)
+    }
+
+    @Test
+    fun testWakefulnessLifecycleDispatch_sleep_setsSmartspaceScreenOnFalse() {
+        // Connect session
+        connectSession()
+
+        // Add mock views
+        val mockSmartspaceView = mock(SmartspaceView::class.java)
+        controller.smartspaceViews.add(mockSmartspaceView)
+
+        // Initiate wakefulness change
+        wakefulnessLifecycle.dispatchFinishedGoingToSleep()
+
+        // Verify smartspace views receive screen on
+        verify(mockSmartspaceView).setScreenOn(false)
+    }
+
     private fun connectSession() {
         val dateView = controller.buildAndConnectDateView(fakeParent)
         dateSmartspaceView = dateView as SmartspaceView
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 24195fe..4eb7daa 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
@@ -109,7 +109,6 @@
         testComponent.apply {
             keyguardRepository.setKeyguardShowing(true)
             keyguardRepository.setKeyguardOccluded(false)
-            deviceProvisioningRepository.setFactoryResetProtectionActive(false)
             powerRepository.updateWakefulness(
                 rawState = WakefulnessState.AWAKE,
                 lastWakeReason = WakeSleepReason.OTHER,
@@ -120,20 +119,6 @@
     }
 
     @Test
-    fun animationsEnabled_isFalse_whenFrpIsActive() =
-        testComponent.runTest {
-            deviceProvisioningRepository.setFactoryResetProtectionActive(true)
-            keyguardTransitionRepository.sendTransitionStep(
-                TransitionStep(
-                    transitionState = TransitionState.STARTED,
-                )
-            )
-            val animationsEnabled by collectLastValue(underTest.areContainerChangesAnimated)
-            runCurrent()
-            assertThat(animationsEnabled).isFalse()
-        }
-
-    @Test
     fun animationsEnabled_isFalse_whenDeviceAsleepAndNotPulsing() =
         testComponent.runTest {
             powerRepository.updateWakefulness(
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 c40401f..35b8493 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
@@ -117,7 +117,6 @@
     fun setup() {
         testComponent.apply {
             keyguardRepository.setKeyguardShowing(false)
-            deviceProvisioningRepository.setFactoryResetProtectionActive(false)
             powerRepository.updateWakefulness(
                 rawState = WakefulnessState.AWAKE,
                 lastWakeReason = WakeSleepReason.OTHER,
@@ -127,20 +126,6 @@
     }
 
     @Test
-    fun animationsEnabled_isFalse_whenFrpIsActive() =
-        testComponent.runTest {
-            deviceProvisioningRepository.setFactoryResetProtectionActive(true)
-            keyguardTransitionRepository.sendTransitionStep(
-                TransitionStep(
-                    transitionState = TransitionState.STARTED,
-                )
-            )
-            val animationsEnabled by collectLastValue(underTest.animationsEnabled)
-            runCurrent()
-            assertThat(animationsEnabled).isFalse()
-        }
-
-    @Test
     fun animationsEnabled_isFalse_whenDeviceAsleepAndNotPulsing() =
         testComponent.runTest {
             powerRepository.updateWakefulness(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index dd53474..ed29665 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -1035,15 +1035,6 @@
         verify(mStatusBarStateController).setState(SHADE);
     }
 
-    @Test
-    public void frpLockedDevice_shadeDisabled() {
-        when(mDeviceProvisionedController.isFrpActive()).thenReturn(true);
-        when(mDozeServiceHost.isPulsing()).thenReturn(true);
-        mCentralSurfaces.updateNotificationPanelTouchState();
-
-        verify(mNotificationPanelViewController).setTouchAndAnimationDisabled(true);
-    }
-
     /** Regression test for b/298355063 */
     @Test
     public void fingerprintManagerNull_noNPE() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImplTest.kt
index 361fa5b..31bd57e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceProvisionedControllerImplTest.kt
@@ -86,7 +86,6 @@
                 globalSettings,
                 userTracker,
                 dumpManager,
-                buildInfo,
                 Handler(testableLooper.looper),
                 mainExecutor
         )
@@ -99,12 +98,6 @@
     }
 
     @Test
-    fun testFrpNotActiveByDefault() {
-        init()
-        assertThat(controller.isFrpActive).isFalse()
-    }
-
-    @Test
     fun testNotUserSetupByDefault() {
         init()
         assertThat(controller.isUserSetup(START_USER)).isFalse()
@@ -119,14 +112,6 @@
     }
 
     @Test
-    fun testFrpActiveWhenCreated() {
-        globalSettings.putInt(Settings.Global.SECURE_FRP_MODE, 1)
-        init()
-
-        assertThat(controller.isFrpActive).isTrue()
-    }
-
-    @Test
     fun testUserSetupWhenCreated() {
         secureSettings.putIntForUser(Settings.Secure.USER_SETUP_COMPLETE, 1, START_USER)
         init()
@@ -145,16 +130,6 @@
     }
 
     @Test
-    fun testFrpActiveChange() {
-        init()
-
-        globalSettings.putInt(Settings.Global.SECURE_FRP_MODE, 1)
-        testableLooper.processAllMessages() // background observer
-
-        assertThat(controller.isFrpActive).isTrue()
-    }
-
-    @Test
     fun testUserSetupChange() {
         init()
 
@@ -197,7 +172,6 @@
         mainExecutor.runAllReady()
 
         verify(listener, never()).onDeviceProvisionedChanged()
-        verify(listener, never()).onFrpActiveChanged()
         verify(listener, never()).onUserSetupChanged()
         verify(listener, never()).onUserSwitched()
     }
@@ -215,7 +189,6 @@
         verify(listener).onUserSwitched()
         verify(listener, never()).onUserSetupChanged()
         verify(listener, never()).onDeviceProvisionedChanged()
-        verify(listener, never()).onFrpActiveChanged()
     }
 
     @Test
@@ -230,7 +203,6 @@
         verify(listener, never()).onUserSwitched()
         verify(listener).onUserSetupChanged()
         verify(listener, never()).onDeviceProvisionedChanged()
-        verify(listener, never()).onFrpActiveChanged()
     }
 
     @Test
@@ -244,26 +216,10 @@
 
         verify(listener, never()).onUserSwitched()
         verify(listener, never()).onUserSetupChanged()
-        verify(listener, never()).onFrpActiveChanged()
         verify(listener).onDeviceProvisionedChanged()
     }
 
     @Test
-    fun testListenerCalledOnFrpActiveChanged() {
-        init()
-        controller.addCallback(listener)
-
-        globalSettings.putInt(Settings.Global.SECURE_FRP_MODE, 1)
-        testableLooper.processAllMessages()
-        mainExecutor.runAllReady()
-
-        verify(listener, never()).onUserSwitched()
-        verify(listener, never()).onUserSetupChanged()
-        verify(listener, never()).onDeviceProvisionedChanged()
-        verify(listener).onFrpActiveChanged()
-    }
-
-    @Test
     fun testRemoveListener() {
         init()
         controller.addCallback(listener)
@@ -272,13 +228,11 @@
         switchUser(10)
         secureSettings.putIntForUser(Settings.Secure.USER_SETUP_COMPLETE, 1, START_USER)
         globalSettings.putInt(Settings.Global.DEVICE_PROVISIONED, 1)
-        globalSettings.putInt(Settings.Global.SECURE_FRP_MODE, 1)
 
         testableLooper.processAllMessages()
         mainExecutor.runAllReady()
 
         verify(listener, never()).onDeviceProvisionedChanged()
-        verify(listener, never()).onFrpActiveChanged()
         verify(listener, never()).onUserSetupChanged()
         verify(listener, never()).onUserSwitched()
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
index c259782..70afbd8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputViewTest.java
@@ -67,10 +67,10 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.logging.testing.UiEventLoggerFake;
 import com.android.systemui.Dependency;
-import com.android.systemui.res.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.AnimatorTestRule;
 import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.res.R;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -448,6 +448,34 @@
         assertEquals(1f, fadeInView.getAlpha());
     }
 
+    @Test
+    public void testUnanimatedFocusAfterDefocusAnimation() throws Exception {
+        NotificationTestHelper helper = new NotificationTestHelper(
+                mContext,
+                mDependency,
+                TestableLooper.get(this));
+        ExpandableNotificationRow row = helper.createRow();
+        RemoteInputView view = RemoteInputView.inflate(mContext, null, row.getEntry(), mController);
+        bindController(view, row.getEntry());
+
+        FrameLayout parent = new FrameLayout(mContext);
+        parent.addView(view);
+
+        // Play defocus animation
+        view.onDefocus(true /* animate */, false /* logClose */, null /* doAfterDefocus */);
+        mAnimatorTestRule.advanceTimeBy(ANIMATION_DURATION_STANDARD);
+
+        // assert that RemoteInputView is no longer visible, but alpha is reset to 1f
+        assertEquals(View.GONE, view.getVisibility());
+        assertEquals(1f, view.getAlpha());
+
+        // focus RemoteInputView without an animation
+        view.focus();
+        // assert that RemoteInputView is visible, and alpha is 1f
+        assertEquals(View.VISIBLE, view.getVisibility());
+        assertEquals(1f, view.getAlpha());
+    }
+
     // NOTE: because we're refactoring the RemoteInputView and moving logic into the
     // RemoteInputViewController, it's easiest to just test the system of the two classes together.
     @NonNull
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepositoryImplTest.kt
index 12694ae..21ed384 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/data/repository/DeviceProvisioningRepositoryImplTest.kt
@@ -78,31 +78,4 @@
             .onDeviceProvisionedChanged()
         assertThat(deviceProvisioned).isFalse()
     }
-
-    @Test
-    fun isFrpActive_reflectsCurrentControllerState() = runTest {
-        whenever(deviceProvisionedController.isFrpActive).thenReturn(true)
-        val frpActive by collectLastValue(underTest.isFactoryResetProtectionActive)
-        assertThat(frpActive).isTrue()
-    }
-
-    @Test
-    fun isFrpActive_updatesWhenControllerStateChanges_toTrue() = runTest {
-        val frpActive by collectLastValue(underTest.isFactoryResetProtectionActive)
-        runCurrent()
-        whenever(deviceProvisionedController.isFrpActive).thenReturn(true)
-        withArgCaptor { verify(deviceProvisionedController).addCallback(capture()) }
-            .onFrpActiveChanged()
-        assertThat(frpActive).isTrue()
-    }
-
-    @Test
-    fun isFrpActive_updatesWhenControllerStateChanges_toFalse() = runTest {
-        val frpActive by collectLastValue(underTest.isFactoryResetProtectionActive)
-        runCurrent()
-        whenever(deviceProvisionedController.isFrpActive).thenReturn(false)
-        withArgCaptor { verify(deviceProvisionedController).addCallback(capture()) }
-            .onFrpActiveChanged()
-        assertThat(frpActive).isFalse()
-    }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
index b91aafe..f856d27 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
@@ -62,6 +62,7 @@
         lockscreenToPrimaryBouncerTransitionViewModel =
             lockscreenToPrimaryBouncerTransitionViewModel,
         occludedToAodTransitionViewModel = occludedToAodTransitionViewModel,
+        occludedToDozingTransitionViewModel = occludedToDozingTransitionViewModel,
         occludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel,
         primaryBouncerToAodTransitionViewModel = primaryBouncerToAodTransitionViewModel,
         primaryBouncerToGoneTransitionViewModel = primaryBouncerToGoneTransitionViewModel,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToDozingTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToDozingTransitionViewModelKosmos.kt
new file mode 100644
index 0000000..a05e606
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToDozingTransitionViewModelKosmos.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 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.keyguard.ui.viewmodel
+
+import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+val Kosmos.occludedToDozingTransitionViewModel by Fixture {
+    OccludedToDozingTransitionViewModel(
+        animationFlow = keyguardTransitionAnimationFlow,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/startable/ShadeStartableKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/startable/ShadeStartableKosmos.kt
index 65e04f4..7dfa686 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/startable/ShadeStartableKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/startable/ShadeStartableKosmos.kt
@@ -22,9 +22,7 @@
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.log.LogBuffer
-import com.android.systemui.shade.ShadeHeaderController
 import com.android.systemui.shade.data.repository.shadeRepository
-import com.android.systemui.shade.shadeController
 import com.android.systemui.shade.transition.ScrimShadeTransitionController
 import com.android.systemui.statusbar.policy.splitShadeStateController
 import com.android.systemui.util.mockito.mock
@@ -37,8 +35,6 @@
         configurationRepository = configurationRepository,
         shadeRepository = shadeRepository,
         controller = splitShadeStateController,
-        shadeHeaderController = mock<ShadeHeaderController>(),
         scrimShadeTransitionController = mock<ScrimShadeTransitionController>(),
-        shadeController = shadeController
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeDeviceProvisionedController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeDeviceProvisionedController.kt
index ebcabf8..91b2956 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeDeviceProvisionedController.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeDeviceProvisionedController.kt
@@ -30,10 +30,6 @@
         return currentUser in usersSetup
     }
 
-    override fun isFrpActive(): Boolean {
-        TODO("Not yet implemented")
-    }
-
     fun setCurrentUser(userId: Int) {
         currentUser = userId
         callbacks.toSet().forEach { it.onUserSwitched() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt
index fc6a800..9247e88 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/data/repository/FakeDeviceProvisioningRepository.kt
@@ -26,14 +26,9 @@
 class FakeDeviceProvisioningRepository @Inject constructor() : DeviceProvisioningRepository {
     private val _isDeviceProvisioned = MutableStateFlow(true)
     override val isDeviceProvisioned: Flow<Boolean> = _isDeviceProvisioned
-    private val _isFactoryResetProtectionActive = MutableStateFlow(false)
-    override val isFactoryResetProtectionActive: Flow<Boolean> = _isFactoryResetProtectionActive
     fun setDeviceProvisioned(isProvisioned: Boolean) {
         _isDeviceProvisioned.value = isProvisioned
     }
-    fun setFactoryResetProtectionActive(isActive: Boolean) {
-        _isFactoryResetProtectionActive.value = isActive
-    }
 }
 
 @Module
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
index fb28055..1f65e15 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
@@ -37,6 +37,8 @@
 import android.annotation.UserIdInt;
 import android.app.PendingIntent;
 import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothManager;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -695,6 +697,13 @@
             throw new IllegalArgumentException(
                     bluetoothAddress + " is not a valid Bluetooth address");
         }
+        final BluetoothManager bluetoothManager =
+                mContext.getSystemService(BluetoothManager.class);
+        final String bluetoothDeviceName = bluetoothManager == null ? null :
+                bluetoothManager.getAdapter().getBondedDevices().stream()
+                        .filter(device -> device.getAddress().equalsIgnoreCase(bluetoothAddress))
+                        .map(BluetoothDevice::getName)
+                        .findFirst().orElse(null);
         synchronized (mLock) {
             checkAccessibilityAccessLocked();
             if (mBrailleDisplayConnection != null) {
@@ -706,7 +715,10 @@
                 connection.setTestData(mTestBrailleDisplays);
             }
             connection.connectLocked(
-                    bluetoothAddress, BrailleDisplayConnection.BUS_BLUETOOTH, controller);
+                    bluetoothAddress,
+                    bluetoothDeviceName,
+                    BrailleDisplayConnection.BUS_BLUETOOTH,
+                    controller);
         }
     }
 
@@ -763,7 +775,10 @@
                 connection.setTestData(mTestBrailleDisplays);
             }
             connection.connectLocked(
-                    usbSerialNumber, BrailleDisplayConnection.BUS_USB, controller);
+                    usbSerialNumber,
+                    usbDevice.getProductName(),
+                    BrailleDisplayConnection.BUS_USB,
+                    controller);
         }
     }
 
diff --git a/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java b/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java
index 8b41873..b0da3f0 100644
--- a/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java
@@ -24,6 +24,7 @@
 import android.accessibilityservice.IBrailleDisplayController;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.PermissionManuallyEnforced;
 import android.annotation.RequiresNoPermission;
 import android.bluetooth.BluetoothDevice;
@@ -33,6 +34,7 @@
 import android.os.IBinder;
 import android.os.Process;
 import android.os.RemoteException;
+import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Pair;
@@ -141,6 +143,8 @@
 
         @BusType
         int getDeviceBusType(@NonNull Path path);
+
+        String getName(@NonNull Path path);
     }
 
     /**
@@ -149,15 +153,19 @@
      * <p>If found, saves instance state for this connection and starts a thread to
      * read from the Braille display.
      *
-     * @param expectedUniqueId  The expected unique ID of the device to connect, from
-     *                          {@link UsbDevice#getSerialNumber()}
-     *                          or {@link BluetoothDevice#getAddress()}
-     * @param expectedBusType   The expected bus type from {@link BusType}.
-     * @param controller        Interface containing oneway callbacks used to communicate with the
-     *                          {@link android.accessibilityservice.BrailleDisplayController}.
+     * @param expectedUniqueId The expected unique ID of the device to connect, from
+     *                         {@link UsbDevice#getSerialNumber()} or
+     *                         {@link BluetoothDevice#getAddress()}.
+     * @param expectedName     The expected name of the device to connect, from
+     *                         {@link BluetoothDevice#getName()} or
+     *                         {@link UsbDevice#getProductName()}.
+     * @param expectedBusType  The expected bus type from {@link BusType}.
+     * @param controller       Interface containing oneway callbacks used to communicate with the
+     *                         {@link android.accessibilityservice.BrailleDisplayController}.
      */
     void connectLocked(
             @NonNull String expectedUniqueId,
+            @Nullable String expectedName,
             @BusType int expectedBusType,
             @NonNull IBrailleDisplayController controller) {
         Objects.requireNonNull(expectedUniqueId);
@@ -179,10 +187,20 @@
                 unableToGetDescriptor = true;
                 continue;
             }
+            final boolean matchesIdentifier;
             final String uniqueId = mScanner.getUniqueId(path);
+            if (uniqueId != null) {
+                matchesIdentifier = expectedUniqueId.equalsIgnoreCase(uniqueId);
+            } else {
+                // HIDIOCGRAWUNIQ was added in kernel version 5.7.
+                // If the device has an older kernel that does not support that ioctl then as a
+                // fallback we can check against the device name (from HIDIOCGRAWNAME).
+                final String name = mScanner.getName(path);
+                matchesIdentifier = !TextUtils.isEmpty(expectedName) && expectedName.equals(name);
+            }
             if (isBrailleDisplay(descriptor)
                     && mScanner.getDeviceBusType(path) == expectedBusType
-                    && expectedUniqueId.equalsIgnoreCase(uniqueId)) {
+                    && matchesIdentifier) {
                 result.add(Pair.create(path.toFile(), descriptor));
             }
         }
@@ -498,6 +516,12 @@
                 Integer busType = readFromFileDescriptor(path, nativeInterface::getHidrawBusType);
                 return busType != null ? busType : BUS_UNKNOWN;
             }
+
+            @Override
+            public String getName(@NonNull Path path) {
+                Objects.requireNonNull(path);
+                return readFromFileDescriptor(path, nativeInterface::getHidrawName);
+            }
         };
     }
 
@@ -542,6 +566,12 @@
                             BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH)
                             ? BUS_BLUETOOTH : BUS_USB;
                 }
+
+                @Override
+                public String getName(@NonNull Path path) {
+                    return brailleDisplayMap.get(path).getString(
+                            BrailleDisplayController.TEST_BRAILLE_DISPLAY_NAME);
+                }
             };
             return mScanner;
         }
@@ -579,6 +609,8 @@
          * @return the result of ioctl(HIDIOCGRAWINFO).bustype, or -1 if the ioctl fails.
          */
         int getHidrawBusType(int fd);
+
+        String getHidrawName(int fd);
     }
 
     /** Native interface that actually calls native HIDRAW ioctls. */
@@ -602,6 +634,11 @@
         public int getHidrawBusType(int fd) {
             return nativeGetHidrawBusType(fd);
         }
+
+        @Override
+        public String getHidrawName(int fd) {
+            return nativeGetHidrawName(fd);
+        }
     }
 
     private static native int nativeGetHidrawDescSize(int fd);
@@ -611,4 +648,6 @@
     private static native String nativeGetHidrawUniq(int fd);
 
     private static native int nativeGetHidrawBusType(int fd);
+
+    private static native String nativeGetHidrawName(int fd);
 }
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 993a1d5..558e07f 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -53,8 +53,10 @@
 import static android.view.autofill.AutofillManager.getSmartSuggestionModeToString;
 
 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+import static com.android.server.autofill.FillRequestEventLogger.TRIGGER_REASON_EXPLICITLY_REQUESTED;
 import static com.android.server.autofill.FillRequestEventLogger.TRIGGER_REASON_NORMAL_TRIGGER;
 import static com.android.server.autofill.FillRequestEventLogger.TRIGGER_REASON_PRE_TRIGGER;
+import static com.android.server.autofill.FillRequestEventLogger.TRIGGER_REASON_RETRIGGER;
 import static com.android.server.autofill.FillRequestEventLogger.TRIGGER_REASON_SERVED_FROM_CACHED_RESPONSE;
 import static com.android.server.autofill.FillResponseEventLogger.AVAILABLE_COUNT_WHEN_FILL_REQUEST_FAILED_OR_TIMEOUT;
 import static com.android.server.autofill.FillResponseEventLogger.DETECTION_PREFER_AUTOFILL_PROVIDER;
@@ -543,6 +545,8 @@
                     synchronized (mLock) {
                         int requestId = intent.getIntExtra(EXTRA_REQUEST_ID, 0);
                         FillResponse response = intent.getParcelableExtra(EXTRA_FILL_RESPONSE, android.service.autofill.FillResponse.class);
+                        mFillRequestEventLogger.maybeSetRequestTriggerReason(
+                                TRIGGER_REASON_RETRIGGER);
                         mAssistReceiver.processDelayedFillLocked(requestId, response);
                     }
                 }
@@ -1268,7 +1272,13 @@
         if(mPreviouslyFillDialogPotentiallyStarted) {
             mFillRequestEventLogger.maybeSetRequestTriggerReason(TRIGGER_REASON_PRE_TRIGGER);
         } else {
-            mFillRequestEventLogger.maybeSetRequestTriggerReason(TRIGGER_REASON_NORMAL_TRIGGER);
+            if ((flags & FLAG_MANUAL_REQUEST) != 0) {
+                mFillRequestEventLogger.maybeSetRequestTriggerReason(
+                        TRIGGER_REASON_EXPLICITLY_REQUESTED);
+            } else {
+                mFillRequestEventLogger.maybeSetRequestTriggerReason(
+                        TRIGGER_REASON_NORMAL_TRIGGER);
+            }
         }
         if (existingResponse != null) {
             setViewStatesLocked(
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index edf9fd1..2d59309 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -23,7 +23,6 @@
 import static android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED;
 import static android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE;
 import static android.Manifest.permission.USE_COMPANION_TRANSPORTS;
-import static android.companion.DevicePresenceEvent.EVENT_BT_CONNECTED;
 import static android.content.pm.PackageManager.CERT_INPUT_SHA256;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.os.Process.SYSTEM_UID;
@@ -53,7 +52,6 @@
 import android.app.AppOpsManager;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
-import android.bluetooth.BluetoothDevice;
 import android.companion.AssociationInfo;
 import android.companion.AssociationRequest;
 import android.companion.IAssociationRequestCallback;
@@ -76,7 +74,6 @@
 import android.os.Environment;
 import android.os.Parcel;
 import android.os.ParcelFileDescriptor;
-import android.os.ParcelUuid;
 import android.os.PowerExemptionManager;
 import android.os.PowerManagerInternal;
 import android.os.RemoteException;
@@ -118,9 +115,7 @@
 import java.io.File;
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
-import java.util.Arrays;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 
@@ -250,38 +245,9 @@
 
     @Override
     public void onUserUnlocked(@NonNull TargetUser user) {
+        Slog.i(TAG, "onUserUnlocked() user=" + user);
         // Notify and bind the app after the phone is unlocked.
-        final int userId = user.getUserIdentifier();
-        final Set<BluetoothDevice> blueToothDevices =
-                mDevicePresenceProcessor.getPendingConnectedDevices().get(userId);
-
-        final List<ObservableUuid> observableUuids =
-                mObservableUuidStore.getObservableUuidsForUser(userId);
-
-        if (blueToothDevices != null) {
-            for (BluetoothDevice bluetoothDevice : blueToothDevices) {
-                final ParcelUuid[] bluetoothDeviceUuids = bluetoothDevice.getUuids();
-
-                final List<ParcelUuid> deviceUuids = ArrayUtils.isEmpty(bluetoothDeviceUuids)
-                        ? Collections.emptyList() : Arrays.asList(bluetoothDeviceUuids);
-
-                for (AssociationInfo ai :
-                        mAssociationStore.getActiveAssociationsByAddress(
-                                bluetoothDevice.getAddress())) {
-                    Slog.i(TAG, "onUserUnlocked, device id( " + ai.getId() + " ) is connected");
-                    mDevicePresenceProcessor.onBluetoothCompanionDeviceConnected(ai.getId());
-                }
-
-                for (ObservableUuid observableUuid : observableUuids) {
-                    if (deviceUuids.contains(observableUuid.getUuid())) {
-                        Slog.i(TAG, "onUserUnlocked, UUID( "
-                                + observableUuid.getUuid() + " ) is connected");
-                        mDevicePresenceProcessor.onDevicePresenceEventByUuid(
-                                observableUuid, EVENT_BT_CONNECTED);
-                    }
-                }
-            }
-        }
+        mDevicePresenceProcessor.sendDevicePresenceEventOnUnlocked(user.getUserIdentifier());
     }
 
     private void onPackageRemoveOrDataClearedInternal(
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
index a789384..daa8fdb 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
@@ -80,25 +80,6 @@
         final int associationId;
 
         try {
-            if ("simulate-device-event".equals(cmd) && Flags.devicePresence()) {
-                associationId = getNextIntArgRequired();
-                int event = getNextIntArgRequired();
-                mDevicePresenceProcessor.simulateDeviceEvent(associationId, event);
-                return 0;
-            }
-
-            if ("simulate-device-uuid-event".equals(cmd) && Flags.devicePresence()) {
-                String uuid = getNextArgRequired();
-                String packageName = getNextArgRequired();
-                int userId = getNextIntArgRequired();
-                int event = getNextIntArgRequired();
-                ObservableUuid observableUuid = new ObservableUuid(
-                        userId, ParcelUuid.fromString(uuid), packageName,
-                        System.currentTimeMillis());
-                mDevicePresenceProcessor.simulateDeviceEventByUuid(observableUuid, event);
-                return 0;
-            }
-
             switch (cmd) {
                 case "list": {
                     final int userId = getNextIntArgRequired();
@@ -167,6 +148,51 @@
                     mDevicePresenceProcessor.simulateDeviceEvent(associationId, /* event */ 1);
                     break;
 
+                case "simulate-device-event": {
+                    if (Flags.devicePresence()) {
+                        associationId = getNextIntArgRequired();
+                        int event = getNextIntArgRequired();
+                        mDevicePresenceProcessor.simulateDeviceEvent(associationId, event);
+                    }
+                    break;
+                }
+
+                case "simulate-device-uuid-event": {
+                    if (Flags.devicePresence()) {
+                        String uuid = getNextArgRequired();
+                        String packageName = getNextArgRequired();
+                        int userId = getNextIntArgRequired();
+                        int event = getNextIntArgRequired();
+                        ObservableUuid observableUuid = new ObservableUuid(
+                                userId, ParcelUuid.fromString(uuid), packageName,
+                                System.currentTimeMillis());
+                        mDevicePresenceProcessor.simulateDeviceEventByUuid(observableUuid, event);
+                    }
+                    break;
+                }
+
+                case "simulate-device-event-device-locked": {
+                    if (Flags.devicePresence()) {
+                        associationId = getNextIntArgRequired();
+                        int userId = getNextIntArgRequired();
+                        int event = getNextIntArgRequired();
+                        String uuid = getNextArgRequired();
+                        ParcelUuid parcelUuid =
+                                uuid.equals("null") ? null : ParcelUuid.fromString(uuid);
+                        mDevicePresenceProcessor.simulateDeviceEventOnDeviceLocked(
+                                associationId, userId, event, parcelUuid);
+                    }
+                    break;
+                }
+
+                case "simulate-device-event-device-unlocked": {
+                    if (Flags.devicePresence()) {
+                        int userId = getNextIntArgRequired();
+                        mDevicePresenceProcessor.simulateDeviceEventOnUserUnlocked(userId);
+                    }
+                    break;
+                }
+
                 case "get-backup-payload": {
                     final int userId = getNextIntArgRequired();
                     byte[] payload = mBackupRestoreProcessor.getBackupPayload(userId);
@@ -478,6 +504,17 @@
             pw.println("      Make CDM act as if the given DEVICE is BT disconnected base"
                     + "on the UUID");
             pw.println("      USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY.");
+
+            pw.println("  simulate-device-event-device-locked"
+                    + " ASSOCIATION_ID USER_ID DEVICE_EVENT PARCEL_UUID");
+            pw.println("  Simulate device event when the device is locked");
+            pw.println("  USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY.");
+
+            pw.println("  simulate-device-event-device-unlocked USER_ID");
+            pw.println("  Simulate device unlocked for given user. This will send corresponding");
+            pw.println("  callback after simulate-device-event-device-locked");
+            pw.println("  command has been called.");
+            pw.println("  USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY.");
         }
 
         pw.println("  remove-inactive-associations");
diff --git a/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java b/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java
index 9c37881..407b9da 100644
--- a/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java
+++ b/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java
@@ -73,9 +73,9 @@
     private static final String TAG = "CDM_BleCompanionDeviceScanner";
 
     interface Callback {
-        void onBleCompanionDeviceFound(int associationId);
+        void onBleCompanionDeviceFound(int associationId, int userId);
 
-        void onBleCompanionDeviceLost(int associationId);
+        void onBleCompanionDeviceLost(int associationId, int userId);
     }
 
     private final @NonNull AssociationStore mAssociationStore;
@@ -259,7 +259,7 @@
         if (DEBUG) Log.d(TAG, "  > associations=" + Arrays.toString(associations.toArray()));
 
         for (AssociationInfo association : associations) {
-            mCallback.onBleCompanionDeviceFound(association.getId());
+            mCallback.onBleCompanionDeviceFound(association.getId(), association.getUserId());
         }
     }
 
@@ -272,7 +272,7 @@
         if (DEBUG) Log.d(TAG, "  > associations=" + Arrays.toString(associations.toArray()));
 
         for (AssociationInfo association : associations) {
-            mCallback.onBleCompanionDeviceLost(association.getId());
+            mCallback.onBleCompanionDeviceLost(association.getId(), association.getUserId());
         }
     }
 
diff --git a/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java b/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java
index 2d345c4..e1a8db4 100644
--- a/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java
+++ b/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java
@@ -34,20 +34,15 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.Log;
-import android.util.Slog;
-import android.util.SparseArray;
 
-import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.ArrayUtils;
 import com.android.server.companion.association.AssociationStore;
 
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 
 @SuppressLint("LongLogTag")
 public class BluetoothCompanionDeviceConnectionListener
@@ -56,14 +51,13 @@
     private static final String TAG = "CDM_BluetoothCompanionDeviceConnectionListener";
 
     interface Callback {
-        void onBluetoothCompanionDeviceConnected(int associationId);
+        void onBluetoothCompanionDeviceConnected(int associationId, int userId);
 
-        void onBluetoothCompanionDeviceDisconnected(int associationId);
+        void onBluetoothCompanionDeviceDisconnected(int associationId, int userId);
 
         void onDevicePresenceEventByUuid(ObservableUuid uuid, int event);
     }
 
-    private final UserManager mUserManager;
     private final @NonNull AssociationStore mAssociationStore;
     private final @NonNull Callback mCallback;
     /** A set of ALL connected BT device (not only companion.) */
@@ -71,21 +65,12 @@
 
     private final @NonNull ObservableUuidStore mObservableUuidStore;
 
-    /**
-     * A structure hold the connected BT devices that are pending to be reported to the companion
-     * app when the user unlocks the local device per userId.
-     */
-    @GuardedBy("mPendingConnectedDevices")
-    @NonNull
-    final SparseArray<Set<BluetoothDevice>> mPendingConnectedDevices = new SparseArray<>();
-
     BluetoothCompanionDeviceConnectionListener(UserManager userManager,
             @NonNull AssociationStore associationStore,
             @NonNull ObservableUuidStore observableUuidStore, @NonNull Callback callback) {
         mAssociationStore = associationStore;
         mObservableUuidStore = observableUuidStore;
         mCallback = callback;
-        mUserManager = userManager;
     }
 
     public void init(@NonNull BluetoothAdapter btAdapter) {
@@ -111,19 +96,8 @@
             if (DEBUG) Log.w(TAG, "Device " + btDeviceToString(device) + " is already connected.");
             return;
         }
-        // Try to bind and notify the app after the phone is unlocked.
-        if (!mUserManager.isUserUnlockingOrUnlocked(UserHandle.myUserId())) {
-            Slog.i(TAG, "Current user is not in unlocking or unlocked stage yet. Notify "
-                        + "the application when the phone is unlocked");
-            synchronized (mPendingConnectedDevices) {
-                Set<BluetoothDevice> bluetoothDevices = mPendingConnectedDevices.get(
-                        userId, new HashSet<>());
-                bluetoothDevices.add(device);
-                mPendingConnectedDevices.put(userId, bluetoothDevices);
-            }
-        } else {
-            onDeviceConnectivityChanged(device, true);
-        }
+
+        onDeviceConnectivityChanged(device, true);
     }
 
     /**
@@ -149,19 +123,6 @@
             return;
         }
 
-        // Do not need to report the connectivity since the user is not unlock the phone so
-        // that cdm is not bind with the app yet.
-        if (!mUserManager.isUserUnlockingOrUnlocked(userId)) {
-            synchronized (mPendingConnectedDevices) {
-                Set<BluetoothDevice> bluetoothDevices = mPendingConnectedDevices.get(userId);
-                if (bluetoothDevices != null) {
-                    bluetoothDevices.remove(device);
-                }
-            }
-
-            return;
-        }
-
         onDeviceConnectivityChanged(device, false);
     }
 
@@ -190,16 +151,15 @@
             if (!association.isNotifyOnDeviceNearby()) continue;
             final int id = association.getId();
             if (connected) {
-                mCallback.onBluetoothCompanionDeviceConnected(id);
+                mCallback.onBluetoothCompanionDeviceConnected(id, association.getUserId());
             } else {
-                mCallback.onBluetoothCompanionDeviceDisconnected(id);
+                mCallback.onBluetoothCompanionDeviceDisconnected(id, association.getUserId());
             }
         }
 
         for (ObservableUuid uuid : observableUuids) {
             if (deviceUuids.contains(uuid.getUuid())) {
-                mCallback.onDevicePresenceEventByUuid(
-                        uuid, connected ? EVENT_BT_CONNECTED
+                mCallback.onDevicePresenceEventByUuid(uuid, connected ? EVENT_BT_CONNECTED
                                 : EVENT_BT_DISCONNECTED);
             }
         }
@@ -210,7 +170,8 @@
         if (DEBUG) Log.d(TAG, "onAssociation_Added() " + association);
 
         if (mAllConnectedDevices.containsKey(association.getDeviceMacAddress())) {
-            mCallback.onBluetoothCompanionDeviceConnected(association.getId());
+            mCallback.onBluetoothCompanionDeviceConnected(
+                    association.getId(), association.getUserId());
         }
     }
 
diff --git a/services/companion/java/com/android/server/companion/presence/DevicePresenceProcessor.java b/services/companion/java/com/android/server/companion/presence/DevicePresenceProcessor.java
index 642460e..092886c 100644
--- a/services/companion/java/com/android/server/companion/presence/DevicePresenceProcessor.java
+++ b/services/companion/java/com/android/server/companion/presence/DevicePresenceProcessor.java
@@ -35,7 +35,6 @@
 import android.annotation.TestApi;
 import android.annotation.UserIdInt;
 import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
 import android.companion.AssociationInfo;
 import android.companion.DeviceNotAssociatedException;
 import android.companion.DevicePresenceEvent;
@@ -59,8 +58,10 @@
 import com.android.server.companion.association.AssociationStore;
 
 import java.io.PrintWriter;
+import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Objects;
 import java.util.Set;
 
 /**
@@ -97,6 +98,8 @@
     private final BleCompanionDeviceScanner mBleScanner;
     @NonNull
     private final PowerManagerInternal mPowerManagerInternal;
+    @NonNull
+    private final UserManager mUserManager;
 
     // NOTE: Same association may appear in more than one of the following sets at the same time.
     // (E.g. self-managed devices that have MAC addresses, could be reported as present by their
@@ -129,6 +132,14 @@
     private final BleDeviceDisappearedScheduler mBleDeviceDisappearedScheduler =
             new BleDeviceDisappearedScheduler();
 
+    /**
+     * A structure hold the DevicePresenceEvents that are pending to be reported to the companion
+     * app when the user unlocks the local device per userId.
+     */
+    @GuardedBy("mPendingDevicePresenceEvents")
+    public final SparseArray<List<DevicePresenceEvent>> mPendingDevicePresenceEvents =
+            new SparseArray<>();
+
     public DevicePresenceProcessor(@NonNull Context context,
             @NonNull CompanionAppBinder companionAppBinder,
             UserManager userManager,
@@ -139,6 +150,7 @@
         mCompanionAppBinder = companionAppBinder;
         mAssociationStore = associationStore;
         mObservableUuidStore = observableUuidStore;
+        mUserManager = userManager;
         mBtConnectionListener = new BluetoothCompanionDeviceConnectionListener(userManager,
                 associationStore, mObservableUuidStore,
                 /* BluetoothCompanionDeviceConnectionListener.Callback */ this);
@@ -461,7 +473,14 @@
     }
 
     @Override
-    public void onBluetoothCompanionDeviceConnected(int associationId) {
+    public void onBluetoothCompanionDeviceConnected(int associationId, int userId) {
+        Slog.i(TAG, "onBluetoothCompanionDeviceConnected: "
+                + "associationId( " + associationId + " )");
+        if (!mUserManager.isUserUnlockingOrUnlocked(userId)) {
+            onDeviceLocked(associationId, userId, EVENT_BT_CONNECTED, /* ParcelUuid */ null);
+            return;
+        }
+
         synchronized (mBtDisconnectedDevices) {
             // A device is considered reconnected within 10 seconds if a pending BLE lost report is
             // followed by a detected Bluetooth connection.
@@ -484,9 +503,15 @@
     }
 
     @Override
-    public void onBluetoothCompanionDeviceDisconnected(int associationId) {
+    public void onBluetoothCompanionDeviceDisconnected(int associationId, int userId) {
         Slog.i(TAG, "onBluetoothCompanionDeviceDisconnected "
                 + "associationId( " + associationId + " )");
+
+        if (!mUserManager.isUserUnlockingOrUnlocked(userId)) {
+            onDeviceLocked(associationId, userId, EVENT_BT_DISCONNECTED, /* ParcelUuid */ null);
+            return;
+        }
+
         // Start BLE scanning when the device is disconnected.
         mBleScanner.startScan();
 
@@ -503,7 +528,13 @@
 
 
     @Override
-    public void onBleCompanionDeviceFound(int associationId) {
+    public void onBleCompanionDeviceFound(int associationId, int userId) {
+        Slog.i(TAG, "onBleCompanionDeviceFound " + "associationId( " + associationId + " )");
+        if (!mUserManager.isUserUnlockingOrUnlocked(userId)) {
+            onDeviceLocked(associationId, userId, EVENT_BLE_APPEARED, /* ParcelUuid */ null);
+            return;
+        }
+
         onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_APPEARED);
         synchronized (mBtDisconnectedDevices) {
             final boolean isCurrentPresent = mBtDisconnectedDevicesBlePresence.get(associationId);
@@ -514,7 +545,13 @@
     }
 
     @Override
-    public void onBleCompanionDeviceLost(int associationId) {
+    public void onBleCompanionDeviceLost(int associationId, int userId) {
+        Slog.i(TAG, "onBleCompanionDeviceLost " + "associationId( " + associationId + " )");
+        if (!mUserManager.isUserUnlockingOrUnlocked(userId)) {
+            onDeviceLocked(associationId, userId, EVENT_BLE_APPEARED, /* ParcelUuid */ null);
+            return;
+        }
+
         onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_DISAPPEARED);
     }
 
@@ -529,18 +566,20 @@
         // Make sure the association exists.
         enforceAssociationExists(associationId);
 
+        final AssociationInfo associationInfo = mAssociationStore.getAssociationById(associationId);
+
         switch (event) {
             case EVENT_BLE_APPEARED:
                 simulateDeviceAppeared(associationId, event);
                 break;
             case EVENT_BT_CONNECTED:
-                onBluetoothCompanionDeviceConnected(associationId);
+                onBluetoothCompanionDeviceConnected(associationId, associationInfo.getUserId());
                 break;
             case EVENT_BLE_DISAPPEARED:
                 simulateDeviceDisappeared(associationId, event);
                 break;
             case EVENT_BT_DISCONNECTED:
-                onBluetoothCompanionDeviceDisconnected(associationId);
+                onBluetoothCompanionDeviceDisconnected(associationId, associationInfo.getUserId());
                 break;
             default:
                 throw new IllegalArgumentException("Event: " + event + "is not supported");
@@ -558,6 +597,29 @@
         onDevicePresenceEventByUuid(uuid, event);
     }
 
+    /** FOR DEBUGGING AND/OR TESTING PURPOSES ONLY. */
+    @TestApi
+    public void simulateDeviceEventOnDeviceLocked(
+            int associationId, int userId, int event, ParcelUuid uuid) {
+        // IMPORTANT: this API should only be invoked via the
+        // 'companiondevice simulate-device-event-device-locked' Shell command,
+        // so the only uid-s allowed to make this call are SHELL and ROOT.
+        // No other caller (including SYSTEM!) should be allowed.
+        enforceCallerShellOrRoot();
+        onDeviceLocked(associationId, userId, event, uuid);
+    }
+
+    /** FOR DEBUGGING AND/OR TESTING PURPOSES ONLY. */
+    @TestApi
+    public void simulateDeviceEventOnUserUnlocked(int userId) {
+        // IMPORTANT: this API should only be invoked via the
+        // 'companiondevice simulate-device-event-device-unlocked' Shell command,
+        // so the only uid-s allowed to make this call are SHELL and ROOT.
+        // No other caller (including SYSTEM!) should be allowed.
+        enforceCallerShellOrRoot();
+        sendDevicePresenceEventOnUnlocked(userId);
+    }
+
     private void simulateDeviceAppeared(int associationId, int state) {
         onDevicePresenceEvent(mSimulated, associationId, state);
         mSchedulerHelper.scheduleOnDeviceGoneCallForSimulatedDevicePresence(associationId);
@@ -660,8 +722,13 @@
                 + "]...");
 
         final ParcelUuid parcelUuid = uuid.getUuid();
-        final String packageName = uuid.getPackageName();
         final int userId = uuid.getUserId();
+        if (!mUserManager.isUserUnlockingOrUnlocked(userId)) {
+            onDeviceLocked(/* associationId */ -1, userId, eventType, parcelUuid);
+            return;
+        }
+
+        final String packageName = uuid.getPackageName();
         final DevicePresenceEvent event = new DevicePresenceEvent(NO_ASSOCIATION, eventType,
                 parcelUuid);
 
@@ -882,14 +949,6 @@
         // what's needed.
     }
 
-    /**
-     * Return a set of devices that pending to report connectivity
-     */
-    public SparseArray<Set<BluetoothDevice>> getPendingConnectedDevices() {
-        synchronized (mBtConnectionListener.mPendingConnectedDevices) {
-            return mBtConnectionListener.mPendingConnectedDevices;
-        }
-    }
 
     private static void enforceCallerShellOrRoot() {
         final int callingUid = Binder.getCallingUid();
@@ -919,6 +978,114 @@
     }
 
     /**
+     * Store the positive DevicePresenceEvent in the cache if the current device is still
+     * locked.
+     * Remove the current DevicePresenceEvent if there's a negative event occurs.
+     */
+    private void onDeviceLocked(int associationId, int userId, int event, ParcelUuid uuid) {
+        switch (event) {
+            case EVENT_BLE_APPEARED, EVENT_BT_CONNECTED -> {
+                // Try to bind and notify the app after the phone is unlocked.
+                Slog.i(TAG, "Current user is not in unlocking or unlocked stage yet. "
+                        + "Notify the application when the phone is unlocked");
+                synchronized (mPendingDevicePresenceEvents) {
+                    final DevicePresenceEvent devicePresenceEvent = new DevicePresenceEvent(
+                            associationId, event, uuid);
+                    List<DevicePresenceEvent> deviceEvents = mPendingDevicePresenceEvents.get(
+                            userId, new ArrayList<>());
+                    deviceEvents.add(devicePresenceEvent);
+                    mPendingDevicePresenceEvents.put(userId, deviceEvents);
+                }
+            }
+            case EVENT_BLE_DISAPPEARED -> {
+                synchronized (mPendingDevicePresenceEvents) {
+                    List<DevicePresenceEvent> deviceEvents = mPendingDevicePresenceEvents
+                            .get(userId);
+                    if (deviceEvents != null) {
+                        deviceEvents.removeIf(deviceEvent ->
+                                deviceEvent.getEvent() == EVENT_BLE_APPEARED
+                                && Objects.equals(deviceEvent.getUuid(), uuid)
+                                && deviceEvent.getAssociationId() == associationId);
+                    }
+                }
+            }
+            case EVENT_BT_DISCONNECTED -> {
+                // Do not need to report the event since the user is not unlock the
+                // phone so that cdm is not bind with the app yet.
+                synchronized (mPendingDevicePresenceEvents) {
+                    List<DevicePresenceEvent> deviceEvents = mPendingDevicePresenceEvents
+                            .get(userId);
+                    if (deviceEvents != null) {
+                        deviceEvents.removeIf(deviceEvent ->
+                                deviceEvent.getEvent() == EVENT_BT_CONNECTED
+                                && Objects.equals(deviceEvent.getUuid(), uuid)
+                                && deviceEvent.getAssociationId() == associationId);
+                    }
+                }
+            }
+            default -> Slog.e(TAG, "Event: " + event + "is not supported");
+        }
+    }
+
+    /**
+     * Send the device presence event by userID when the device is unlocked.
+     */
+    public void sendDevicePresenceEventOnUnlocked(int userId) {
+        final List<DevicePresenceEvent> deviceEvents = getPendingDevicePresenceEventsByUserId(
+                userId);
+        final List<ObservableUuid> observableUuids =
+                mObservableUuidStore.getObservableUuidsForUser(userId);
+        // Notify and bind the app after the phone is unlocked.
+        for (DevicePresenceEvent deviceEvent : deviceEvents) {
+            boolean isUuid = deviceEvent.getUuid() != null;
+            if (isUuid) {
+                for (ObservableUuid uuid : observableUuids) {
+                    if (uuid.getUuid().equals(deviceEvent.getUuid())) {
+                        onDevicePresenceEventByUuid(uuid, EVENT_BT_CONNECTED);
+                    }
+                }
+            } else {
+                int event = deviceEvent.getEvent();
+                int associationId = deviceEvent.getAssociationId();
+                final AssociationInfo associationInfo = mAssociationStore.getAssociationById(
+                        associationId);
+
+                if (associationInfo == null) {
+                    return;
+                }
+
+                switch(event) {
+                    case EVENT_BLE_APPEARED:
+                        onBleCompanionDeviceFound(
+                                associationInfo.getId(), associationInfo.getUserId());
+                        break;
+                    case EVENT_BT_CONNECTED:
+                        onBluetoothCompanionDeviceConnected(
+                                associationInfo.getId(), associationInfo.getUserId());
+                        break;
+                    default:
+                        Slog.e(TAG, "Event: " + event + "is not supported");
+                        break;
+                }
+            }
+        }
+
+        clearPendingDevicePresenceEventsByUserId(userId);
+    }
+
+    private List<DevicePresenceEvent> getPendingDevicePresenceEventsByUserId(int userId) {
+        synchronized (mPendingDevicePresenceEvents) {
+            return mPendingDevicePresenceEvents.get(userId, new ArrayList<>());
+        }
+    }
+
+    private void clearPendingDevicePresenceEventsByUserId(int userId) {
+        synchronized (mPendingDevicePresenceEvents) {
+            mPendingDevicePresenceEvents.get(userId).clear();
+        }
+    }
+
+    /**
      * Dumps system information about devices that are marked as "present".
      */
     public void dump(@NonNull PrintWriter out) {
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 3ec6e47..8b98d12 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -272,7 +272,8 @@
                 params,
                 DisplayManagerGlobal.getInstance(),
                 isVirtualCameraEnabled()
-                        ? new VirtualCameraController(params.getDevicePolicy(POLICY_TYPE_CAMERA))
+                        ? new VirtualCameraController(
+                                params.getDevicePolicy(POLICY_TYPE_CAMERA), deviceId)
                         : null);
     }
 
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 3bb1e33..4547bd6 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
@@ -58,19 +58,21 @@
     @Nullable private IVirtualCameraService mVirtualCameraService;
     @DevicePolicy
     private final int mCameraPolicy;
+    private final int mDeviceId;
 
     @GuardedBy("mCameras")
     private final Map<IBinder, CameraDescriptor> mCameras = new ArrayMap<>();
 
-    public VirtualCameraController(@DevicePolicy int cameraPolicy) {
-        this(/* virtualCameraService= */ null, cameraPolicy);
+    public VirtualCameraController(@DevicePolicy int cameraPolicy, int deviceId) {
+        this(/* virtualCameraService= */ null, cameraPolicy, deviceId);
     }
 
     @VisibleForTesting
     VirtualCameraController(IVirtualCameraService virtualCameraService,
-            @DevicePolicy int cameraPolicy) {
+            @DevicePolicy int cameraPolicy, int deviceId) {
         mVirtualCameraService = virtualCameraService;
         mCameraPolicy = cameraPolicy;
+        mDeviceId = deviceId;
     }
 
     /**
@@ -251,7 +253,7 @@
         VirtualCameraConfiguration serviceConfiguration = getServiceCameraConfiguration(config);
         synchronized (mServiceLock) {
             return mVirtualCameraService.registerCamera(config.getCallback().asBinder(),
-                    serviceConfiguration);
+                    serviceConfiguration, mDeviceId);
         }
     }
 
diff --git a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
index e3f16ae..253fe35 100644
--- a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
+++ b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
@@ -20,6 +20,11 @@
 import static android.provider.Settings.Global.DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS;
 import static android.view.flags.Flags.sensitiveContentAppProtection;
 
+import static com.android.internal.util.FrameworkStatsLog.SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION;
+import static com.android.internal.util.FrameworkStatsLog.SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION__SOURCE__FRAMEWORKS;
+import static com.android.internal.util.FrameworkStatsLog.SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION__STATE__START;
+import static com.android.internal.util.FrameworkStatsLog.SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION__STATE__STOP;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.ComponentName;
@@ -86,9 +91,11 @@
     private static class MediaProjectionSession {
         final int mUid;
         final long mSessionId;
+        final boolean mIsExempted;
 
-        MediaProjectionSession(int uid, long sessionId) {
+        MediaProjectionSession(int uid, boolean isExempted, long sessionId) {
             mUid = uid;
+            mIsExempted = isExempted;
             mSessionId = sessionId;
         }
     }
@@ -105,11 +112,28 @@
                     } finally {
                         Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
                     }
+                    FrameworkStatsLog.write(
+                            SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION,
+                            mMediaProjectionSession.mSessionId,
+                            mMediaProjectionSession.mUid,
+                            mMediaProjectionSession.mIsExempted,
+                            SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION__STATE__START,
+                            SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION__SOURCE__FRAMEWORKS
+                    );
                 }
 
                 @Override
                 public void onStop(MediaProjectionInfo info) {
                     if (DEBUG) Log.d(TAG, "onStop projection: " + info);
+                    FrameworkStatsLog.write(
+                            SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION,
+                            mMediaProjectionSession.mSessionId,
+                            mMediaProjectionSession.mUid,
+                            mMediaProjectionSession.mIsExempted,
+                            SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION__STATE__STOP,
+                            SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION__SOURCE__FRAMEWORKS
+                    );
+
                     Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER,
                             "SensitiveContentProtectionManagerService.onProjectionStop");
                     try {
@@ -207,28 +231,25 @@
     }
 
     private void onProjectionStart(MediaProjectionInfo projectionInfo) {
-        // exempt on device screen recorder as well.
-        if ((mExemptedPackages != null && mExemptedPackages.contains(
+        int uid = mPackageManagerInternal.getPackageUid(projectionInfo.getPackageName(), 0,
+                projectionInfo.getUserHandle().getIdentifier());
+        boolean isPackageExempted = (mExemptedPackages != null && mExemptedPackages.contains(
                 projectionInfo.getPackageName()))
-                || canRecordSensitiveContent(projectionInfo.getPackageName())) {
-            Log.w(TAG, projectionInfo.getPackageName() + " is exempted.");
-            return;
-        }
+                || canRecordSensitiveContent(projectionInfo.getPackageName());
         // TODO(b/324447419): move GlobalSettings lookup to background thread
-        boolean disableScreenShareProtections =
-                Settings.Global.getInt(getContext().getContentResolver(),
-                        DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS, 0) != 0;
-        if (disableScreenShareProtections) {
-            Log.w(TAG, "Screen share protections disabled, ignoring projection start");
+        boolean isFeatureDisabled = Settings.Global.getInt(getContext().getContentResolver(),
+                DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS, 0) != 0;
+        mMediaProjectionSession = new MediaProjectionSession(
+                uid, isPackageExempted || isFeatureDisabled, new Random().nextLong());
+
+        if (isPackageExempted || isFeatureDisabled) {
+            Log.w(TAG, "projection session is exempted, package ="
+                    + projectionInfo.getPackageName() + ", isFeatureDisabled=" + isFeatureDisabled);
             return;
         }
 
         synchronized (mSensitiveContentProtectionLock) {
             mProjectionActive = true;
-            int uid = mPackageManagerInternal.getPackageUid(projectionInfo.getPackageName(), 0,
-                    projectionInfo.getUserHandle().getIdentifier());
-            // TODO review sessionId, whether to use a sequence generator or random is good?
-            mMediaProjectionSession = new MediaProjectionSession(uid, new Random().nextLong());
             if (sensitiveNotificationAppProtection()) {
                 updateAppsThatShouldBlockScreenCapture();
             }
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 04dd2f3..90a9d1b 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -2659,6 +2659,9 @@
                         }
                         updateNumForegroundServicesLocked();
                     }
+
+                    maybeUpdateShortFgsTrackingLocked(r,
+                            extendShortServiceTimeout);
                     // Even if the service is already a FGS, we need to update the notification,
                     // so we need to call it again.
                     signalForegroundServiceObserversLocked(r);
@@ -2670,8 +2673,6 @@
                     mAm.notifyPackageUse(r.serviceInfo.packageName,
                             PackageManager.NOTIFY_PACKAGE_USE_FOREGROUND_SERVICE);
 
-                    maybeUpdateShortFgsTrackingLocked(r,
-                            extendShortServiceTimeout);
                     maybeUpdateFgsTrackingLocked(r, extendFgsTimeout);
                 } else {
                     if (DEBUG_FOREGROUND_SERVICE) {
@@ -4158,7 +4159,7 @@
                         || (callerApp.mState.getCurProcState() <= PROCESS_STATE_TOP
                             && c.hasFlag(Context.BIND_TREAT_LIKE_ACTIVITY)),
                         b.client);
-                if (!s.mOomAdjBumpedInExec && (serviceBindingOomAdjPolicy
+                if (!s.wasOomAdjUpdated() && (serviceBindingOomAdjPolicy
                         & SERVICE_BIND_OOMADJ_POLICY_SKIP_OOM_UPDATE_ON_CONNECT) == 0) {
                     needOomAdj = true;
                     mAm.enqueueOomAdjTargetLocked(s.app);
@@ -4308,7 +4309,7 @@
                 }
 
                 serviceDoneExecutingLocked(r, mDestroyingServices.contains(r), false, false,
-                        !Flags.serviceBindingOomAdjPolicy() || r.mOomAdjBumpedInExec
+                        !Flags.serviceBindingOomAdjPolicy() || r.wasOomAdjUpdated()
                         ? OOM_ADJ_REASON_EXECUTING_SERVICE : OOM_ADJ_REASON_NONE);
             }
         } finally {
@@ -4456,7 +4457,7 @@
                 }
 
                 serviceDoneExecutingLocked(r, inDestroying, false, false,
-                        !Flags.serviceBindingOomAdjPolicy() || r.mOomAdjBumpedInExec
+                        !Flags.serviceBindingOomAdjPolicy() || r.wasOomAdjUpdated()
                         ? OOM_ADJ_REASON_UNBIND_SERVICE : OOM_ADJ_REASON_NONE);
             }
         } finally {
@@ -5004,13 +5005,16 @@
                 }
             }
         }
-        if (oomAdjReason != OOM_ADJ_REASON_NONE && r.app != null
+        if (r.app != null
                 && r.app.mState.getCurProcState() > ActivityManager.PROCESS_STATE_SERVICE) {
-            // Force an immediate oomAdjUpdate, so the client app could be in the correct process
-            // state before doing any service related transactions
+            // Enqueue the oom adj target anyway for opportunistic oom adj updates.
             mAm.enqueueOomAdjTargetLocked(r.app);
-            mAm.updateOomAdjPendingTargetsLocked(oomAdjReason);
-            r.mOomAdjBumpedInExec = true;
+            r.updateOomAdjSeq();
+            if (oomAdjReason != OOM_ADJ_REASON_NONE) {
+                // Force an immediate oomAdjUpdate, so the client app could be in the correct
+                // process state before doing any service related transactions
+                mAm.updateOomAdjPendingTargetsLocked(oomAdjReason);
+            }
         }
         r.executeFg |= fg;
         r.executeNesting++;
@@ -5050,7 +5054,7 @@
                 if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "Crashed while binding " + r, e);
                 final boolean inDestroying = mDestroyingServices.contains(r);
                 serviceDoneExecutingLocked(r, inDestroying, inDestroying, false,
-                        !Flags.serviceBindingOomAdjPolicy() || r.mOomAdjBumpedInExec
+                        !Flags.serviceBindingOomAdjPolicy() || r.wasOomAdjUpdated()
                         ? OOM_ADJ_REASON_UNBIND_SERVICE : OOM_ADJ_REASON_NONE);
                 throw e;
             } catch (RemoteException e) {
@@ -5058,7 +5062,7 @@
                 // Keep the executeNesting count accurate.
                 final boolean inDestroying = mDestroyingServices.contains(r);
                 serviceDoneExecutingLocked(r, inDestroying, inDestroying, false,
-                        !Flags.serviceBindingOomAdjPolicy() || r.mOomAdjBumpedInExec
+                        !Flags.serviceBindingOomAdjPolicy() || r.wasOomAdjUpdated()
                         ? OOM_ADJ_REASON_UNBIND_SERVICE : OOM_ADJ_REASON_NONE);
                 return false;
             }
@@ -5854,8 +5858,8 @@
             // Force an immediate oomAdjUpdate, so the host app could be in the correct
             // process state before doing any service related transactions
             mAm.enqueueOomAdjTargetLocked(app);
+            r.updateOomAdjSeq();
             mAm.updateOomAdjPendingTargetsLocked(OOM_ADJ_REASON_START_SERVICE);
-            r.mOomAdjBumpedInExec = true;
         } else {
             // Since we skipped the oom adj update, the Service#onCreate() might be running in
             // the cached state, if the service process drops into the cached state after the call.
@@ -5896,7 +5900,7 @@
                 // Keep the executeNesting count accurate.
                 final boolean inDestroying = mDestroyingServices.contains(r);
                 serviceDoneExecutingLocked(r, inDestroying, inDestroying, false,
-                        !Flags.serviceBindingOomAdjPolicy() || r.mOomAdjBumpedInExec
+                        !Flags.serviceBindingOomAdjPolicy() || r.wasOomAdjUpdated()
                         ? OOM_ADJ_REASON_STOP_SERVICE : OOM_ADJ_REASON_NONE);
 
                 // Cleanup.
@@ -5932,7 +5936,7 @@
                     null, null, 0, null, null, ActivityManager.PROCESS_STATE_UNKNOWN));
         }
 
-        sendServiceArgsLocked(r, execInFg, r.mOomAdjBumpedInExec);
+        sendServiceArgsLocked(r, execInFg, r.wasOomAdjUpdated());
 
         if (r.delayed) {
             if (DEBUG_DELAYED_STARTS) Slog.v(TAG_SERVICE, "REM FR DELAY LIST (new proc): " + r);
@@ -6119,7 +6123,7 @@
             }
         }
 
-        boolean oomAdjusted = Flags.serviceBindingOomAdjPolicy() && r.mOomAdjBumpedInExec;
+        boolean oomAdjusted = Flags.serviceBindingOomAdjPolicy() && r.wasOomAdjUpdated();
 
         // Tell the service that it has been unbound.
         if (r.app != null && r.app.isThreadReady()) {
@@ -6132,7 +6136,7 @@
                         bumpServiceExecutingLocked(r, false, "bring down unbind",
                                 oomAdjusted ? OOM_ADJ_REASON_NONE : OOM_ADJ_REASON_UNBIND_SERVICE,
                                 oomAdjusted /* skipTimeoutIfPossible */);
-                        oomAdjusted |= r.mOomAdjBumpedInExec;
+                        oomAdjusted |= r.wasOomAdjUpdated();
                         ibr.hasBound = false;
                         ibr.requested = false;
                         r.app.getThread().scheduleUnbindService(r,
@@ -6292,7 +6296,7 @@
                                 oomAdjusted ? OOM_ADJ_REASON_NONE : OOM_ADJ_REASON_UNBIND_SERVICE,
                                 oomAdjusted /* skipTimeoutIfPossible */);
                         mDestroyingServices.add(r);
-                        oomAdjusted |= r.mOomAdjBumpedInExec;
+                        oomAdjusted |= r.wasOomAdjUpdated();
                         r.destroying = true;
                         r.app.getThread().scheduleStopService(r);
                     } catch (Exception e) {
@@ -6579,7 +6583,7 @@
             }
             final long origId = mAm.mInjector.clearCallingIdentity();
             serviceDoneExecutingLocked(r, inDestroying, inDestroying, enqueueOomAdj,
-                    !Flags.serviceBindingOomAdjPolicy() || r.mOomAdjBumpedInExec || needOomAdj
+                    !Flags.serviceBindingOomAdjPolicy() || r.wasOomAdjUpdated() || needOomAdj
                     ? OOM_ADJ_REASON_EXECUTING_SERVICE : OOM_ADJ_REASON_NONE);
             mAm.mInjector.restoreCallingIdentity(origId);
         } else {
@@ -6645,7 +6649,7 @@
                 } else {
                     // Skip oom adj if it wasn't bumped during the bumpServiceExecutingLocked()
                 }
-                r.mOomAdjBumpedInExec = false;
+                r.updateOomAdjSeq();
             }
             r.executeFg = false;
             if (r.tracker != null) {
@@ -7029,7 +7033,6 @@
             sr.setProcess(null, null, 0, null);
             sr.isolationHostProc = null;
             sr.executeNesting = 0;
-            sr.mOomAdjBumpedInExec = false;
             synchronized (mAm.mProcessStats.mLock) {
                 sr.forceClearTracker();
             }
@@ -9013,6 +9016,7 @@
         r.isForeground = true;
         r.mFgsEnterTime = SystemClock.uptimeMillis();
         r.foregroundServiceType = options.mForegroundServiceTypes;
+        r.updateOomAdjSeq();
         setFgsRestrictionLocked(callingPackage, callingPid, callingUid, intent, r, userId,
                 BackgroundStartPrivileges.NONE,  false /* isBindService */);
         final ProcessServiceRecord psr = callerApp.mServices;
@@ -9075,6 +9079,7 @@
             }
         }
         if (r != null) {
+            r.updateOomAdjSeq();
             bringDownServiceLocked(r, false);
         } else {
             Slog.e(TAG, "stopForegroundServiceDelegateLocked delegate does not exist "
@@ -9100,6 +9105,7 @@
             }
         }
         if (r != null) {
+            r.updateOomAdjSeq();
             bringDownServiceLocked(r, false);
         } else {
             Slog.e(TAG, "stopForegroundServiceDelegateLocked delegate does not exist");
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 4f1a35c..ed1a763 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -20868,4 +20868,14 @@
         }
         mOomAdjuster.mCachedAppOptimizer.binderError(debugPid, app, code, flags, err);
     }
+
+    @GuardedBy("this")
+    void enqueuePendingTopAppIfNecessaryLocked() {
+        mPendingStartActivityUids.enqueuePendingTopAppIfNecessaryLocked(this);
+    }
+
+    @GuardedBy("this")
+    void clearPendingTopAppLocked() {
+        mPendingStartActivityUids.clear();
+    }
 }
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index 5e91cd3..df6481d 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -409,6 +409,12 @@
 
     private final OomAdjusterDebugLogger mLogger;
 
+    /**
+     * The process state of the current TOP app.
+     */
+    @GuardedBy("mService")
+    protected int mProcessStateCurTop = PROCESS_STATE_TOP;
+
     /** Overrideable by a test */
     @VisibleForTesting
     protected boolean isChangeEnabled(@CachedCompatChangeId int cachedCompatChangeId,
@@ -518,59 +524,6 @@
     }
 
     /**
-     * Perform oom adj update on the given process. It does NOT do the re-computation
-     * if there is a cycle, caller should check {@link #mProcessesInCycle} and do it on its own.
-     */
-    @GuardedBy({"mService", "mProcLock"})
-    private boolean performUpdateOomAdjLSP(ProcessRecord app, int cachedAdj,
-            ProcessRecord topApp, long now, @OomAdjReason int oomAdjReason) {
-        if (app.getThread() == null) {
-            return false;
-        }
-
-        app.mState.resetCachedInfo();
-        app.mState.setCurBoundByNonBgRestrictedApp(false);
-        UidRecord uidRec = app.getUidRecord();
-        if (uidRec != null) {
-            if (DEBUG_UID_OBSERVERS) {
-                Slog.i(TAG_UID_OBSERVERS, "Starting update of " + uidRec);
-            }
-            uidRec.reset();
-        }
-
-        // Check if this process is in the pending list too, remove from pending list if so.
-        mPendingProcessSet.remove(app);
-
-        mProcessesInCycle.clear();
-        computeOomAdjLSP(app, cachedAdj, topApp, false, now, false, true, oomAdjReason, true);
-        if (!mProcessesInCycle.isEmpty()) {
-            // We can't use the score here if there is a cycle, abort.
-            for (int i = mProcessesInCycle.size() - 1; i >= 0; i--) {
-                // Reset the adj seq
-                mProcessesInCycle.valueAt(i).mState.setCompletedAdjSeq(mAdjSeq - 1);
-            }
-            return true;
-        }
-
-        if (uidRec != null) {
-            // After uidRec.reset() above, for UidRecord with multiple processes (ProcessRecord),
-            // we need to apply all ProcessRecord into UidRecord.
-            uidRec.forEachProcess(this::updateAppUidRecIfNecessaryLSP);
-            if (uidRec.getCurProcState() != PROCESS_STATE_NONEXISTENT
-                    && (uidRec.getSetProcState() != uidRec.getCurProcState()
-                    || uidRec.getSetCapability() != uidRec.getCurCapability()
-                    || uidRec.isSetAllowListed() != uidRec.isCurAllowListed())) {
-                final ActiveUids uids = mTmpUidRecords;
-                uids.clear();
-                uids.put(uidRec.getUid(), uidRec);
-                updateUidsLSP(uids, SystemClock.elapsedRealtime());
-            }
-        }
-
-        return applyOomAdjLSP(app, false, now, SystemClock.elapsedRealtime(), oomAdjReason);
-    }
-
-    /**
      * Update OomAdj for all processes in LRU list
      */
     @GuardedBy("mService")
@@ -599,6 +552,7 @@
     @GuardedBy({"mService", "mProcLock"})
     protected void performUpdateOomAdjLSP(@OomAdjReason int oomAdjReason) {
         final ProcessRecord topApp = mService.getTopApp();
+        mProcessStateCurTop = mService.mAtmInternal.getTopProcessState();
         // Clear any pending ones because we are doing a full update now.
         mPendingProcessSet.clear();
         mService.mAppProfiler.mHasPreviousProcess = mService.mAppProfiler.mHasHomeProcess = false;
@@ -649,54 +603,14 @@
         mLastReason = oomAdjReason;
         Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, oomAdjReasonToString(oomAdjReason));
         mService.mOomAdjProfiler.oomAdjStarted();
-        mAdjSeq++;
 
         final ProcessStateRecord state = app.mState;
-        final boolean wasCached = state.isCached();
-        final int oldAdj = state.getCurRawAdj();
-        final int cachedAdj = oldAdj >= CACHED_APP_MIN_ADJ
-                ? oldAdj : UNKNOWN_ADJ;
-
-        // Firstly, try to see if the importance of itself gets changed
-        final boolean wasBackground = ActivityManager.isProcStateBackground(
-                state.getSetProcState());
-        final int oldCap = state.getSetCapability();
-        state.setContainsCycle(false);
-        state.setProcStateChanged(false);
-        state.resetCachedInfo();
-        state.setCurBoundByNonBgRestrictedApp(false);
-        // Check if this process is in the pending list too, remove from pending list if so.
-        mPendingProcessSet.remove(app);
-        app.mOptRecord.setLastOomAdjChangeReason(oomAdjReason);
-        boolean success = performUpdateOomAdjLSP(app, cachedAdj, topApp,
-                SystemClock.uptimeMillis(), oomAdjReason);
-        // The 'app' here itself might or might not be in the cycle, for example,
-        // the case A <=> B vs. A -> B <=> C; anyway, if we spot a cycle here, re-compute them.
-        if (!success || (wasCached == state.isCached() && oldAdj != INVALID_ADJ
-                && mProcessesInCycle.isEmpty() /* Force re-compute if there is a cycle */
-                && oldCap == state.getCurCapability()
-                && wasBackground == ActivityManager.isProcStateBackground(
-                        state.getSetProcState()))) {
-            mProcessesInCycle.clear();
-            // Okay, it's unchanged, it won't impact any service it binds to, we're done here.
-            if (DEBUG_OOM_ADJ) {
-                Slog.i(TAG_OOM_ADJ, "No oomadj changes for " + app);
-            }
-            mService.mOomAdjProfiler.oomAdjEnded();
-            Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
-            return success;
-        }
 
         // Next to find out all its reachable processes
         ArrayList<ProcessRecord> processes = mTmpProcessList;
         ActiveUids uids = mTmpUidRecords;
         mPendingProcessSet.add(app);
-
-        // Add all processes with cycles into the list to scan
-        for (int i = mProcessesInCycle.size() - 1; i >= 0; i--) {
-            mPendingProcessSet.add(mProcessesInCycle.valueAt(i));
-        }
-        mProcessesInCycle.clear();
+        mProcessStateCurTop = enqueuePendingTopAppIfNecessaryLSP();
 
         boolean containsCycle = collectReachableProcessesLocked(mPendingProcessSet,
                 processes, uids);
@@ -704,14 +618,8 @@
         // Clear the pending set as they should've been included in 'processes'.
         mPendingProcessSet.clear();
 
-        if (!containsCycle) {
-            // Remove this app from the return list because we've done the computation on it.
-            processes.remove(app);
-        }
-
         int size = processes.size();
         if (size > 0) {
-            mAdjSeq--;
             // Update these reachable processes
             updateOomAdjInnerLSP(oomAdjReason, topApp, processes, uids, containsCycle, false);
         } else if (state.getCurRawAdj() == UNKNOWN_ADJ) {
@@ -723,11 +631,25 @@
                     SystemClock.elapsedRealtime(), oomAdjReason);
         }
         mTmpProcessList.clear();
+        mService.clearPendingTopAppLocked();
         mService.mOomAdjProfiler.oomAdjEnded();
         Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
         return true;
     }
 
+    @GuardedBy({"mService", "mProcLock"})
+    protected int enqueuePendingTopAppIfNecessaryLSP() {
+        final int prevTopProcessState = mService.mAtmInternal.getTopProcessState();
+        mService.enqueuePendingTopAppIfNecessaryLocked();
+        final int topProcessState = mService.mAtmInternal.getTopProcessState();
+        if (prevTopProcessState != topProcessState) {
+            // Unlikely but possible: WM just updated the top process state, it may have
+            // enqueued the new top app to the pending top UID list. Enqueue that one here too.
+            mService.enqueuePendingTopAppIfNecessaryLocked();
+        }
+        return topProcessState;
+    }
+
     /**
      * Collect the reachable processes from the given {@code apps}, the result will be
      * returned in the given {@code processes}, which will include the processes from
@@ -930,6 +852,7 @@
         mLastReason = oomAdjReason;
         Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, oomAdjReasonToString(oomAdjReason));
         mService.mOomAdjProfiler.oomAdjStarted();
+        mProcessStateCurTop = enqueuePendingTopAppIfNecessaryLSP();
 
         final ArrayList<ProcessRecord> processes = mTmpProcessList;
         final ActiveUids uids = mTmpUidRecords;
@@ -939,6 +862,7 @@
             updateOomAdjInnerLSP(oomAdjReason, topApp, processes, uids, true, false);
         }
         processes.clear();
+        mService.clearPendingTopAppLocked();
 
         mService.mOomAdjProfiler.oomAdjEnded();
         Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
@@ -1337,6 +1261,7 @@
                     // Avoid trimming processes that are still initializing. If they aren't
                     // hosting any components yet because they may be unfairly killed.
                     // We however apply the oom scores set at #setAttachingProcessStatesLSP.
+                    updateAppUidRecLSP(app);
                     continue;
                 }
 
@@ -1882,7 +1807,7 @@
 
         state.setSystemNoUi(false);
 
-        final int PROCESS_STATE_CUR_TOP = mService.mAtmInternal.getTopProcessState();
+        final int PROCESS_STATE_CUR_TOP = mProcessStateCurTop;
 
         // Determine the importance of the process, starting with most
         // important to least, and assign an appropriate OOM adjustment.
@@ -3286,13 +3211,15 @@
                     // If the partial values are no better, skip until the next
                     // attempt
                     if (client.getCurRawProcState() >= procState
-                            && client.getCurRawAdj() >= adj) {
+                            && client.getCurRawAdj() >= adj
+                            && (client.getCurCapability() & app.mState.getCurCapability())
+                            == client.getCurCapability()) {
                         return true;
                     }
                     // Else use the client's partial procstate and adj to adjust the
                     // effect of the binding
                 } else {
-                    return true;
+                    return false;
                 }
             }
         }
diff --git a/services/core/java/com/android/server/am/OomAdjusterModernImpl.java b/services/core/java/com/android/server/am/OomAdjusterModernImpl.java
index 5feac1f..00e1482 100644
--- a/services/core/java/com/android/server/am/OomAdjusterModernImpl.java
+++ b/services/core/java/com/android/server/am/OomAdjusterModernImpl.java
@@ -737,6 +737,7 @@
     @Override
     protected void performUpdateOomAdjLSP(@OomAdjReason int oomAdjReason) {
         final ProcessRecord topApp = mService.getTopApp();
+        mProcessStateCurTop = mService.mAtmInternal.getTopProcessState();
         // Clear any pending ones because we are doing a full update now.
         mPendingProcessSet.clear();
         mService.mAppProfiler.mHasPreviousProcess = mService.mAppProfiler.mHasHomeProcess = false;
@@ -763,6 +764,7 @@
     @Override
     protected void performUpdateOomAdjPendingTargetsLocked(@OomAdjReason int oomAdjReason) {
         mLastReason = oomAdjReason;
+        mProcessStateCurTop = enqueuePendingTopAppIfNecessaryLSP();
         Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, oomAdjReasonToString(oomAdjReason));
         mService.mOomAdjProfiler.oomAdjStarted();
 
diff --git a/services/core/java/com/android/server/am/PendingStartActivityUids.java b/services/core/java/com/android/server/am/PendingStartActivityUids.java
index da09317..e912d07 100644
--- a/services/core/java/com/android/server/am/PendingStartActivityUids.java
+++ b/services/core/java/com/android/server/am/PendingStartActivityUids.java
@@ -87,4 +87,22 @@
     synchronized boolean isPendingTopUid(int uid) {
         return mPendingUids.get(uid) != null;
     }
+
+    // Must called with AMS locked.
+    synchronized void enqueuePendingTopAppIfNecessaryLocked(ActivityManagerService ams) {
+        for (int i = 0, size = mPendingUids.size(); i < size; i++) {
+            final Pair<Integer, Long> p = mPendingUids.valueAt(i);
+            final ProcessRecord app;
+            synchronized (ams.mPidsSelfLocked) {
+                app = ams.mPidsSelfLocked.get(p.first);
+            }
+            if (app != null) {
+                ams.enqueueOomAdjTargetLocked(app);
+            }
+        }
+    }
+
+    synchronized void clear() {
+        mPendingUids.clear();
+    }
 }
diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java
index e3aac02..5834dcd 100644
--- a/services/core/java/com/android/server/am/ServiceRecord.java
+++ b/services/core/java/com/android/server/am/ServiceRecord.java
@@ -267,9 +267,13 @@
     int mAllowStart_byBindings = REASON_DENIED;
 
     /**
-     * Whether or not we've bumped its oom adj scores during its execution.
+     * The oom adj seq number snapshot of the host process. We're taking a snapshot
+     * before executing the service. Since we may or may not bump the host process's
+     * proc state / oom adj value before that, at the end of the execution, we could
+     * compare this seq against the current seq of the host process to see if we could
+     * skip the oom adj update from there too.
      */
-    boolean mOomAdjBumpedInExec;
+    int mAdjSeq;
 
     /**
      * Whether to use the new "while-in-use permission" logic for FGS start
@@ -1884,4 +1888,17 @@
         }
         return true;
     }
+
+    /**
+     * @return {@code true} if the host process has updated its oom adj scores.
+     */
+    boolean wasOomAdjUpdated() {
+        return app != null && app.mState.getAdjSeq() > mAdjSeq;
+    }
+
+    void updateOomAdjSeq() {
+        if (app != null) {
+            mAdjSeq = app.mState.getAdjSeq();
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index c020a9f..c2613bc 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -993,6 +993,13 @@
         if (isCurrentUserLU(userId)) {
             return USER_OP_IS_CURRENT;
         }
+        // TODO(b/324647580): Refactor the idea of "force" and clean up. In the meantime...
+        final int parentId = mUserProfileGroupIds.get(userId, UserInfo.NO_PROFILE_GROUP_ID);
+        if (parentId != UserInfo.NO_PROFILE_GROUP_ID && parentId != userId) {
+            if ((UserHandle.USER_SYSTEM == parentId || isCurrentUserLU(parentId)) && !force) {
+                return USER_OP_ERROR_RELATED_USERS_CANNOT_STOP;
+            }
+        }
         TimingsTraceAndSlog t = new TimingsTraceAndSlog();
         int[] usersToStop = getUsersToStopLU(userId);
         // If one of related users is system or current, no related users should be stopped
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index f1eea72..951f676 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -127,8 +127,7 @@
     private final Object mDeviceStateLock = new Object();
 
     // Request to override default use of A2DP for media.
-    @GuardedBy("mDeviceStateLock")
-    private boolean mBluetoothA2dpEnabled;
+    private AtomicBoolean mBluetoothA2dpEnabled = new AtomicBoolean(false);
 
     // lock always taken when accessing AudioService.mSetModeDeathHandlers
     // TODO do not "share" the lock between AudioService and BtHelpr, see b/123769055
@@ -275,17 +274,8 @@
     }
 
     /*package*/ void setBluetoothA2dpOn_Async(boolean on, String source) {
-        synchronized (mDeviceStateLock) {
-            if (mBluetoothA2dpEnabled == on) {
-                return;
-            }
-            mBluetoothA2dpEnabled = on;
-            mBrokerHandler.removeMessages(MSG_IIL_SET_FORCE_BT_A2DP_USE);
-            sendIILMsgNoDelay(MSG_IIL_SET_FORCE_BT_A2DP_USE, SENDMSG_QUEUE,
-                    AudioSystem.FOR_MEDIA,
-                    mBluetoothA2dpEnabled ? AudioSystem.FORCE_NONE : AudioSystem.FORCE_NO_BT_A2DP,
-                    source);
-        }
+        mBluetoothA2dpEnabled.set(on);
+        sendLMsgNoDelay(MSG_L_SET_FORCE_BT_A2DP_USE, SENDMSG_REPLACE, source);
     }
 
     /**
@@ -1223,16 +1213,8 @@
         }
     }
 
-    /*package*/ boolean isAvrcpAbsoluteVolumeSupported() {
-        synchronized (mDeviceStateLock) {
-            return mBtHelper.isAvrcpAbsoluteVolumeSupported();
-        }
-    }
-
     /*package*/ boolean isBluetoothA2dpOn() {
-        synchronized (mDeviceStateLock) {
-            return mBluetoothA2dpEnabled;
-        }
+        return mBluetoothA2dpEnabled.get();
     }
 
     /*package*/ void postSetAvrcpAbsoluteVolumeIndex(int index) {
@@ -1601,15 +1583,12 @@
                 .append(") from u/pid:").append(Binder.getCallingUid()).append("/")
                 .append(Binder.getCallingPid()).append(" src:").append(source).toString();
 
-        synchronized (mDeviceStateLock) {
-            mBluetoothA2dpEnabled = on;
-            mBrokerHandler.removeMessages(MSG_IIL_SET_FORCE_BT_A2DP_USE);
-            onSetForceUse(
-                    AudioSystem.FOR_MEDIA,
-                    mBluetoothA2dpEnabled ? AudioSystem.FORCE_NONE : AudioSystem.FORCE_NO_BT_A2DP,
-                    fromA2dp,
-                    eventSource);
-        }
+        mBluetoothA2dpEnabled.set(on);
+        onSetForceUse(
+                AudioSystem.FOR_MEDIA,
+                on ? AudioSystem.FORCE_NONE : AudioSystem.FORCE_NO_BT_A2DP,
+                fromA2dp,
+                eventSource);
     }
 
     /*package*/ boolean handleDeviceConnection(@NonNull AudioDeviceAttributes attributes,
@@ -1658,9 +1637,7 @@
     }
 
     /*package*/ boolean getBluetoothA2dpEnabled() {
-        synchronized (mDeviceStateLock) {
-            return mBluetoothA2dpEnabled;
-        }
+        return mBluetoothA2dpEnabled.get();
     }
 
     /*package*/ int getLeAudioDeviceGroupId(BluetoothDevice device) {
@@ -1821,9 +1798,12 @@
                     }
                     break;
                 case MSG_IIL_SET_FORCE_USE: // intended fall-through
-                case MSG_IIL_SET_FORCE_BT_A2DP_USE:
-                    onSetForceUse(msg.arg1, msg.arg2,
-                                  (msg.what == MSG_IIL_SET_FORCE_BT_A2DP_USE), (String) msg.obj);
+                    onSetForceUse(msg.arg1, msg.arg2, false, (String) msg.obj);
+                    break;
+                case MSG_L_SET_FORCE_BT_A2DP_USE:
+                    int forcedUsage = mBluetoothA2dpEnabled.get()
+                            ? AudioSystem.FORCE_NONE : AudioSystem.FORCE_NO_BT_A2DP;
+                    onSetForceUse(AudioSystem.FOR_MEDIA, forcedUsage, true, (String) msg.obj);
                     break;
                 case MSG_REPORT_NEW_ROUTES:
                 case MSG_REPORT_NEW_ROUTES_A2DP:
@@ -1831,35 +1811,38 @@
                         mDeviceInventory.onReportNewRoutes();
                     }
                     break;
-                case MSG_L_SET_BT_ACTIVE_DEVICE:
-                    synchronized (mSetModeLock) {
-                        synchronized (mDeviceStateLock) {
-                            final BtDeviceInfo btInfo = (BtDeviceInfo) msg.obj;
-                            if (btInfo.mState == BluetoothProfile.STATE_CONNECTED
-                                    && !mBtHelper.isProfilePoxyConnected(btInfo.mProfile)) {
-                                AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
-                                        "msg: MSG_L_SET_BT_ACTIVE_DEVICE "
-                                            + "received with null profile proxy: "
-                                            + btInfo)).printLog(TAG));
-                            } else {
-                                @AudioSystem.AudioFormatNativeEnumForBtCodec final int codec =
-                                        mBtHelper.getCodecWithFallback(btInfo.mDevice,
-                                                btInfo.mProfile, btInfo.mIsLeOutput,
-                                                "MSG_L_SET_BT_ACTIVE_DEVICE");
+                case MSG_L_SET_BT_ACTIVE_DEVICE: {
+                    final BtDeviceInfo btInfo = (BtDeviceInfo) msg.obj;
+                    if (btInfo.mState == BluetoothProfile.STATE_CONNECTED
+                            && !mBtHelper.isProfilePoxyConnected(btInfo.mProfile)) {
+                        AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
+                                "msg: MSG_L_SET_BT_ACTIVE_DEVICE "
+                                        + "received with null profile proxy: "
+                                        + btInfo)).printLog(TAG));
+                    } else {
+                        @AudioSystem.AudioFormatNativeEnumForBtCodec final int codec =
+                                mBtHelper.getCodecWithFallback(btInfo.mDevice,
+                                        btInfo.mProfile, btInfo.mIsLeOutput,
+                                        "MSG_L_SET_BT_ACTIVE_DEVICE");
+                        synchronized (mSetModeLock) {
+                            synchronized (mDeviceStateLock) {
                                 mDeviceInventory.onSetBtActiveDevice(btInfo, codec,
                                         (btInfo.mProfile
-                                                != BluetoothProfile.LE_AUDIO || btInfo.mIsLeOutput)
+                                                != BluetoothProfile.LE_AUDIO
+                                                || btInfo.mIsLeOutput)
                                                 ? mAudioService.getBluetoothContextualVolumeStream()
                                                 : AudioSystem.STREAM_DEFAULT);
                                 if (btInfo.mProfile == BluetoothProfile.LE_AUDIO
-                                        || btInfo.mProfile == BluetoothProfile.HEARING_AID) {
-                                    onUpdateCommunicationRouteClient(isBluetoothScoRequested(),
+                                        || btInfo.mProfile
+                                        == BluetoothProfile.HEARING_AID) {
+                                    onUpdateCommunicationRouteClient(
+                                            isBluetoothScoRequested(),
                                             "setBluetoothActiveDevice");
                                 }
                             }
                         }
                     }
-                    break;
+                } break;
                 case MSG_BT_HEADSET_CNCT_FAILED:
                     synchronized (mSetModeLock) {
                         synchronized (mDeviceStateLock) {
@@ -1883,11 +1866,11 @@
                     break;
                 case MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE: {
                     final BtDeviceInfo btInfo = (BtDeviceInfo) msg.obj;
+                    @AudioSystem.AudioFormatNativeEnumForBtCodec final int codec =
+                            mBtHelper.getCodecWithFallback(btInfo.mDevice,
+                                    btInfo.mProfile, btInfo.mIsLeOutput,
+                                    "MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE");
                     synchronized (mDeviceStateLock) {
-                        @AudioSystem.AudioFormatNativeEnumForBtCodec final int codec =
-                                mBtHelper.getCodecWithFallback(btInfo.mDevice,
-                                        btInfo.mProfile, btInfo.mIsLeOutput,
-                                        "MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE");
                         mDeviceInventory.onBluetoothDeviceConfigChange(
                                 btInfo, codec, BtHelper.EVENT_DEVICE_CONFIG_CHANGE);
                     }
@@ -2098,7 +2081,7 @@
     private static final int MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE = 2;
     private static final int MSG_I_BROADCAST_BT_CONNECTION_STATE = 3;
     private static final int MSG_IIL_SET_FORCE_USE = 4;
-    private static final int MSG_IIL_SET_FORCE_BT_A2DP_USE = 5;
+    private static final int MSG_L_SET_FORCE_BT_A2DP_USE = 5;
     private static final int MSG_TOGGLE_HDMI = 6;
     private static final int MSG_L_SET_BT_ACTIVE_DEVICE = 7;
     private static final int MSG_BT_HEADSET_CNCT_FAILED = 9;
@@ -2295,7 +2278,7 @@
         MESSAGES_MUTE_MUSIC.add(MSG_L_SET_BT_ACTIVE_DEVICE);
         MESSAGES_MUTE_MUSIC.add(MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE);
         MESSAGES_MUTE_MUSIC.add(MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT);
-        MESSAGES_MUTE_MUSIC.add(MSG_IIL_SET_FORCE_BT_A2DP_USE);
+        MESSAGES_MUTE_MUSIC.add(MSG_L_SET_FORCE_BT_A2DP_USE);
     }
 
     private AtomicBoolean mMusicMuted = new AtomicBoolean(false);
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 649b9ef..be47f85 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -46,6 +46,7 @@
 import static com.android.media.audio.Flags.alarmMinVolumeZero;
 import static com.android.media.audio.Flags.disablePrescaleAbsoluteVolume;
 import static com.android.media.audio.Flags.ringerModeAffectsAlarm;
+import static com.android.media.audio.Flags.setStreamVolumeOrder;
 import static com.android.server.audio.SoundDoseHelper.ACTION_CHECK_MUSIC_ACTIVE;
 import static com.android.server.utils.EventLogger.Event.ALOGE;
 import static com.android.server.utils.EventLogger.Event.ALOGI;
@@ -4538,6 +4539,8 @@
                 + focusFreezeTestApi());
         pw.println("\tcom.android.media.audio.disablePrescaleAbsoluteVolume:"
                 + disablePrescaleAbsoluteVolume());
+        pw.println("\tcom.android.media.audio.setStreamVolumeOrder:"
+                + setStreamVolumeOrder());
         pw.println("\tandroid.media.audio.foregroundAudioControl:"
                 + foregroundAudioControl());
     }
@@ -4705,6 +4708,30 @@
 
         index = rescaleIndex(index * 10, streamType, streamTypeAlias);
 
+        if (setStreamVolumeOrder()) {
+            flags &= ~AudioManager.FLAG_FIXED_VOLUME;
+            if (streamTypeAlias == AudioSystem.STREAM_MUSIC && isFixedVolumeDevice(device)) {
+                flags |= AudioManager.FLAG_FIXED_VOLUME;
+
+                // volume is either 0 or max allowed for fixed volume devices
+                if (index != 0) {
+                    index = mSoundDoseHelper.getSafeMediaVolumeIndex(device);
+                    if (index < 0) {
+                        index = streamState.getMaxIndex();
+                    }
+                }
+            }
+
+            if (!mSoundDoseHelper.willDisplayWarningAfterCheckVolume(streamType, index, device,
+                    flags)) {
+                onSetStreamVolume(streamType, index, flags, device, caller, hasModifyAudioSettings,
+                        // ada is non-null when called from setDeviceVolume,
+                        // which shouldn't update the mute state
+                        canChangeMuteAndUpdateController /*canChangeMute*/);
+                index = mStreamStates[streamType].getIndex(device);
+            }
+        }
+
         if (streamTypeAlias == AudioSystem.STREAM_MUSIC
                 && AudioSystem.DEVICE_OUT_ALL_A2DP_SET.contains(device)
                 && (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) == 0) {
@@ -4738,26 +4765,28 @@
             mDeviceBroker.postSetHearingAidVolumeIndex(index, streamType);
         }
 
-        flags &= ~AudioManager.FLAG_FIXED_VOLUME;
-        if (streamTypeAlias == AudioSystem.STREAM_MUSIC && isFixedVolumeDevice(device)) {
-            flags |= AudioManager.FLAG_FIXED_VOLUME;
+        if (!setStreamVolumeOrder()) {
+            flags &= ~AudioManager.FLAG_FIXED_VOLUME;
+            if (streamTypeAlias == AudioSystem.STREAM_MUSIC && isFixedVolumeDevice(device)) {
+                flags |= AudioManager.FLAG_FIXED_VOLUME;
 
-            // volume is either 0 or max allowed for fixed volume devices
-            if (index != 0) {
-                index = mSoundDoseHelper.getSafeMediaVolumeIndex(device);
-                if (index < 0) {
-                    index = streamState.getMaxIndex();
+                // volume is either 0 or max allowed for fixed volume devices
+                if (index != 0) {
+                    index = mSoundDoseHelper.getSafeMediaVolumeIndex(device);
+                    if (index < 0) {
+                        index = streamState.getMaxIndex();
+                    }
                 }
             }
-        }
 
-        if (!mSoundDoseHelper.willDisplayWarningAfterCheckVolume(streamType, index, device,
-                flags)) {
-            onSetStreamVolume(streamType, index, flags, device, caller, hasModifyAudioSettings,
-                    // ada is non-null when called from setDeviceVolume,
-                    // which shouldn't update the mute state
-                    canChangeMuteAndUpdateController /*canChangeMute*/);
-            index = mStreamStates[streamType].getIndex(device);
+            if (!mSoundDoseHelper.willDisplayWarningAfterCheckVolume(streamType, index, device,
+                    flags)) {
+                onSetStreamVolume(streamType, index, flags, device, caller, hasModifyAudioSettings,
+                        // ada is non-null when called from setDeviceVolume,
+                        // which shouldn't update the mute state
+                        canChangeMuteAndUpdateController /*canChangeMute*/);
+                index = mStreamStates[streamType].getIndex(device);
+            }
         }
 
         synchronized (mHdmiClientLock) {
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index 0f3f807..f3a5fdb 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -235,10 +235,6 @@
         mDeviceBroker.setForceUse_Async(AudioSystem.FOR_MEDIA, forMed, "onAudioServerDied()");
     }
 
-    /*package*/ synchronized boolean isAvrcpAbsoluteVolumeSupported() {
-        return (mA2dp != null && mAvrcpAbsVolSupported);
-    }
-
     /*package*/ synchronized void setAvrcpAbsoluteVolumeSupported(boolean supported) {
         mAvrcpAbsVolSupported = supported;
         Log.i(TAG, "setAvrcpAbsoluteVolumeSupported supported=" + supported);
@@ -648,8 +644,6 @@
         }
     }
 
-    // @GuardedBy("mDeviceBroker.mSetModeLock")
-    @GuardedBy("AudioDeviceBroker.this.mDeviceStateLock")
     /*package*/ synchronized boolean isProfilePoxyConnected(int profile) {
         switch (profile) {
             case BluetoothProfile.HEADSET:
diff --git a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
index e2ae3de..e8394d4 100644
--- a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
+++ b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
@@ -399,7 +399,7 @@
         final DeviceState baseState = mBaseState.orElse(INVALID_DEVICE_STATE);
         final DeviceState currentState = mCommittedState.orElse(INVALID_DEVICE_STATE);
 
-        return new DeviceStateInfo(supportedStates, baseState,
+        return new DeviceStateInfo(new ArrayList<>(supportedStates), baseState,
                 createMergedDeviceState(currentState, baseState));
     }
 
diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
index 1546010..4aab9d2 100644
--- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java
+++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
@@ -29,6 +29,9 @@
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
 import android.hardware.SensorManager;
 import android.hardware.display.BrightnessConfiguration;
 import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest;
@@ -37,19 +40,20 @@
 import android.os.Message;
 import android.os.PowerManager;
 import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.Trace;
 import android.util.EventLog;
 import android.util.MathUtils;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.util.TimeUtils;
 import android.view.Display;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.display.BrightnessSynchronizer;
 import com.android.internal.os.BackgroundThread;
-import com.android.internal.os.Clock;
 import com.android.server.EventLogTags;
 import com.android.server.display.brightness.BrightnessEvent;
-import com.android.server.display.brightness.LightSensorController;
 import com.android.server.display.brightness.clamper.BrightnessClamperController;
 
 import java.io.PrintWriter;
@@ -64,6 +68,8 @@
 public class AutomaticBrightnessController {
     private static final String TAG = "AutomaticBrightnessController";
 
+    private static final boolean DEBUG_PRETEND_LIGHT_SENSOR_ABSENT = false;
+
     public static final int AUTO_BRIGHTNESS_ENABLED = 1;
     public static final int AUTO_BRIGHTNESS_DISABLED = 2;
     public static final int AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE = 3;
@@ -81,20 +87,32 @@
     public static final int AUTO_BRIGHTNESS_MODE_DOZE = 2;
     public static final int AUTO_BRIGHTNESS_MODE_MAX = AUTO_BRIGHTNESS_MODE_DOZE;
 
+    // How long the current sensor reading is assumed to be valid beyond the current time.
+    // This provides a bit of prediction, as well as ensures that the weight for the last sample is
+    // non-zero, which in turn ensures that the total weight is non-zero.
+    private static final long AMBIENT_LIGHT_PREDICTION_TIME_MILLIS = 100;
+
     // Debounce for sampling user-initiated changes in display brightness to ensure
     // the user is satisfied with the result before storing the sample.
     private static final int BRIGHTNESS_ADJUSTMENT_SAMPLE_DEBOUNCE_MILLIS = 10000;
 
-    private static final int MSG_BRIGHTNESS_ADJUSTMENT_SAMPLE = 1;
-    private static final int MSG_INVALIDATE_CURRENT_SHORT_TERM_MODEL = 2;
-    private static final int MSG_UPDATE_FOREGROUND_APP = 3;
-    private static final int MSG_UPDATE_FOREGROUND_APP_SYNC = 4;
-    private static final int MSG_RUN_UPDATE = 5;
-    private static final int MSG_INVALIDATE_PAUSED_SHORT_TERM_MODEL = 6;
+    private static final int MSG_UPDATE_AMBIENT_LUX = 1;
+    private static final int MSG_BRIGHTNESS_ADJUSTMENT_SAMPLE = 2;
+    private static final int MSG_INVALIDATE_CURRENT_SHORT_TERM_MODEL = 3;
+    private static final int MSG_UPDATE_FOREGROUND_APP = 4;
+    private static final int MSG_UPDATE_FOREGROUND_APP_SYNC = 5;
+    private static final int MSG_RUN_UPDATE = 6;
+    private static final int MSG_INVALIDATE_PAUSED_SHORT_TERM_MODEL = 7;
 
     // Callbacks for requesting updates to the display's power state
     private final Callbacks mCallbacks;
 
+    // The sensor manager.
+    private final SensorManager mSensorManager;
+
+    // The light sensor, or null if not available or needed.
+    private final Sensor mLightSensor;
+
     // The mapper to translate ambient lux to screen brightness in the range [0, 1.0].
     @NonNull
     private BrightnessMappingStrategy mCurrentBrightnessMapper;
@@ -109,21 +127,94 @@
     // How much to scale doze brightness by (should be (0, 1.0]).
     private final float mDozeScaleFactor;
 
+    // Initial light sensor event rate in milliseconds.
+    private final int mInitialLightSensorRate;
+
+    // Steady-state light sensor event rate in milliseconds.
+    private final int mNormalLightSensorRate;
+
+    // The current light sensor event rate in milliseconds.
+    private int mCurrentLightSensorRate;
+
+    // Stability requirements in milliseconds for accepting a new brightness level.  This is used
+    // for debouncing the light sensor.  Different constants are used to debounce the light sensor
+    // when adapting to brighter or darker environments.  This parameter controls how quickly
+    // brightness changes occur in response to an observed change in light level that exceeds the
+    // hysteresis threshold.
+    private final long mBrighteningLightDebounceConfig;
+    private final long mDarkeningLightDebounceConfig;
+    private final long mBrighteningLightDebounceConfigIdle;
+    private final long mDarkeningLightDebounceConfigIdle;
+
+    // If true immediately after the screen is turned on the controller will try to adjust the
+    // brightness based on the current sensor reads. If false, the controller will collect more data
+    // and only then decide whether to change brightness.
+    private final boolean mResetAmbientLuxAfterWarmUpConfig;
+
+    // Period of time in which to consider light samples for a short/long-term estimate of ambient
+    // light in milliseconds.
+    private final int mAmbientLightHorizonLong;
+    private final int mAmbientLightHorizonShort;
+
+    // The intercept used for the weighting calculation. This is used in order to keep all possible
+    // weighting values positive.
+    private final int mWeightingIntercept;
+
     // Configuration object for determining thresholds to change brightness dynamically
+    private final HysteresisLevels mAmbientBrightnessThresholds;
     private final HysteresisLevels mScreenBrightnessThresholds;
+    private final HysteresisLevels mAmbientBrightnessThresholdsIdle;
     private final HysteresisLevels mScreenBrightnessThresholdsIdle;
 
     private boolean mLoggingEnabled;
+
+    // Amount of time to delay auto-brightness after screen on while waiting for
+    // the light sensor to warm-up in milliseconds.
+    // May be 0 if no warm-up is required.
+    private int mLightSensorWarmUpTimeConfig;
+
+    // Set to true if the light sensor is enabled.
+    private boolean mLightSensorEnabled;
+
+    // The time when the light sensor was enabled.
+    private long mLightSensorEnableTime;
+
     // The currently accepted nominal ambient light level.
     private float mAmbientLux;
+
+    // The last calculated ambient light level (long time window).
+    private float mSlowAmbientLux;
+
+    // The last calculated ambient light level (short time window).
+    private float mFastAmbientLux;
+
+    // The last ambient lux value prior to passing the darkening or brightening threshold.
+    private float mPreThresholdLux;
+
     // True if mAmbientLux holds a valid value.
     private boolean mAmbientLuxValid;
+
+    // The ambient light level threshold at which to brighten or darken the screen.
+    private float mAmbientBrighteningThreshold;
+    private float mAmbientDarkeningThreshold;
+
     // The last brightness value prior to passing the darkening or brightening threshold.
     private float mPreThresholdBrightness;
 
     // The screen brightness threshold at which to brighten or darken the screen.
     private float mScreenBrighteningThreshold;
     private float mScreenDarkeningThreshold;
+    // The most recent light sample.
+    private float mLastObservedLux = INVALID_LUX;
+
+    // The time of the most light recent sample.
+    private long mLastObservedLuxTime;
+
+    // The number of light samples collected since the light sensor was enabled.
+    private int mRecentLightSamples;
+
+    // A ring buffer containing all of the recent ambient light sensor readings.
+    private AmbientLightRingBuffer mAmbientLightRingBuffer;
 
     // The handler
     private AutomaticBrightnessHandler mHandler;
@@ -182,55 +273,88 @@
     private Context mContext;
     private int mState = AUTO_BRIGHTNESS_DISABLED;
 
-    private final Clock mClock;
+    private Clock mClock;
     private final Injector mInjector;
 
-    private final LightSensorController mLightSensorController;
-
-    AutomaticBrightnessController(Callbacks callbacks, Looper looper, SensorManager sensorManager,
+    AutomaticBrightnessController(Callbacks callbacks, Looper looper,
+            SensorManager sensorManager, Sensor lightSensor,
             SparseArray<BrightnessMappingStrategy> brightnessMappingStrategyMap,
-            float brightnessMin, float brightnessMax, float dozeScaleFactor,
+            int lightSensorWarmUpTime, float brightnessMin, float brightnessMax,
+            float dozeScaleFactor, int lightSensorRate, int initialLightSensorRate,
+            long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
+            long brighteningLightDebounceConfigIdle, long darkeningLightDebounceConfigIdle,
+            boolean resetAmbientLuxAfterWarmUpConfig, HysteresisLevels ambientBrightnessThresholds,
             HysteresisLevels screenBrightnessThresholds,
+            HysteresisLevels ambientBrightnessThresholdsIdle,
             HysteresisLevels screenBrightnessThresholdsIdle, Context context,
             BrightnessRangeController brightnessModeController,
-            BrightnessThrottler brightnessThrottler, float userLux, float userNits,
-            int displayId, LightSensorController.LightSensorControllerConfig config,
+            BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
+            int ambientLightHorizonLong, float userLux, float userNits,
             BrightnessClamperController brightnessClamperController) {
-        this(new Injector(), callbacks, looper,
-                brightnessMappingStrategyMap, brightnessMin, brightnessMax, dozeScaleFactor,
-                screenBrightnessThresholds,
+        this(new Injector(), callbacks, looper, sensorManager, lightSensor,
+                brightnessMappingStrategyMap, lightSensorWarmUpTime, brightnessMin, brightnessMax,
+                dozeScaleFactor, lightSensorRate, initialLightSensorRate,
+                brighteningLightDebounceConfig, darkeningLightDebounceConfig,
+                brighteningLightDebounceConfigIdle, darkeningLightDebounceConfigIdle,
+                resetAmbientLuxAfterWarmUpConfig, ambientBrightnessThresholds,
+                screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
                 screenBrightnessThresholdsIdle, context, brightnessModeController,
-                brightnessThrottler, userLux, userNits,
-                new LightSensorController(sensorManager, looper, displayId, config),
-                brightnessClamperController
+                brightnessThrottler, ambientLightHorizonShort, ambientLightHorizonLong, userLux,
+                userNits, brightnessClamperController
         );
     }
 
     @VisibleForTesting
     AutomaticBrightnessController(Injector injector, Callbacks callbacks, Looper looper,
+            SensorManager sensorManager, Sensor lightSensor,
             SparseArray<BrightnessMappingStrategy> brightnessMappingStrategyMap,
-            float brightnessMin, float brightnessMax, float dozeScaleFactor,
+            int lightSensorWarmUpTime, float brightnessMin, float brightnessMax,
+            float dozeScaleFactor, int lightSensorRate, int initialLightSensorRate,
+            long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
+            long brighteningLightDebounceConfigIdle, long darkeningLightDebounceConfigIdle,
+            boolean resetAmbientLuxAfterWarmUpConfig, HysteresisLevels ambientBrightnessThresholds,
             HysteresisLevels screenBrightnessThresholds,
+            HysteresisLevels ambientBrightnessThresholdsIdle,
             HysteresisLevels screenBrightnessThresholdsIdle, Context context,
             BrightnessRangeController brightnessRangeController,
-            BrightnessThrottler brightnessThrottler, float userLux, float userNits,
-            LightSensorController lightSensorController,
+            BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
+            int ambientLightHorizonLong, float userLux, float userNits,
             BrightnessClamperController brightnessClamperController) {
         mInjector = injector;
         mClock = injector.createClock();
         mContext = context;
         mCallbacks = callbacks;
+        mSensorManager = sensorManager;
         mCurrentBrightnessMapper = brightnessMappingStrategyMap.get(AUTO_BRIGHTNESS_MODE_DEFAULT);
         mScreenBrightnessRangeMinimum = brightnessMin;
         mScreenBrightnessRangeMaximum = brightnessMax;
+        mLightSensorWarmUpTimeConfig = lightSensorWarmUpTime;
         mDozeScaleFactor = dozeScaleFactor;
+        mNormalLightSensorRate = lightSensorRate;
+        mInitialLightSensorRate = initialLightSensorRate;
+        mCurrentLightSensorRate = -1;
+        mBrighteningLightDebounceConfig = brighteningLightDebounceConfig;
+        mDarkeningLightDebounceConfig = darkeningLightDebounceConfig;
+        mBrighteningLightDebounceConfigIdle = brighteningLightDebounceConfigIdle;
+        mDarkeningLightDebounceConfigIdle = darkeningLightDebounceConfigIdle;
+        mResetAmbientLuxAfterWarmUpConfig = resetAmbientLuxAfterWarmUpConfig;
+        mAmbientLightHorizonLong = ambientLightHorizonLong;
+        mAmbientLightHorizonShort = ambientLightHorizonShort;
+        mWeightingIntercept = ambientLightHorizonLong;
+        mAmbientBrightnessThresholds = ambientBrightnessThresholds;
+        mAmbientBrightnessThresholdsIdle = ambientBrightnessThresholdsIdle;
         mScreenBrightnessThresholds = screenBrightnessThresholds;
         mScreenBrightnessThresholdsIdle = screenBrightnessThresholdsIdle;
         mShortTermModel = new ShortTermModel();
         mPausedShortTermModel = new ShortTermModel();
         mHandler = new AutomaticBrightnessHandler(looper);
-        mLightSensorController = lightSensorController;
-        mLightSensorController.setListener(this::setAmbientLux);
+        mAmbientLightRingBuffer =
+            new AmbientLightRingBuffer(mNormalLightSensorRate, mAmbientLightHorizonLong, mClock);
+
+        if (!DEBUG_PRETEND_LIGHT_SENSOR_ABSENT) {
+            mLightSensor = lightSensor;
+        }
+
         mActivityTaskManager = ActivityTaskManager.getService();
         mPackageManager = mContext.getPackageManager();
         mTaskStackListener = new TaskStackListenerImpl();
@@ -282,13 +406,13 @@
         if (brightnessEvent != null) {
             brightnessEvent.setLux(
                     mAmbientLuxValid ? mAmbientLux : PowerManager.BRIGHTNESS_INVALID_FLOAT);
+            brightnessEvent.setPreThresholdLux(mPreThresholdLux);
             brightnessEvent.setPreThresholdBrightness(mPreThresholdBrightness);
             brightnessEvent.setRecommendedBrightness(mScreenAutoBrightness);
             brightnessEvent.setFlags(brightnessEvent.getFlags()
                     | (!mAmbientLuxValid ? BrightnessEvent.FLAG_INVALID_LUX : 0)
                     | (shouldApplyDozeScaleFactor() ? BrightnessEvent.FLAG_DOZE_SCALE : 0));
             brightnessEvent.setAutoBrightnessMode(getMode());
-            mLightSensorController.updateBrightnessEvent(brightnessEvent);
         }
 
         if (!mAmbientLuxValid) {
@@ -311,22 +435,21 @@
      */
     public float getAutomaticScreenBrightnessBasedOnLastObservedLux(
             BrightnessEvent brightnessEvent) {
-        float lastObservedLux = mLightSensorController.getLastObservedLux();
-        if (lastObservedLux == INVALID_LUX) {
+        if (mLastObservedLux == INVALID_LUX) {
             return PowerManager.BRIGHTNESS_INVALID_FLOAT;
         }
 
-        float brightness = mCurrentBrightnessMapper.getBrightness(lastObservedLux,
+        float brightness = mCurrentBrightnessMapper.getBrightness(mLastObservedLux,
                 mForegroundAppPackageName, mForegroundAppCategory);
         if (shouldApplyDozeScaleFactor()) {
             brightness *= mDozeScaleFactor;
         }
 
         if (brightnessEvent != null) {
-            brightnessEvent.setLux(lastObservedLux);
+            brightnessEvent.setLux(mLastObservedLux);
             brightnessEvent.setRecommendedBrightness(brightness);
             brightnessEvent.setFlags(brightnessEvent.getFlags()
-                    | (lastObservedLux == INVALID_LUX ? BrightnessEvent.FLAG_INVALID_LUX : 0)
+                    | (mLastObservedLux == INVALID_LUX ? BrightnessEvent.FLAG_INVALID_LUX : 0)
                     | (shouldApplyDozeScaleFactor() ? BrightnessEvent.FLAG_DOZE_SCALE : 0));
             brightnessEvent.setAutoBrightnessMode(getMode());
         }
@@ -378,7 +501,6 @@
 
     public void stop() {
         setLightSensorEnabled(false);
-        mLightSensorController.stop();
     }
 
     public boolean hasUserDataPoints() {
@@ -407,6 +529,14 @@
         return mAmbientLux;
     }
 
+    float getSlowAmbientLux() {
+        return mSlowAmbientLux;
+    }
+
+    float getFastAmbientLux() {
+        return mFastAmbientLux;
+    }
+
     private boolean setDisplayPolicy(int policy) {
         if (mDisplayPolicy == policy) {
             return false;
@@ -485,13 +615,36 @@
         pw.println("  mScreenBrightnessRangeMinimum=" + mScreenBrightnessRangeMinimum);
         pw.println("  mScreenBrightnessRangeMaximum=" + mScreenBrightnessRangeMaximum);
         pw.println("  mDozeScaleFactor=" + mDozeScaleFactor);
+        pw.println("  mInitialLightSensorRate=" + mInitialLightSensorRate);
+        pw.println("  mNormalLightSensorRate=" + mNormalLightSensorRate);
+        pw.println("  mLightSensorWarmUpTimeConfig=" + mLightSensorWarmUpTimeConfig);
+        pw.println("  mBrighteningLightDebounceConfig=" + mBrighteningLightDebounceConfig);
+        pw.println("  mDarkeningLightDebounceConfig=" + mDarkeningLightDebounceConfig);
+        pw.println("  mBrighteningLightDebounceConfigIdle=" + mBrighteningLightDebounceConfigIdle);
+        pw.println("  mDarkeningLightDebounceConfigIdle=" + mDarkeningLightDebounceConfigIdle);
+        pw.println("  mResetAmbientLuxAfterWarmUpConfig=" + mResetAmbientLuxAfterWarmUpConfig);
+        pw.println("  mAmbientLightHorizonLong=" + mAmbientLightHorizonLong);
+        pw.println("  mAmbientLightHorizonShort=" + mAmbientLightHorizonShort);
+        pw.println("  mWeightingIntercept=" + mWeightingIntercept);
+
         pw.println();
         pw.println("Automatic Brightness Controller State:");
+        pw.println("  mLightSensor=" + mLightSensor);
+        pw.println("  mLightSensorEnabled=" + mLightSensorEnabled);
+        pw.println("  mLightSensorEnableTime=" + TimeUtils.formatUptime(mLightSensorEnableTime));
+        pw.println("  mCurrentLightSensorRate=" + mCurrentLightSensorRate);
         pw.println("  mAmbientLux=" + mAmbientLux);
         pw.println("  mAmbientLuxValid=" + mAmbientLuxValid);
+        pw.println("  mPreThresholdLux=" + mPreThresholdLux);
         pw.println("  mPreThresholdBrightness=" + mPreThresholdBrightness);
+        pw.println("  mAmbientBrighteningThreshold=" + mAmbientBrighteningThreshold);
+        pw.println("  mAmbientDarkeningThreshold=" + mAmbientDarkeningThreshold);
         pw.println("  mScreenBrighteningThreshold=" + mScreenBrighteningThreshold);
         pw.println("  mScreenDarkeningThreshold=" + mScreenDarkeningThreshold);
+        pw.println("  mLastObservedLux=" + mLastObservedLux);
+        pw.println("  mLastObservedLuxTime=" + TimeUtils.formatUptime(mLastObservedLuxTime));
+        pw.println("  mRecentLightSamples=" + mRecentLightSamples);
+        pw.println("  mAmbientLightRingBuffer=" + mAmbientLightRingBuffer);
         pw.println("  mScreenAutoBrightness=" + mScreenAutoBrightness);
         pw.println("  mDisplayPolicy=" + DisplayPowerRequest.policyToString(mDisplayPolicy));
         pw.println("  mShortTermModel=");
@@ -520,21 +673,22 @@
         }
 
         pw.println();
+        pw.println("  mAmbientBrightnessThresholds=");
+        mAmbientBrightnessThresholds.dump(pw);
         pw.println("  mScreenBrightnessThresholds=");
         mScreenBrightnessThresholds.dump(pw);
         pw.println("  mScreenBrightnessThresholdsIdle=");
         mScreenBrightnessThresholdsIdle.dump(pw);
-
-        pw.println();
-        mLightSensorController.dump(pw);
+        pw.println("  mAmbientBrightnessThresholdsIdle=");
+        mAmbientBrightnessThresholdsIdle.dump(pw);
     }
 
     public float[] getLastSensorValues() {
-        return mLightSensorController.getLastSensorValues();
+        return mAmbientLightRingBuffer.getAllLuxValues();
     }
 
     public long[] getLastSensorTimestamps() {
-        return mLightSensorController.getLastSensorTimestamps();
+        return mAmbientLightRingBuffer.getAllTimestamps();
     }
 
     private String configStateToString(int state) {
@@ -551,33 +705,273 @@
     }
 
     private boolean setLightSensorEnabled(boolean enable) {
-        if (enable && mLightSensorController.enableLightSensorIfNeeded()) {
-            registerForegroundAppUpdater();
-            return true;
-        } else if (!enable && mLightSensorController.disableLightSensorIfNeeded()) {
+        if (enable) {
+            if (!mLightSensorEnabled) {
+                mLightSensorEnabled = true;
+                mLightSensorEnableTime = mClock.uptimeMillis();
+                mCurrentLightSensorRate = mInitialLightSensorRate;
+                registerForegroundAppUpdater();
+                mSensorManager.registerListener(mLightSensorListener, mLightSensor,
+                        mCurrentLightSensorRate * 1000, mHandler);
+                return true;
+            }
+        } else if (mLightSensorEnabled) {
+            mLightSensorEnabled = false;
+            mAmbientLuxValid = !mResetAmbientLuxAfterWarmUpConfig;
+            if (!mAmbientLuxValid) {
+                mPreThresholdLux = PowerManager.BRIGHTNESS_INVALID_FLOAT;
+            }
             mScreenAutoBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
             mRawScreenAutoBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
             mPreThresholdBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
-            mAmbientLuxValid = mLightSensorController.hasValidAmbientLux();
+            mRecentLightSamples = 0;
+            mAmbientLightRingBuffer.clear();
+            mCurrentLightSensorRate = -1;
+            mHandler.removeMessages(MSG_UPDATE_AMBIENT_LUX);
             unregisterForegroundAppUpdater();
+            mSensorManager.unregisterListener(mLightSensorListener);
         }
         return false;
     }
 
+    private void handleLightSensorEvent(long time, float lux) {
+        Trace.traceCounter(Trace.TRACE_TAG_POWER, "ALS", (int) lux);
+        mHandler.removeMessages(MSG_UPDATE_AMBIENT_LUX);
+
+        if (mAmbientLightRingBuffer.size() == 0) {
+            // switch to using the steady-state sample rate after grabbing the initial light sample
+            adjustLightSensorRate(mNormalLightSensorRate);
+        }
+        applyLightSensorMeasurement(time, lux);
+        updateAmbientLux(time);
+    }
+
+    private void applyLightSensorMeasurement(long time, float lux) {
+        mRecentLightSamples++;
+        mAmbientLightRingBuffer.prune(time - mAmbientLightHorizonLong);
+        mAmbientLightRingBuffer.push(time, lux);
+
+        // Remember this sample value.
+        mLastObservedLux = lux;
+        mLastObservedLuxTime = time;
+    }
+
+    private void adjustLightSensorRate(int lightSensorRate) {
+        // if the light sensor rate changed, update the sensor listener
+        if (lightSensorRate != mCurrentLightSensorRate) {
+            if (mLoggingEnabled) {
+                Slog.d(TAG, "adjustLightSensorRate: " +
+                        "previousRate=" + mCurrentLightSensorRate + ", " +
+                        "currentRate=" + lightSensorRate);
+            }
+            mCurrentLightSensorRate = lightSensorRate;
+            mSensorManager.unregisterListener(mLightSensorListener);
+            mSensorManager.registerListener(mLightSensorListener, mLightSensor,
+                    lightSensorRate * 1000, mHandler);
+        }
+    }
+
     private boolean setAutoBrightnessAdjustment(float adjustment) {
         return mCurrentBrightnessMapper.setAutoBrightnessAdjustment(adjustment);
     }
 
     private void setAmbientLux(float lux) {
-        // called by LightSensorController.setAmbientLux
-        mAmbientLuxValid = true;
+        if (mLoggingEnabled) {
+            Slog.d(TAG, "setAmbientLux(" + lux + ")");
+        }
+        if (lux < 0) {
+            Slog.w(TAG, "Ambient lux was negative, ignoring and setting to 0");
+            lux = 0;
+        }
         mAmbientLux = lux;
+        if (isInIdleMode()) {
+            mAmbientBrighteningThreshold =
+                    mAmbientBrightnessThresholdsIdle.getBrighteningThreshold(lux);
+            mAmbientDarkeningThreshold =
+                    mAmbientBrightnessThresholdsIdle.getDarkeningThreshold(lux);
+        } else {
+            mAmbientBrighteningThreshold =
+                    mAmbientBrightnessThresholds.getBrighteningThreshold(lux);
+            mAmbientDarkeningThreshold =
+                    mAmbientBrightnessThresholds.getDarkeningThreshold(lux);
+        }
         mBrightnessRangeController.onAmbientLuxChange(mAmbientLux);
         mBrightnessClamperController.onAmbientLuxChange(mAmbientLux);
 
         // If the short term model was invalidated and the change is drastic enough, reset it.
         mShortTermModel.maybeReset(mAmbientLux);
-        updateAutoBrightness(true /* sendUpdate */, false /* isManuallySet */);
+    }
+
+    private float calculateAmbientLux(long now, long horizon) {
+        if (mLoggingEnabled) {
+            Slog.d(TAG, "calculateAmbientLux(" + now + ", " + horizon + ")");
+        }
+        final int N = mAmbientLightRingBuffer.size();
+        if (N == 0) {
+            Slog.e(TAG, "calculateAmbientLux: No ambient light readings available");
+            return -1;
+        }
+
+        // Find the first measurement that is just outside of the horizon.
+        int endIndex = 0;
+        final long horizonStartTime = now - horizon;
+        for (int i = 0; i < N-1; i++) {
+            if (mAmbientLightRingBuffer.getTime(i + 1) <= horizonStartTime) {
+                endIndex++;
+            } else {
+                break;
+            }
+        }
+        if (mLoggingEnabled) {
+            Slog.d(TAG, "calculateAmbientLux: selected endIndex=" + endIndex + ", point=(" +
+                    mAmbientLightRingBuffer.getTime(endIndex) + ", " +
+                    mAmbientLightRingBuffer.getLux(endIndex) + ")");
+        }
+        float sum = 0;
+        float totalWeight = 0;
+        long endTime = AMBIENT_LIGHT_PREDICTION_TIME_MILLIS;
+        for (int i = N - 1; i >= endIndex; i--) {
+            long eventTime = mAmbientLightRingBuffer.getTime(i);
+            if (i == endIndex && eventTime < horizonStartTime) {
+                // If we're at the final value, make sure we only consider the part of the sample
+                // within our desired horizon.
+                eventTime = horizonStartTime;
+            }
+            final long startTime = eventTime - now;
+            float weight = calculateWeight(startTime, endTime);
+            float lux = mAmbientLightRingBuffer.getLux(i);
+            if (mLoggingEnabled) {
+                Slog.d(TAG, "calculateAmbientLux: [" + startTime + ", " + endTime + "]: " +
+                        "lux=" + lux + ", " +
+                        "weight=" + weight);
+            }
+            totalWeight += weight;
+            sum += lux * weight;
+            endTime = startTime;
+        }
+        if (mLoggingEnabled) {
+            Slog.d(TAG, "calculateAmbientLux: " +
+                    "totalWeight=" + totalWeight + ", " +
+                    "newAmbientLux=" + (sum / totalWeight));
+        }
+        return sum / totalWeight;
+    }
+
+    private float calculateWeight(long startDelta, long endDelta) {
+        return weightIntegral(endDelta) - weightIntegral(startDelta);
+    }
+
+    // Evaluates the integral of y = x + mWeightingIntercept. This is always positive for the
+    // horizon we're looking at and provides a non-linear weighting for light samples.
+    private float weightIntegral(long x) {
+        return x * (x * 0.5f + mWeightingIntercept);
+    }
+
+    private long nextAmbientLightBrighteningTransition(long time) {
+        final int N = mAmbientLightRingBuffer.size();
+        long earliestValidTime = time;
+        for (int i = N - 1; i >= 0; i--) {
+            if (mAmbientLightRingBuffer.getLux(i) <= mAmbientBrighteningThreshold) {
+                break;
+            }
+            earliestValidTime = mAmbientLightRingBuffer.getTime(i);
+        }
+        return earliestValidTime + (isInIdleMode()
+                ? mBrighteningLightDebounceConfigIdle : mBrighteningLightDebounceConfig);
+    }
+
+    private long nextAmbientLightDarkeningTransition(long time) {
+        final int N = mAmbientLightRingBuffer.size();
+        long earliestValidTime = time;
+        for (int i = N - 1; i >= 0; i--) {
+            if (mAmbientLightRingBuffer.getLux(i) >= mAmbientDarkeningThreshold) {
+                break;
+            }
+            earliestValidTime = mAmbientLightRingBuffer.getTime(i);
+        }
+        return earliestValidTime + (isInIdleMode()
+                ? mDarkeningLightDebounceConfigIdle : mDarkeningLightDebounceConfig);
+    }
+
+    private void updateAmbientLux() {
+        long time = mClock.uptimeMillis();
+        mAmbientLightRingBuffer.prune(time - mAmbientLightHorizonLong);
+        updateAmbientLux(time);
+    }
+
+    private void updateAmbientLux(long time) {
+        // If the light sensor was just turned on then immediately update our initial
+        // estimate of the current ambient light level.
+        if (!mAmbientLuxValid) {
+            final long timeWhenSensorWarmedUp =
+                mLightSensorWarmUpTimeConfig + mLightSensorEnableTime;
+            if (time < timeWhenSensorWarmedUp) {
+                if (mLoggingEnabled) {
+                    Slog.d(TAG, "updateAmbientLux: Sensor not ready yet: "
+                            + "time=" + time + ", "
+                            + "timeWhenSensorWarmedUp=" + timeWhenSensorWarmedUp);
+                }
+                mHandler.sendEmptyMessageAtTime(MSG_UPDATE_AMBIENT_LUX,
+                        timeWhenSensorWarmedUp);
+                return;
+            }
+            setAmbientLux(calculateAmbientLux(time, mAmbientLightHorizonShort));
+            mAmbientLuxValid = true;
+            if (mLoggingEnabled) {
+                Slog.d(TAG, "updateAmbientLux: Initializing: " +
+                        "mAmbientLightRingBuffer=" + mAmbientLightRingBuffer + ", " +
+                        "mAmbientLux=" + mAmbientLux);
+            }
+            updateAutoBrightness(true /* sendUpdate */, false /* isManuallySet */);
+        }
+
+        long nextBrightenTransition = nextAmbientLightBrighteningTransition(time);
+        long nextDarkenTransition = nextAmbientLightDarkeningTransition(time);
+        // Essentially, we calculate both a slow ambient lux, to ensure there's a true long-term
+        // change in lighting conditions, and a fast ambient lux to determine what the new
+        // brightness situation is since the slow lux can be quite slow to converge.
+        //
+        // Note that both values need to be checked for sufficient change before updating the
+        // proposed ambient light value since the slow value might be sufficiently far enough away
+        // from the fast value to cause a recalculation while its actually just converging on
+        // the fast value still.
+        mSlowAmbientLux = calculateAmbientLux(time, mAmbientLightHorizonLong);
+        mFastAmbientLux = calculateAmbientLux(time, mAmbientLightHorizonShort);
+
+        if ((mSlowAmbientLux >= mAmbientBrighteningThreshold
+                && mFastAmbientLux >= mAmbientBrighteningThreshold
+                && nextBrightenTransition <= time)
+                || (mSlowAmbientLux <= mAmbientDarkeningThreshold
+                        && mFastAmbientLux <= mAmbientDarkeningThreshold
+                        && nextDarkenTransition <= time)) {
+            mPreThresholdLux = mAmbientLux;
+            setAmbientLux(mFastAmbientLux);
+            if (mLoggingEnabled) {
+                Slog.d(TAG, "updateAmbientLux: "
+                        + ((mFastAmbientLux > mAmbientLux) ? "Brightened" : "Darkened") + ": "
+                        + "mAmbientBrighteningThreshold=" + mAmbientBrighteningThreshold + ", "
+                        + "mAmbientDarkeningThreshold=" + mAmbientDarkeningThreshold + ", "
+                        + "mAmbientLightRingBuffer=" + mAmbientLightRingBuffer + ", "
+                        + "mAmbientLux=" + mAmbientLux);
+            }
+            updateAutoBrightness(true /* sendUpdate */, false /* isManuallySet */);
+            nextBrightenTransition = nextAmbientLightBrighteningTransition(time);
+            nextDarkenTransition = nextAmbientLightDarkeningTransition(time);
+        }
+        long nextTransitionTime = Math.min(nextDarkenTransition, nextBrightenTransition);
+        // If one of the transitions is ready to occur, but the total weighted ambient lux doesn't
+        // exceed the necessary threshold, then it's possible we'll get a transition time prior to
+        // now. Rather than continually checking to see whether the weighted lux exceeds the
+        // threshold, schedule an update for when we'd normally expect another light sample, which
+        // should be enough time to decide whether we should actually transition to the new
+        // weighted ambient lux or not.
+        nextTransitionTime =
+                nextTransitionTime > time ? nextTransitionTime : time + mNormalLightSensorRate;
+        if (mLoggingEnabled) {
+            Slog.d(TAG, "updateAmbientLux: Scheduling ambient lux update for " +
+                    nextTransitionTime + TimeUtils.formatUptime(nextTransitionTime));
+        }
+        mHandler.sendEmptyMessageAtTime(MSG_UPDATE_AMBIENT_LUX, nextTransitionTime);
     }
 
     private void updateAutoBrightness(boolean sendUpdate, boolean isManuallySet) {
@@ -678,7 +1072,8 @@
                 if (mLoggingEnabled) {
                     Slog.d(TAG, "Auto-brightness adjustment changed by user: "
                             + "lux=" + mAmbientLux + ", "
-                            + "brightness=" + mScreenAutoBrightness);
+                            + "brightness=" + mScreenAutoBrightness + ", "
+                            + "ring=" + mAmbientLightRingBuffer);
                 }
 
                 EventLog.writeEvent(EventLogTags.AUTO_BRIGHTNESS_ADJ,
@@ -808,7 +1203,6 @@
         if (mode == AUTO_BRIGHTNESS_MODE_IDLE
                 || mCurrentBrightnessMapper.getMode() == AUTO_BRIGHTNESS_MODE_IDLE) {
             switchModeAndShortTermModels(mode);
-            mLightSensorController.setIdleMode(isInIdleMode());
         } else {
             resetShortTermModel();
             mCurrentBrightnessMapper = mBrightnessMappingStrategyMap.get(mode);
@@ -965,6 +1359,10 @@
                     updateAutoBrightness(true /*sendUpdate*/, false /*isManuallySet*/);
                     break;
 
+                case MSG_UPDATE_AMBIENT_LUX:
+                    updateAmbientLux();
+                    break;
+
                 case MSG_BRIGHTNESS_ADJUSTMENT_SAMPLE:
                     collectBrightnessAdjustmentSample();
                     break;
@@ -988,6 +1386,22 @@
         }
     }
 
+    private final SensorEventListener mLightSensorListener = new SensorEventListener() {
+        @Override
+        public void onSensorChanged(SensorEvent event) {
+            if (mLightSensorEnabled) {
+                final long time = mClock.uptimeMillis();
+                final float lux = event.values[0];
+                handleLightSensorEvent(time, lux);
+            }
+        }
+
+        @Override
+        public void onAccuracyChanged(Sensor sensor, int accuracy) {
+            // Not used.
+        }
+    };
+
     // Call back whenever the tasks stack changes, which includes tasks being created, removed, and
     // moving to top.
     class TaskStackListenerImpl extends TaskStackListener {
@@ -1002,13 +1416,192 @@
         void updateBrightness();
     }
 
+    /** Functional interface for providing time. */
+    @VisibleForTesting
+    interface Clock {
+        /**
+         * Returns current time in milliseconds since boot, not counting time spent in deep sleep.
+         */
+        long uptimeMillis();
+    }
+
+    /**
+     * A ring buffer of ambient light measurements sorted by time.
+     *
+     * Each entry consists of a timestamp and a lux measurement, and the overall buffer is sorted
+     * from oldest to newest.
+     */
+    private static final class AmbientLightRingBuffer {
+        // Proportional extra capacity of the buffer beyond the expected number of light samples
+        // in the horizon
+        private static final float BUFFER_SLACK = 1.5f;
+        private float[] mRingLux;
+        private long[] mRingTime;
+        private int mCapacity;
+
+        // The first valid element and the next open slot.
+        // Note that if mCount is zero then there are no valid elements.
+        private int mStart;
+        private int mEnd;
+        private int mCount;
+        Clock mClock;
+
+        public AmbientLightRingBuffer(long lightSensorRate, int ambientLightHorizon, Clock clock) {
+            if (lightSensorRate <= 0) {
+                throw new IllegalArgumentException("lightSensorRate must be above 0");
+            }
+            mCapacity = (int) Math.ceil(ambientLightHorizon * BUFFER_SLACK / lightSensorRate);
+            mRingLux = new float[mCapacity];
+            mRingTime = new long[mCapacity];
+            mClock = clock;
+        }
+
+        public float getLux(int index) {
+            return mRingLux[offsetOf(index)];
+        }
+
+        public float[] getAllLuxValues() {
+            float[] values = new float[mCount];
+            if (mCount == 0) {
+                return values;
+            }
+
+            if (mStart < mEnd) {
+                System.arraycopy(mRingLux, mStart, values, 0, mCount);
+            } else {
+                System.arraycopy(mRingLux, mStart, values, 0, mCapacity - mStart);
+                System.arraycopy(mRingLux, 0, values, mCapacity - mStart, mEnd);
+            }
+
+            return values;
+        }
+
+        public long getTime(int index) {
+            return mRingTime[offsetOf(index)];
+        }
+
+        public long[] getAllTimestamps() {
+            long[] values = new long[mCount];
+            if (mCount == 0) {
+                return values;
+            }
+
+            if (mStart < mEnd) {
+                System.arraycopy(mRingTime, mStart, values, 0, mCount);
+            } else {
+                System.arraycopy(mRingTime, mStart, values, 0, mCapacity - mStart);
+                System.arraycopy(mRingTime, 0, values, mCapacity - mStart, mEnd);
+            }
+
+            return values;
+        }
+
+        public void push(long time, float lux) {
+            int next = mEnd;
+            if (mCount == mCapacity) {
+                int newSize = mCapacity * 2;
+
+                float[] newRingLux = new float[newSize];
+                long[] newRingTime = new long[newSize];
+                int length = mCapacity - mStart;
+                System.arraycopy(mRingLux, mStart, newRingLux, 0, length);
+                System.arraycopy(mRingTime, mStart, newRingTime, 0, length);
+                if (mStart != 0) {
+                    System.arraycopy(mRingLux, 0, newRingLux, length, mStart);
+                    System.arraycopy(mRingTime, 0, newRingTime, length, mStart);
+                }
+                mRingLux = newRingLux;
+                mRingTime = newRingTime;
+
+                next = mCapacity;
+                mCapacity = newSize;
+                mStart = 0;
+            }
+            mRingTime[next] = time;
+            mRingLux[next] = lux;
+            mEnd = next + 1;
+            if (mEnd == mCapacity) {
+                mEnd = 0;
+            }
+            mCount++;
+        }
+
+        public void prune(long horizon) {
+            if (mCount == 0) {
+                return;
+            }
+
+            while (mCount > 1) {
+                int next = mStart + 1;
+                if (next >= mCapacity) {
+                    next -= mCapacity;
+                }
+                if (mRingTime[next] > horizon) {
+                    // Some light sensors only produce data upon a change in the ambient light
+                    // levels, so we need to consider the previous measurement as the ambient light
+                    // level for all points in time up until we receive a new measurement. Thus, we
+                    // always want to keep the youngest element that would be removed from the
+                    // buffer and just set its measurement time to the horizon time since at that
+                    // point it is the ambient light level, and to remove it would be to drop a
+                    // valid data point within our horizon.
+                    break;
+                }
+                mStart = next;
+                mCount -= 1;
+            }
+
+            if (mRingTime[mStart] < horizon) {
+                mRingTime[mStart] = horizon;
+            }
+        }
+
+        public int size() {
+            return mCount;
+        }
+
+        public void clear() {
+            mStart = 0;
+            mEnd = 0;
+            mCount = 0;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder buf = new StringBuilder();
+            buf.append('[');
+            for (int i = 0; i < mCount; i++) {
+                final long next = i + 1 < mCount ? getTime(i + 1) : mClock.uptimeMillis();
+                if (i != 0) {
+                    buf.append(", ");
+                }
+                buf.append(getLux(i));
+                buf.append(" / ");
+                buf.append(next - getTime(i));
+                buf.append("ms");
+            }
+            buf.append(']');
+            return buf.toString();
+        }
+
+        private int offsetOf(int index) {
+            if (index >= mCount || index < 0) {
+                throw new ArrayIndexOutOfBoundsException(index);
+            }
+            index += mStart;
+            if (index >= mCapacity) {
+                index -= mCapacity;
+            }
+            return index;
+        }
+    }
+
     public static class Injector {
         public Handler getBackgroundThreadHandler() {
             return BackgroundThread.getHandler();
         }
 
         Clock createClock() {
-            return Clock.SYSTEM_CLOCK;
+            return SystemClock::uptimeMillis;
         }
     }
 }
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index 641b6a2..61ecb93 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -1930,7 +1930,6 @@
      *
      * @return true if even dimmer mode is enabled
      */
-    @VisibleForTesting
     public boolean isEvenDimmerAvailable() {
         return mEvenDimmerBrightnessData != null;
     }
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 043ff0e..b21a89a 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -82,7 +82,6 @@
 import com.android.server.display.brightness.BrightnessReason;
 import com.android.server.display.brightness.BrightnessUtils;
 import com.android.server.display.brightness.DisplayBrightnessController;
-import com.android.server.display.brightness.LightSensorController;
 import com.android.server.display.brightness.clamper.BrightnessClamperController;
 import com.android.server.display.brightness.strategy.AutomaticBrightnessStrategy;
 import com.android.server.display.color.ColorDisplayService.ColorDisplayServiceInternal;
@@ -1050,13 +1049,102 @@
         }
 
         if (defaultModeBrightnessMapper != null) {
+            // Ambient Lux - Active Mode Brightness Thresholds
+            float[] ambientBrighteningThresholds =
+                    mDisplayDeviceConfig.getAmbientBrighteningPercentages();
+            float[] ambientDarkeningThresholds =
+                    mDisplayDeviceConfig.getAmbientDarkeningPercentages();
+            float[] ambientBrighteningLevels =
+                    mDisplayDeviceConfig.getAmbientBrighteningLevels();
+            float[] ambientDarkeningLevels =
+                    mDisplayDeviceConfig.getAmbientDarkeningLevels();
+            float ambientDarkeningMinThreshold =
+                    mDisplayDeviceConfig.getAmbientLuxDarkeningMinThreshold();
+            float ambientBrighteningMinThreshold =
+                    mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold();
+            HysteresisLevels ambientBrightnessThresholds = mInjector.getHysteresisLevels(
+                    ambientBrighteningThresholds, ambientDarkeningThresholds,
+                    ambientBrighteningLevels, ambientDarkeningLevels, ambientDarkeningMinThreshold,
+                    ambientBrighteningMinThreshold);
+
             // Display - Active Mode Brightness Thresholds
-            HysteresisLevels screenBrightnessThresholds =
-                    mInjector.getBrightnessThresholdsHysteresisLevels(mDisplayDeviceConfig);
+            float[] screenBrighteningThresholds =
+                    mDisplayDeviceConfig.getScreenBrighteningPercentages();
+            float[] screenDarkeningThresholds =
+                    mDisplayDeviceConfig.getScreenDarkeningPercentages();
+            float[] screenBrighteningLevels =
+                    mDisplayDeviceConfig.getScreenBrighteningLevels();
+            float[] screenDarkeningLevels =
+                    mDisplayDeviceConfig.getScreenDarkeningLevels();
+            float screenDarkeningMinThreshold =
+                    mDisplayDeviceConfig.getScreenDarkeningMinThreshold();
+            float screenBrighteningMinThreshold =
+                    mDisplayDeviceConfig.getScreenBrighteningMinThreshold();
+            HysteresisLevels screenBrightnessThresholds = mInjector.getHysteresisLevels(
+                    screenBrighteningThresholds, screenDarkeningThresholds,
+                    screenBrighteningLevels, screenDarkeningLevels, screenDarkeningMinThreshold,
+                    screenBrighteningMinThreshold, true);
+
+            // Ambient Lux - Idle Screen Brightness Thresholds
+            float ambientDarkeningMinThresholdIdle =
+                    mDisplayDeviceConfig.getAmbientLuxDarkeningMinThresholdIdle();
+            float ambientBrighteningMinThresholdIdle =
+                    mDisplayDeviceConfig.getAmbientLuxBrighteningMinThresholdIdle();
+            float[] ambientBrighteningThresholdsIdle =
+                    mDisplayDeviceConfig.getAmbientBrighteningPercentagesIdle();
+            float[] ambientDarkeningThresholdsIdle =
+                    mDisplayDeviceConfig.getAmbientDarkeningPercentagesIdle();
+            float[] ambientBrighteningLevelsIdle =
+                    mDisplayDeviceConfig.getAmbientBrighteningLevelsIdle();
+            float[] ambientDarkeningLevelsIdle =
+                    mDisplayDeviceConfig.getAmbientDarkeningLevelsIdle();
+            HysteresisLevels ambientBrightnessThresholdsIdle = mInjector.getHysteresisLevels(
+                    ambientBrighteningThresholdsIdle, ambientDarkeningThresholdsIdle,
+                    ambientBrighteningLevelsIdle, ambientDarkeningLevelsIdle,
+                    ambientDarkeningMinThresholdIdle, ambientBrighteningMinThresholdIdle);
 
             // Display - Idle Screen Brightness Thresholds
-            HysteresisLevels screenBrightnessThresholdsIdle =
-                    mInjector.getBrightnessThresholdsIdleHysteresisLevels(mDisplayDeviceConfig);
+            float screenDarkeningMinThresholdIdle =
+                    mDisplayDeviceConfig.getScreenDarkeningMinThresholdIdle();
+            float screenBrighteningMinThresholdIdle =
+                    mDisplayDeviceConfig.getScreenBrighteningMinThresholdIdle();
+            float[] screenBrighteningThresholdsIdle =
+                    mDisplayDeviceConfig.getScreenBrighteningPercentagesIdle();
+            float[] screenDarkeningThresholdsIdle =
+                    mDisplayDeviceConfig.getScreenDarkeningPercentagesIdle();
+            float[] screenBrighteningLevelsIdle =
+                    mDisplayDeviceConfig.getScreenBrighteningLevelsIdle();
+            float[] screenDarkeningLevelsIdle =
+                    mDisplayDeviceConfig.getScreenDarkeningLevelsIdle();
+            HysteresisLevels screenBrightnessThresholdsIdle = mInjector.getHysteresisLevels(
+                    screenBrighteningThresholdsIdle, screenDarkeningThresholdsIdle,
+                    screenBrighteningLevelsIdle, screenDarkeningLevelsIdle,
+                    screenDarkeningMinThresholdIdle, screenBrighteningMinThresholdIdle);
+
+            long brighteningLightDebounce = mDisplayDeviceConfig
+                    .getAutoBrightnessBrighteningLightDebounce();
+            long darkeningLightDebounce = mDisplayDeviceConfig
+                    .getAutoBrightnessDarkeningLightDebounce();
+            long brighteningLightDebounceIdle = mDisplayDeviceConfig
+                    .getAutoBrightnessBrighteningLightDebounceIdle();
+            long darkeningLightDebounceIdle = mDisplayDeviceConfig
+                    .getAutoBrightnessDarkeningLightDebounceIdle();
+            boolean autoBrightnessResetAmbientLuxAfterWarmUp = context.getResources().getBoolean(
+                    R.bool.config_autoBrightnessResetAmbientLuxAfterWarmUp);
+
+            int lightSensorWarmUpTimeConfig = context.getResources().getInteger(
+                    R.integer.config_lightSensorWarmupTime);
+            int lightSensorRate = context.getResources().getInteger(
+                    R.integer.config_autoBrightnessLightSensorRate);
+            int initialLightSensorRate = context.getResources().getInteger(
+                    R.integer.config_autoBrightnessInitialLightSensorRate);
+            if (initialLightSensorRate == -1) {
+                initialLightSensorRate = lightSensorRate;
+            } else if (initialLightSensorRate > lightSensorRate) {
+                Slog.w(mTag, "Expected config_autoBrightnessInitialLightSensorRate ("
+                        + initialLightSensorRate + ") to be less than or equal to "
+                        + "config_autoBrightnessLightSensorRate (" + lightSensorRate + ").");
+            }
 
             loadAmbientLightSensor();
             // BrightnessTracker should only use one light sensor, we want to use the light sensor
@@ -1068,15 +1156,17 @@
             if (mAutomaticBrightnessController != null) {
                 mAutomaticBrightnessController.stop();
             }
-
-            LightSensorController.LightSensorControllerConfig config =
-                    mInjector.getLightSensorControllerConfig(context, mDisplayDeviceConfig);
             mAutomaticBrightnessController = mInjector.getAutomaticBrightnessController(
-                    this, handler.getLooper(), mSensorManager, brightnessMappers,
-                    PowerManager.BRIGHTNESS_MIN, PowerManager.BRIGHTNESS_MAX, mDozeScaleFactor,
-                    screenBrightnessThresholds, screenBrightnessThresholdsIdle,
-                    mContext, mBrightnessRangeController,
-                    mBrightnessThrottler, userLux, userNits, mDisplayId, config,
+                    this, handler.getLooper(), mSensorManager, mLightSensor,
+                    brightnessMappers, lightSensorWarmUpTimeConfig, PowerManager.BRIGHTNESS_MIN,
+                    PowerManager.BRIGHTNESS_MAX, mDozeScaleFactor, lightSensorRate,
+                    initialLightSensorRate, brighteningLightDebounce, darkeningLightDebounce,
+                    brighteningLightDebounceIdle, darkeningLightDebounceIdle,
+                    autoBrightnessResetAmbientLuxAfterWarmUp, ambientBrightnessThresholds,
+                    screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
+                    screenBrightnessThresholdsIdle, mContext, mBrightnessRangeController,
+                    mBrightnessThrottler, mDisplayDeviceConfig.getAmbientHorizonShort(),
+                    mDisplayDeviceConfig.getAmbientHorizonLong(), userLux, userNits,
                     mBrightnessClamperController);
             mDisplayBrightnessController.setAutomaticBrightnessController(
                     mAutomaticBrightnessController);
@@ -3083,34 +3173,32 @@
 
         AutomaticBrightnessController getAutomaticBrightnessController(
                 AutomaticBrightnessController.Callbacks callbacks, Looper looper,
-                SensorManager sensorManager,
+                SensorManager sensorManager, Sensor lightSensor,
                 SparseArray<BrightnessMappingStrategy> brightnessMappingStrategyMap,
-                float brightnessMin, float brightnessMax, float dozeScaleFactor,
+                int lightSensorWarmUpTime, float brightnessMin, float brightnessMax,
+                float dozeScaleFactor, int lightSensorRate, int initialLightSensorRate,
+                long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
+                long brighteningLightDebounceConfigIdle, long darkeningLightDebounceConfigIdle,
+                boolean resetAmbientLuxAfterWarmUpConfig,
+                HysteresisLevels ambientBrightnessThresholds,
                 HysteresisLevels screenBrightnessThresholds,
+                HysteresisLevels ambientBrightnessThresholdsIdle,
                 HysteresisLevels screenBrightnessThresholdsIdle, Context context,
                 BrightnessRangeController brightnessModeController,
-                BrightnessThrottler brightnessThrottler, float userLux, float userNits,
-                int displayId, LightSensorController.LightSensorControllerConfig config,
+                BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
+                int ambientLightHorizonLong, float userLux, float userNits,
                 BrightnessClamperController brightnessClamperController) {
-            return new AutomaticBrightnessController(callbacks, looper, sensorManager,
-                    brightnessMappingStrategyMap, brightnessMin, brightnessMax, dozeScaleFactor,
-                    screenBrightnessThresholds, screenBrightnessThresholdsIdle, context,
-                    brightnessModeController, brightnessThrottler, userLux, userNits, displayId,
-                    config, brightnessClamperController);
-        }
 
-        LightSensorController.LightSensorControllerConfig getLightSensorControllerConfig(
-                Context context, DisplayDeviceConfig displayDeviceConfig) {
-            return LightSensorController.LightSensorControllerConfig.create(
-                    context.getResources(), displayDeviceConfig);
-        }
-
-        HysteresisLevels getBrightnessThresholdsIdleHysteresisLevels(DisplayDeviceConfig ddc) {
-            return HysteresisLevels.getBrightnessThresholdsIdle(ddc);
-        }
-
-        HysteresisLevels getBrightnessThresholdsHysteresisLevels(DisplayDeviceConfig ddc) {
-            return HysteresisLevels.getBrightnessThresholds(ddc);
+            return new AutomaticBrightnessController(callbacks, looper, sensorManager, lightSensor,
+                    brightnessMappingStrategyMap, lightSensorWarmUpTime, brightnessMin,
+                    brightnessMax, dozeScaleFactor, lightSensorRate, initialLightSensorRate,
+                    brighteningLightDebounceConfig, darkeningLightDebounceConfig,
+                    brighteningLightDebounceConfigIdle, darkeningLightDebounceConfigIdle,
+                    resetAmbientLuxAfterWarmUpConfig, ambientBrightnessThresholds,
+                    screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
+                    screenBrightnessThresholdsIdle, context, brightnessModeController,
+                    brightnessThrottler, ambientLightHorizonShort, ambientLightHorizonLong, userLux,
+                    userNits, brightnessClamperController);
         }
 
         BrightnessMappingStrategy getDefaultModeBrightnessMapper(Context context,
@@ -3120,6 +3208,25 @@
                     AUTO_BRIGHTNESS_MODE_DEFAULT, displayWhiteBalanceController);
         }
 
+        HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+                float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+                float[] darkeningThresholdLevels, float minDarkeningThreshold,
+                float minBrighteningThreshold) {
+            return new HysteresisLevels(brighteningThresholdsPercentages,
+                    darkeningThresholdsPercentages, brighteningThresholdLevels,
+                    darkeningThresholdLevels, minDarkeningThreshold, minBrighteningThreshold);
+        }
+
+        HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+                float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+                float[] darkeningThresholdLevels, float minDarkeningThreshold,
+                float minBrighteningThreshold, boolean potentialOldBrightnessRange) {
+            return new HysteresisLevels(brighteningThresholdsPercentages,
+                    darkeningThresholdsPercentages, brighteningThresholdLevels,
+                    darkeningThresholdLevels, minDarkeningThreshold, minBrighteningThreshold,
+                    potentialOldBrightnessRange);
+        }
+
         ScreenOffBrightnessSensorController getScreenOffBrightnessSensorController(
                 SensorManager sensorManager,
                 Sensor lightSensor,
diff --git a/services/core/java/com/android/server/display/HysteresisLevels.java b/services/core/java/com/android/server/display/HysteresisLevels.java
index bb349e7..0521b8a 100644
--- a/services/core/java/com/android/server/display/HysteresisLevels.java
+++ b/services/core/java/com/android/server/display/HysteresisLevels.java
@@ -18,7 +18,6 @@
 
 import android.util.Slog;
 
-import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.display.utils.DebugUtils;
 
 import java.io.PrintWriter;
@@ -53,8 +52,7 @@
      * @param potentialOldBrightnessRange whether or not the values used could be from the old
      *                                    screen brightness range ie, between 1-255.
     */
-    @VisibleForTesting
-    public HysteresisLevels(float[] brighteningThresholdsPercentages,
+    HysteresisLevels(float[] brighteningThresholdsPercentages,
             float[] darkeningThresholdsPercentages,
             float[] brighteningThresholdLevels, float[] darkeningThresholdLevels,
             float minDarkeningThreshold, float minBrighteningThreshold,
@@ -140,10 +138,7 @@
         return levelArray;
     }
 
-    /**
-     * Print the object's debug information into the given stream.
-     */
-    public void dump(PrintWriter pw) {
+    void dump(PrintWriter pw) {
         pw.println("HysteresisLevels");
         pw.println("  mBrighteningThresholdLevels=" + Arrays.toString(mBrighteningThresholdLevels));
         pw.println("  mBrighteningThresholdsPercentages="
@@ -154,45 +149,4 @@
                 + Arrays.toString(mDarkeningThresholdsPercentages));
         pw.println("  mMinDarkening=" + mMinDarkening);
     }
-
-
-    /**
-     * Creates hysteresis levels for Active Ambient Lux
-     */
-    public static HysteresisLevels getAmbientBrightnessThresholds(DisplayDeviceConfig ddc) {
-        return new HysteresisLevels(ddc.getAmbientBrighteningPercentages(),
-                ddc.getAmbientDarkeningPercentages(), ddc.getAmbientBrighteningLevels(),
-                ddc.getAmbientDarkeningLevels(), ddc.getAmbientLuxDarkeningMinThreshold(),
-                ddc.getAmbientLuxBrighteningMinThreshold());
-    }
-
-    /**
-     * Creates hysteresis levels for Active Screen Brightness
-     */
-    public static HysteresisLevels getBrightnessThresholds(DisplayDeviceConfig ddc) {
-        return new HysteresisLevels(ddc.getScreenBrighteningPercentages(),
-                ddc.getScreenDarkeningPercentages(), ddc.getScreenBrighteningLevels(),
-                ddc.getScreenDarkeningLevels(), ddc.getScreenDarkeningMinThreshold(),
-                ddc.getScreenBrighteningMinThreshold(), true);
-    }
-
-    /**
-     * Creates hysteresis levels for Idle Ambient Lux
-     */
-    public static HysteresisLevels getAmbientBrightnessThresholdsIdle(DisplayDeviceConfig ddc) {
-        return new HysteresisLevels(ddc.getAmbientBrighteningPercentagesIdle(),
-                ddc.getAmbientDarkeningPercentagesIdle(), ddc.getAmbientBrighteningLevelsIdle(),
-                ddc.getAmbientDarkeningLevelsIdle(), ddc.getAmbientLuxDarkeningMinThresholdIdle(),
-                ddc.getAmbientLuxBrighteningMinThresholdIdle());
-    }
-
-    /**
-     * Creates hysteresis levels for Idle Screen Brightness
-     */
-    public static HysteresisLevels getBrightnessThresholdsIdle(DisplayDeviceConfig ddc) {
-        return new HysteresisLevels(ddc.getScreenBrighteningPercentagesIdle(),
-                ddc.getScreenDarkeningPercentagesIdle(), ddc.getScreenBrighteningLevelsIdle(),
-                ddc.getScreenDarkeningLevelsIdle(), ddc.getScreenDarkeningMinThresholdIdle(),
-                ddc.getScreenBrighteningMinThresholdIdle());
-    }
 }
diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
index a577e22..1dfe037 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -940,7 +940,9 @@
                             final float nits = backlightToNits(backlight);
                             final float sdrNits = backlightToNits(sdrBacklight);
 
-                            if (getFeatureFlags().isEvenDimmerEnabled()) {
+                            if (getFeatureFlags().isEvenDimmerEnabled()
+                                    && mDisplayDeviceConfig != null
+                                    && mDisplayDeviceConfig.isEvenDimmerAvailable()) {
                                 applyColorMatrixBasedDimming(brightnessState);
                             }
 
diff --git a/services/core/java/com/android/server/display/brightness/LightSensorController.java b/services/core/java/com/android/server/display/brightness/LightSensorController.java
deleted file mode 100644
index d82d698..0000000
--- a/services/core/java/com/android/server/display/brightness/LightSensorController.java
+++ /dev/null
@@ -1,868 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.display.brightness;
-
-import static com.android.server.display.BrightnessMappingStrategy.INVALID_LUX;
-
-import android.annotation.Nullable;
-import android.content.res.Resources;
-import android.hardware.Sensor;
-import android.hardware.SensorEvent;
-import android.hardware.SensorEventListener;
-import android.hardware.SensorManager;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.PowerManager;
-import android.os.Trace;
-import android.util.Slog;
-import android.util.TimeUtils;
-import android.view.Display;
-
-import com.android.internal.R;
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.os.Clock;
-import com.android.server.display.DisplayDeviceConfig;
-import com.android.server.display.HysteresisLevels;
-import com.android.server.display.config.SensorData;
-import com.android.server.display.utils.SensorUtils;
-
-import java.io.PrintWriter;
-
-/**
- * Manages light sensor subscription and notifies its listeners about ambient lux changes based on
- * configuration
- */
-public class LightSensorController {
-    // How long the current sensor reading is assumed to be valid beyond the current time.
-    // This provides a bit of prediction, as well as ensures that the weight for the last sample is
-    // non-zero, which in turn ensures that the total weight is non-zero.
-    private static final long AMBIENT_LIGHT_PREDICTION_TIME_MILLIS = 100;
-
-    // Proportional extra capacity of the buffer beyond the expected number of light samples
-    // in the horizon
-    private static final float BUFFER_SLACK = 1.5f;
-
-    private boolean mLoggingEnabled;
-    private boolean mLightSensorEnabled;
-    private long mLightSensorEnableTime;
-    // The current light sensor event rate in milliseconds.
-    private int mCurrentLightSensorRate = -1;
-    // The number of light samples collected since the light sensor was enabled.
-    private int mRecentLightSamples;
-    private float mAmbientLux;
-    // True if mAmbientLux holds a valid value.
-    private boolean mAmbientLuxValid;
-    // The last ambient lux value prior to passing the darkening or brightening threshold.
-    private float mPreThresholdLux;
-    // The most recent light sample.
-    private float mLastObservedLux = INVALID_LUX;
-    // The time of the most light recent sample.
-    private long mLastObservedLuxTime;
-    // The last calculated ambient light level (long time window).
-    private float mSlowAmbientLux;
-    // The last calculated ambient light level (short time window).
-    private float mFastAmbientLux;
-    private volatile boolean mIsIdleMode;
-    // The ambient light level threshold at which to brighten or darken the screen.
-    private float mAmbientBrighteningThreshold;
-    private float mAmbientDarkeningThreshold;
-
-    private final LightSensorControllerConfig mConfig;
-
-    // The light sensor, or null if not available or needed.
-    @Nullable
-    private final Sensor mLightSensor;
-
-    // A ring buffer containing all of the recent ambient light sensor readings.
-    private final AmbientLightRingBuffer mAmbientLightRingBuffer;
-
-    private final Injector mInjector;
-
-    private final SensorEventListener mLightSensorListener = new SensorEventListener() {
-        @Override
-        public void onSensorChanged(SensorEvent event) {
-            if (mLightSensorEnabled) {
-                final long time = mClock.uptimeMillis();
-                final float lux = event.values[0];
-                handleLightSensorEvent(time, lux);
-            }
-        }
-
-        @Override
-        public void onAccuracyChanged(Sensor sensor, int accuracy) {
-            // Not used.
-        }
-    };
-
-    // Runnable used to delay ambient lux update when:
-    // 1) update triggered before configured warm up time
-    // 2) next brightening or darkening transition need to happen
-    private final Runnable mAmbientLuxUpdater = this::updateAmbientLux;
-
-    private final Clock mClock;
-
-    private final Handler mHandler;
-
-    private final String mTag;
-
-    private LightSensorListener mListener;
-
-    public LightSensorController(
-            SensorManager sensorManager,
-            Looper looper,
-            int displayId,
-            LightSensorControllerConfig config) {
-        this(config, new RealInjector(sensorManager, displayId), new LightSensorHandler(looper));
-    }
-
-    @VisibleForTesting
-    LightSensorController(
-            LightSensorControllerConfig config,
-            Injector injector,
-            Handler handler) {
-        if (config.mNormalLightSensorRate <= 0) {
-            throw new IllegalArgumentException("lightSensorRate must be above 0");
-        }
-        mInjector = injector;
-        int bufferInitialCapacity = (int) Math.ceil(
-                config.mAmbientLightHorizonLong * BUFFER_SLACK / config.mNormalLightSensorRate);
-        mClock = injector.getClock();
-        mHandler = handler;
-        mAmbientLightRingBuffer = new AmbientLightRingBuffer(bufferInitialCapacity, mClock);
-        mConfig = config;
-        mLightSensor = mInjector.getLightSensor(mConfig);
-        mTag = mInjector.getTag();
-    }
-
-    public void setListener(LightSensorListener listener) {
-        mListener = listener;
-    }
-
-    /**
-     * @return true if sensor registered, false if sensor already registered
-     */
-    public boolean enableLightSensorIfNeeded() {
-        if (!mLightSensorEnabled) {
-            mLightSensorEnabled = true;
-            mLightSensorEnableTime = mClock.uptimeMillis();
-            mCurrentLightSensorRate = mConfig.mInitialLightSensorRate;
-            mInjector.registerLightSensorListener(
-                    mLightSensorListener, mLightSensor, mCurrentLightSensorRate, mHandler);
-            return true;
-        }
-        return false;
-    }
-
-    /**
-     * @return true if sensor unregistered, false if sensor already unregistered
-     */
-    public boolean disableLightSensorIfNeeded() {
-        if (mLightSensorEnabled) {
-            mLightSensorEnabled = false;
-            mAmbientLuxValid = !mConfig.mResetAmbientLuxAfterWarmUpConfig;
-            if (!mAmbientLuxValid) {
-                mPreThresholdLux = PowerManager.BRIGHTNESS_INVALID_FLOAT;
-            }
-            mRecentLightSamples = 0;
-            mAmbientLightRingBuffer.clear();
-            mCurrentLightSensorRate = -1;
-            mInjector.unregisterLightSensorListener(mLightSensorListener);
-            return true;
-        }
-        return false;
-    }
-
-    public void setLoggingEnabled(boolean loggingEnabled) {
-        mLoggingEnabled = loggingEnabled;
-    }
-
-    /**
-     * Updates BrightnessEvent with LightSensorController details
-     */
-    public void updateBrightnessEvent(BrightnessEvent brightnessEvent) {
-        brightnessEvent.setPreThresholdLux(mPreThresholdLux);
-    }
-
-    /**
-     * Print the object's debug information into the given stream.
-     */
-    public void dump(PrintWriter pw) {
-        pw.println("LightSensorController state:");
-        pw.println("  mLightSensorEnabled=" + mLightSensorEnabled);
-        pw.println("  mLightSensorEnableTime=" + TimeUtils.formatUptime(mLightSensorEnableTime));
-        pw.println("  mCurrentLightSensorRate=" + mCurrentLightSensorRate);
-        pw.println("  mRecentLightSamples=" + mRecentLightSamples);
-        pw.println("  mAmbientLux=" + mAmbientLux);
-        pw.println("  mAmbientLuxValid=" + mAmbientLuxValid);
-        pw.println("  mPreThresholdLux=" + mPreThresholdLux);
-        pw.println("  mLastObservedLux=" + mLastObservedLux);
-        pw.println("  mLastObservedLuxTime=" + TimeUtils.formatUptime(mLastObservedLuxTime));
-        pw.println("  mSlowAmbientLux=" + mSlowAmbientLux);
-        pw.println("  mFastAmbientLux=" + mFastAmbientLux);
-        pw.println("  mIsIdleMode=" + mIsIdleMode);
-        pw.println("  mAmbientBrighteningThreshold=" + mAmbientBrighteningThreshold);
-        pw.println("  mAmbientDarkeningThreshold=" + mAmbientDarkeningThreshold);
-        pw.println("  mAmbientLightRingBuffer=" + mAmbientLightRingBuffer);
-        pw.println("  mLightSensor=" + mLightSensor);
-        mConfig.dump(pw);
-    }
-
-    /**
-     * This method should be called when this LightSensorController is no longer in use
-     * i.e. when corresponding display removed
-     */
-    public void stop() {
-        mHandler.removeCallbacksAndMessages(null);
-        disableLightSensorIfNeeded();
-    }
-
-    public void setIdleMode(boolean isIdleMode) {
-        mIsIdleMode = isIdleMode;
-    }
-
-    /**
-     * returns true if LightSensorController holds valid ambient lux value
-     */
-    public boolean hasValidAmbientLux() {
-        return mAmbientLuxValid;
-    }
-
-    /**
-     * returns all last observed sensor values
-     */
-    public float[] getLastSensorValues() {
-        return mAmbientLightRingBuffer.getAllLuxValues();
-    }
-
-    /**
-     * returns all last observed sensor event timestamps
-     */
-    public long[] getLastSensorTimestamps() {
-        return mAmbientLightRingBuffer.getAllTimestamps();
-    }
-
-    public float getLastObservedLux() {
-        return mLastObservedLux;
-    }
-
-    private void handleLightSensorEvent(long time, float lux) {
-        Trace.traceCounter(Trace.TRACE_TAG_POWER, "ALS", (int) lux);
-        mHandler.removeCallbacks(mAmbientLuxUpdater);
-
-        if (mAmbientLightRingBuffer.size() == 0) {
-            // switch to using the steady-state sample rate after grabbing the initial light sample
-            adjustLightSensorRate(mConfig.mNormalLightSensorRate);
-        }
-        applyLightSensorMeasurement(time, lux);
-        updateAmbientLux(time);
-    }
-
-    private void applyLightSensorMeasurement(long time, float lux) {
-        mRecentLightSamples++;
-        mAmbientLightRingBuffer.prune(time - mConfig.mAmbientLightHorizonLong);
-        mAmbientLightRingBuffer.push(time, lux);
-        // Remember this sample value.
-        mLastObservedLux = lux;
-        mLastObservedLuxTime = time;
-    }
-
-    private void adjustLightSensorRate(int lightSensorRate) {
-        // if the light sensor rate changed, update the sensor listener
-        if (lightSensorRate != mCurrentLightSensorRate) {
-            if (mLoggingEnabled) {
-                Slog.d(mTag, "adjustLightSensorRate: "
-                        + "previousRate=" + mCurrentLightSensorRate + ", "
-                        + "currentRate=" + lightSensorRate);
-            }
-            mCurrentLightSensorRate = lightSensorRate;
-            mInjector.unregisterLightSensorListener(mLightSensorListener);
-            mInjector.registerLightSensorListener(
-                    mLightSensorListener, mLightSensor, lightSensorRate, mHandler);
-        }
-    }
-
-    private void setAmbientLux(float lux) {
-        if (mLoggingEnabled) {
-            Slog.d(mTag, "setAmbientLux(" + lux + ")");
-        }
-        if (lux < 0) {
-            Slog.w(mTag, "Ambient lux was negative, ignoring and setting to 0");
-            lux = 0;
-        }
-        mAmbientLux = lux;
-
-        if (mIsIdleMode) {
-            mAmbientBrighteningThreshold =
-                    mConfig.mAmbientBrightnessThresholdsIdle.getBrighteningThreshold(lux);
-            mAmbientDarkeningThreshold =
-                    mConfig.mAmbientBrightnessThresholdsIdle.getDarkeningThreshold(lux);
-        } else {
-            mAmbientBrighteningThreshold =
-                    mConfig.mAmbientBrightnessThresholds.getBrighteningThreshold(lux);
-            mAmbientDarkeningThreshold =
-                    mConfig.mAmbientBrightnessThresholds.getDarkeningThreshold(lux);
-        }
-
-        mListener.onAmbientLuxChange(mAmbientLux);
-    }
-
-    private float calculateAmbientLux(long now, long horizon) {
-        if (mLoggingEnabled) {
-            Slog.d(mTag, "calculateAmbientLux(" + now + ", " + horizon + ")");
-        }
-        final int size = mAmbientLightRingBuffer.size();
-        if (size == 0) {
-            Slog.e(mTag, "calculateAmbientLux: No ambient light readings available");
-            return -1;
-        }
-
-        // Find the first measurement that is just outside of the horizon.
-        int endIndex = 0;
-        final long horizonStartTime = now - horizon;
-        for (int i = 0; i < size - 1; i++) {
-            if (mAmbientLightRingBuffer.getTime(i + 1) <= horizonStartTime) {
-                endIndex++;
-            } else {
-                break;
-            }
-        }
-        if (mLoggingEnabled) {
-            Slog.d(mTag, "calculateAmbientLux: selected endIndex=" + endIndex + ", point=("
-                    + mAmbientLightRingBuffer.getTime(endIndex) + ", "
-                    + mAmbientLightRingBuffer.getLux(endIndex) + ")");
-        }
-        float sum = 0;
-        float totalWeight = 0;
-        long endTime = AMBIENT_LIGHT_PREDICTION_TIME_MILLIS;
-        for (int i = size - 1; i >= endIndex; i--) {
-            long eventTime = mAmbientLightRingBuffer.getTime(i);
-            if (i == endIndex && eventTime < horizonStartTime) {
-                // If we're at the final value, make sure we only consider the part of the sample
-                // within our desired horizon.
-                eventTime = horizonStartTime;
-            }
-            final long startTime = eventTime - now;
-            float weight = calculateWeight(startTime, endTime);
-            float lux = mAmbientLightRingBuffer.getLux(i);
-            if (mLoggingEnabled) {
-                Slog.d(mTag, "calculateAmbientLux: [" + startTime + ", " + endTime + "]: "
-                        + "lux=" + lux + ", "
-                        + "weight=" + weight);
-            }
-            totalWeight += weight;
-            sum += lux * weight;
-            endTime = startTime;
-        }
-        if (mLoggingEnabled) {
-            Slog.d(mTag, "calculateAmbientLux: "
-                    + "totalWeight=" + totalWeight + ", "
-                    + "newAmbientLux=" + (sum / totalWeight));
-        }
-        return sum / totalWeight;
-    }
-
-    private float calculateWeight(long startDelta, long endDelta) {
-        return weightIntegral(endDelta) - weightIntegral(startDelta);
-    }
-
-    // Evaluates the integral of y = x + mWeightingIntercept. This is always positive for the
-    // horizon we're looking at and provides a non-linear weighting for light samples.
-    private float weightIntegral(long x) {
-        return x * (x * 0.5f + mConfig.mWeightingIntercept);
-    }
-
-    private long nextAmbientLightBrighteningTransition(long time) {
-        final int size = mAmbientLightRingBuffer.size();
-        long earliestValidTime = time;
-        for (int i = size - 1; i >= 0; i--) {
-            if (mAmbientLightRingBuffer.getLux(i) <= mAmbientBrighteningThreshold) {
-                break;
-            }
-            earliestValidTime = mAmbientLightRingBuffer.getTime(i);
-        }
-        return earliestValidTime + (mIsIdleMode ? mConfig.mBrighteningLightDebounceConfigIdle
-                : mConfig.mBrighteningLightDebounceConfig);
-    }
-
-    private long nextAmbientLightDarkeningTransition(long time) {
-        final int size = mAmbientLightRingBuffer.size();
-        long earliestValidTime = time;
-        for (int i = size - 1; i >= 0; i--) {
-            if (mAmbientLightRingBuffer.getLux(i) >= mAmbientDarkeningThreshold) {
-                break;
-            }
-            earliestValidTime = mAmbientLightRingBuffer.getTime(i);
-        }
-        return earliestValidTime + (mIsIdleMode ? mConfig.mDarkeningLightDebounceConfigIdle
-                : mConfig.mDarkeningLightDebounceConfig);
-    }
-
-    private void updateAmbientLux() {
-        long time = mClock.uptimeMillis();
-        mAmbientLightRingBuffer.prune(time - mConfig.mAmbientLightHorizonLong);
-        updateAmbientLux(time);
-    }
-
-    private void updateAmbientLux(long time) {
-        // If the light sensor was just turned on then immediately update our initial
-        // estimate of the current ambient light level.
-        if (!mAmbientLuxValid) {
-            final long timeWhenSensorWarmedUp =
-                    mConfig.mLightSensorWarmUpTimeConfig + mLightSensorEnableTime;
-            if (time < timeWhenSensorWarmedUp) {
-                if (mLoggingEnabled) {
-                    Slog.d(mTag, "updateAmbientLux: Sensor not ready yet: "
-                            + "time=" + time + ", "
-                            + "timeWhenSensorWarmedUp=" + timeWhenSensorWarmedUp);
-                }
-                mHandler.postAtTime(mAmbientLuxUpdater, timeWhenSensorWarmedUp);
-                return;
-            }
-            mAmbientLuxValid = true;
-            setAmbientLux(calculateAmbientLux(time, mConfig.mAmbientLightHorizonShort));
-            if (mLoggingEnabled) {
-                Slog.d(mTag, "updateAmbientLux: Initializing: "
-                        + "mAmbientLightRingBuffer=" + mAmbientLightRingBuffer + ", "
-                        + "mAmbientLux=" + mAmbientLux);
-            }
-        }
-
-        long nextBrightenTransition = nextAmbientLightBrighteningTransition(time);
-        long nextDarkenTransition = nextAmbientLightDarkeningTransition(time);
-        // Essentially, we calculate both a slow ambient lux, to ensure there's a true long-term
-        // change in lighting conditions, and a fast ambient lux to determine what the new
-        // brightness situation is since the slow lux can be quite slow to converge.
-        //
-        // Note that both values need to be checked for sufficient change before updating the
-        // proposed ambient light value since the slow value might be sufficiently far enough away
-        // from the fast value to cause a recalculation while its actually just converging on
-        // the fast value still.
-        mSlowAmbientLux = calculateAmbientLux(time, mConfig.mAmbientLightHorizonLong);
-        mFastAmbientLux = calculateAmbientLux(time, mConfig.mAmbientLightHorizonShort);
-
-        if ((mSlowAmbientLux >= mAmbientBrighteningThreshold
-                && mFastAmbientLux >= mAmbientBrighteningThreshold
-                && nextBrightenTransition <= time)
-                || (mSlowAmbientLux <= mAmbientDarkeningThreshold
-                && mFastAmbientLux <= mAmbientDarkeningThreshold
-                && nextDarkenTransition <= time)) {
-            mPreThresholdLux = mAmbientLux;
-            setAmbientLux(mFastAmbientLux);
-            if (mLoggingEnabled) {
-                Slog.d(mTag, "updateAmbientLux: "
-                        + ((mFastAmbientLux > mAmbientLux) ? "Brightened" : "Darkened") + ": "
-                        + "mAmbientBrighteningThreshold=" + mAmbientBrighteningThreshold + ", "
-                        + "mAmbientDarkeningThreshold=" + mAmbientDarkeningThreshold + ", "
-                        + "mAmbientLightRingBuffer=" + mAmbientLightRingBuffer + ", "
-                        + "mAmbientLux=" + mAmbientLux);
-            }
-            nextBrightenTransition = nextAmbientLightBrighteningTransition(time);
-            nextDarkenTransition = nextAmbientLightDarkeningTransition(time);
-        }
-        long nextTransitionTime = Math.min(nextDarkenTransition, nextBrightenTransition);
-        // If one of the transitions is ready to occur, but the total weighted ambient lux doesn't
-        // exceed the necessary threshold, then it's possible we'll get a transition time prior to
-        // now. Rather than continually checking to see whether the weighted lux exceeds the
-        // threshold, schedule an update for when we'd normally expect another light sample, which
-        // should be enough time to decide whether we should actually transition to the new
-        // weighted ambient lux or not.
-        nextTransitionTime = nextTransitionTime > time ? nextTransitionTime
-                : time + mConfig.mNormalLightSensorRate;
-        if (mLoggingEnabled) {
-            Slog.d(mTag, "updateAmbientLux: Scheduling ambient lux update for "
-                    + nextTransitionTime + TimeUtils.formatUptime(nextTransitionTime));
-        }
-        mHandler.postAtTime(mAmbientLuxUpdater, nextTransitionTime);
-    }
-
-    public interface LightSensorListener {
-        /**
-         * Called when new ambient lux value is ready
-         */
-        void onAmbientLuxChange(float ambientLux);
-    }
-
-    private static final class LightSensorHandler extends Handler {
-        private LightSensorHandler(Looper looper) {
-            super(looper, /* callback= */ null, /* async= */ true);
-        }
-    }
-
-    /**
-     * A ring buffer of ambient light measurements sorted by time.
-     * Each entry consists of a timestamp and a lux measurement, and the overall buffer is sorted
-     * from oldest to newest.
-     */
-    @VisibleForTesting
-    static final class AmbientLightRingBuffer {
-
-        private float[] mRingLux;
-        private long[] mRingTime;
-        private int mCapacity;
-
-        // The first valid element and the next open slot.
-        // Note that if mCount is zero then there are no valid elements.
-        private int mStart;
-        private int mEnd;
-        private int mCount;
-
-        private final Clock mClock;
-
-        @VisibleForTesting
-        AmbientLightRingBuffer(int initialCapacity, Clock clock) {
-            mCapacity = initialCapacity;
-            mRingLux = new float[mCapacity];
-            mRingTime = new long[mCapacity];
-            mClock = clock;
-
-        }
-
-        @VisibleForTesting
-        float getLux(int index) {
-            return mRingLux[offsetOf(index)];
-        }
-
-        @VisibleForTesting
-        float[] getAllLuxValues() {
-            float[] values = new float[mCount];
-            if (mCount == 0) {
-                return values;
-            }
-
-            if (mStart < mEnd) {
-                System.arraycopy(mRingLux, mStart, values, 0, mCount);
-            } else {
-                System.arraycopy(mRingLux, mStart, values, 0, mCapacity - mStart);
-                System.arraycopy(mRingLux, 0, values, mCapacity - mStart, mEnd);
-            }
-
-            return values;
-        }
-
-        @VisibleForTesting
-        long getTime(int index) {
-            return mRingTime[offsetOf(index)];
-        }
-
-        @VisibleForTesting
-        long[] getAllTimestamps() {
-            long[] values = new long[mCount];
-            if (mCount == 0) {
-                return values;
-            }
-
-            if (mStart < mEnd) {
-                System.arraycopy(mRingTime, mStart, values, 0, mCount);
-            } else {
-                System.arraycopy(mRingTime, mStart, values, 0, mCapacity - mStart);
-                System.arraycopy(mRingTime, 0, values, mCapacity - mStart, mEnd);
-            }
-
-            return values;
-        }
-
-        @VisibleForTesting
-        void push(long time, float lux) {
-            int next = mEnd;
-            if (mCount == mCapacity) {
-                int newSize = mCapacity * 2;
-
-                float[] newRingLux = new float[newSize];
-                long[] newRingTime = new long[newSize];
-                int length = mCapacity - mStart;
-                System.arraycopy(mRingLux, mStart, newRingLux, 0, length);
-                System.arraycopy(mRingTime, mStart, newRingTime, 0, length);
-                if (mStart != 0) {
-                    System.arraycopy(mRingLux, 0, newRingLux, length, mStart);
-                    System.arraycopy(mRingTime, 0, newRingTime, length, mStart);
-                }
-                mRingLux = newRingLux;
-                mRingTime = newRingTime;
-
-                next = mCapacity;
-                mCapacity = newSize;
-                mStart = 0;
-            }
-            mRingTime[next] = time;
-            mRingLux[next] = lux;
-            mEnd = next + 1;
-            if (mEnd == mCapacity) {
-                mEnd = 0;
-            }
-            mCount++;
-        }
-
-        @VisibleForTesting
-        void prune(long horizon) {
-            if (mCount == 0) {
-                return;
-            }
-
-            while (mCount > 1) {
-                int next = mStart + 1;
-                if (next >= mCapacity) {
-                    next -= mCapacity;
-                }
-                if (mRingTime[next] > horizon) {
-                    // Some light sensors only produce data upon a change in the ambient light
-                    // levels, so we need to consider the previous measurement as the ambient light
-                    // level for all points in time up until we receive a new measurement. Thus, we
-                    // always want to keep the youngest element that would be removed from the
-                    // buffer and just set its measurement time to the horizon time since at that
-                    // point it is the ambient light level, and to remove it would be to drop a
-                    // valid data point within our horizon.
-                    break;
-                }
-                mStart = next;
-                mCount -= 1;
-            }
-
-            if (mRingTime[mStart] < horizon) {
-                mRingTime[mStart] = horizon;
-            }
-        }
-
-        @VisibleForTesting
-        int size() {
-            return mCount;
-        }
-
-        @VisibleForTesting
-        void clear() {
-            mStart = 0;
-            mEnd = 0;
-            mCount = 0;
-        }
-
-        @Override
-        public String toString() {
-            StringBuilder buf = new StringBuilder();
-            buf.append('[');
-            for (int i = 0; i < mCount; i++) {
-                final long next = i + 1 < mCount ? getTime(i + 1) : mClock.uptimeMillis();
-                if (i != 0) {
-                    buf.append(", ");
-                }
-                buf.append(getLux(i));
-                buf.append(" / ");
-                buf.append(next - getTime(i));
-                buf.append("ms");
-            }
-            buf.append(']');
-            return buf.toString();
-        }
-
-        private int offsetOf(int index) {
-            if (index >= mCount || index < 0) {
-                throw new ArrayIndexOutOfBoundsException(index);
-            }
-            index += mStart;
-            if (index >= mCapacity) {
-                index -= mCapacity;
-            }
-            return index;
-        }
-    }
-
-    @VisibleForTesting
-    interface Injector {
-        Clock getClock();
-
-        Sensor getLightSensor(LightSensorControllerConfig config);
-
-        boolean registerLightSensorListener(
-                SensorEventListener listener, Sensor sensor, int rate, Handler handler);
-
-        void unregisterLightSensorListener(SensorEventListener listener);
-
-        String getTag();
-
-    }
-
-    private static class RealInjector implements Injector {
-        private final SensorManager mSensorManager;
-        private final int mSensorFallbackType;
-
-        private final String mTag;
-
-        private RealInjector(SensorManager sensorManager, int displayId) {
-            mSensorManager = sensorManager;
-            mSensorFallbackType = displayId == Display.DEFAULT_DISPLAY
-                    ? Sensor.TYPE_LIGHT : SensorUtils.NO_FALLBACK;
-            mTag = "LightSensorController [" + displayId + "]";
-        }
-
-        @Override
-        public Clock getClock() {
-            return Clock.SYSTEM_CLOCK;
-        }
-
-        @Override
-        public Sensor getLightSensor(LightSensorControllerConfig config) {
-            return SensorUtils.findSensor(
-                    mSensorManager, config.mAmbientLightSensor, mSensorFallbackType);
-        }
-
-        @Override
-        public boolean registerLightSensorListener(
-                SensorEventListener listener, Sensor sensor, int rate, Handler handler) {
-            return mSensorManager.registerListener(listener, sensor, rate * 1000, handler);
-        }
-
-        @Override
-        public void unregisterLightSensorListener(SensorEventListener listener) {
-            mSensorManager.unregisterListener(listener);
-        }
-
-        @Override
-        public String getTag() {
-            return mTag;
-        }
-    }
-
-    public static class LightSensorControllerConfig {
-        // Steady-state light sensor event rate in milliseconds.
-        private final int mNormalLightSensorRate;
-        private final int mInitialLightSensorRate;
-
-        // If true immediately after the screen is turned on the controller will try to adjust the
-        // brightness based on the current sensor reads. If false, the controller will collect
-        // more data
-        // and only then decide whether to change brightness.
-        private final boolean mResetAmbientLuxAfterWarmUpConfig;
-
-        // Period of time in which to consider light samples for a short/long-term estimate of
-        // ambient
-        // light in milliseconds.
-        private final int mAmbientLightHorizonShort;
-        private final int mAmbientLightHorizonLong;
-
-
-        // Amount of time to delay auto-brightness after screen on while waiting for
-        // the light sensor to warm-up in milliseconds.
-        // May be 0 if no warm-up is required.
-        private final int mLightSensorWarmUpTimeConfig;
-
-
-        // The intercept used for the weighting calculation. This is used in order to keep all
-        // possible
-        // weighting values positive.
-        private final int mWeightingIntercept;
-
-        // Configuration object for determining thresholds to change brightness dynamically
-        private final HysteresisLevels mAmbientBrightnessThresholds;
-        private final HysteresisLevels mAmbientBrightnessThresholdsIdle;
-
-
-        // Stability requirements in milliseconds for accepting a new brightness level.  This is
-        // used
-        // for debouncing the light sensor.  Different constants are used to debounce the light
-        // sensor
-        // when adapting to brighter or darker environments.  This parameter controls how quickly
-        // brightness changes occur in response to an observed change in light level that exceeds
-        // the
-        // hysteresis threshold.
-        private final long mBrighteningLightDebounceConfig;
-        private final long mDarkeningLightDebounceConfig;
-        private final long mBrighteningLightDebounceConfigIdle;
-        private final long mDarkeningLightDebounceConfigIdle;
-
-        private final SensorData mAmbientLightSensor;
-
-        @VisibleForTesting
-        LightSensorControllerConfig(int initialLightSensorRate, int normalLightSensorRate,
-                boolean resetAmbientLuxAfterWarmUpConfig, int ambientLightHorizonShort,
-                int ambientLightHorizonLong, int lightSensorWarmUpTimeConfig,
-                int weightingIntercept, HysteresisLevels ambientBrightnessThresholds,
-                HysteresisLevels ambientBrightnessThresholdsIdle,
-                long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
-                long brighteningLightDebounceConfigIdle, long darkeningLightDebounceConfigIdle,
-                SensorData ambientLightSensor) {
-            mInitialLightSensorRate = initialLightSensorRate;
-            mNormalLightSensorRate = normalLightSensorRate;
-            mResetAmbientLuxAfterWarmUpConfig = resetAmbientLuxAfterWarmUpConfig;
-            mAmbientLightHorizonShort = ambientLightHorizonShort;
-            mAmbientLightHorizonLong = ambientLightHorizonLong;
-            mLightSensorWarmUpTimeConfig = lightSensorWarmUpTimeConfig;
-            mWeightingIntercept = weightingIntercept;
-            mAmbientBrightnessThresholds = ambientBrightnessThresholds;
-            mAmbientBrightnessThresholdsIdle = ambientBrightnessThresholdsIdle;
-            mBrighteningLightDebounceConfig = brighteningLightDebounceConfig;
-            mDarkeningLightDebounceConfig = darkeningLightDebounceConfig;
-            mBrighteningLightDebounceConfigIdle = brighteningLightDebounceConfigIdle;
-            mDarkeningLightDebounceConfigIdle = darkeningLightDebounceConfigIdle;
-            mAmbientLightSensor = ambientLightSensor;
-        }
-
-        private void dump(PrintWriter pw) {
-            pw.println("LightSensorControllerConfig:");
-            pw.println("  mInitialLightSensorRate=" + mInitialLightSensorRate);
-            pw.println("  mNormalLightSensorRate=" + mNormalLightSensorRate);
-            pw.println("  mResetAmbientLuxAfterWarmUpConfig=" + mResetAmbientLuxAfterWarmUpConfig);
-            pw.println("  mAmbientLightHorizonShort=" + mAmbientLightHorizonShort);
-            pw.println("  mAmbientLightHorizonLong=" + mAmbientLightHorizonLong);
-            pw.println("  mLightSensorWarmUpTimeConfig=" + mLightSensorWarmUpTimeConfig);
-            pw.println("  mWeightingIntercept=" + mWeightingIntercept);
-            pw.println("  mAmbientBrightnessThresholds=");
-            mAmbientBrightnessThresholds.dump(pw);
-            pw.println("  mAmbientBrightnessThresholdsIdle=");
-            mAmbientBrightnessThresholdsIdle.dump(pw);
-            pw.println("  mBrighteningLightDebounceConfig=" + mBrighteningLightDebounceConfig);
-            pw.println("  mDarkeningLightDebounceConfig=" + mDarkeningLightDebounceConfig);
-            pw.println(
-                    "  mBrighteningLightDebounceConfigIdle=" + mBrighteningLightDebounceConfigIdle);
-            pw.println("  mDarkeningLightDebounceConfigIdle=" + mDarkeningLightDebounceConfigIdle);
-            pw.println("  mAmbientLightSensor=" + mAmbientLightSensor);
-        }
-
-        /**
-         * Creates LightSensorControllerConfig object form Resources and DisplayDeviceConfig
-         */
-        public static LightSensorControllerConfig create(Resources res, DisplayDeviceConfig ddc) {
-            int lightSensorRate = res.getInteger(R.integer.config_autoBrightnessLightSensorRate);
-            int initialLightSensorRate = res.getInteger(
-                    R.integer.config_autoBrightnessInitialLightSensorRate);
-            if (initialLightSensorRate == -1) {
-                initialLightSensorRate = lightSensorRate;
-            } else if (initialLightSensorRate > lightSensorRate) {
-                Slog.w("LightSensorControllerConfig",
-                        "Expected config_autoBrightnessInitialLightSensorRate ("
-                                + initialLightSensorRate + ") to be less than or equal to "
-                                + "config_autoBrightnessLightSensorRate (" + lightSensorRate
-                                + ").");
-            }
-
-            boolean resetAmbientLuxAfterWarmUp = res.getBoolean(
-                    R.bool.config_autoBrightnessResetAmbientLuxAfterWarmUp);
-            int lightSensorWarmUpTimeConfig = res.getInteger(
-                    R.integer.config_lightSensorWarmupTime);
-
-            return new LightSensorControllerConfig(initialLightSensorRate, lightSensorRate,
-                    resetAmbientLuxAfterWarmUp, ddc.getAmbientHorizonShort(),
-                    ddc.getAmbientHorizonLong(), lightSensorWarmUpTimeConfig,
-                    ddc.getAmbientHorizonLong(),
-                    HysteresisLevels.getAmbientBrightnessThresholds(ddc),
-                    HysteresisLevels.getAmbientBrightnessThresholdsIdle(ddc),
-                    ddc.getAutoBrightnessBrighteningLightDebounce(),
-                    ddc.getAutoBrightnessDarkeningLightDebounce(),
-                    ddc.getAutoBrightnessBrighteningLightDebounceIdle(),
-                    ddc.getAutoBrightnessDarkeningLightDebounceIdle(),
-                    ddc.getAmbientLightSensor()
-            );
-        }
-    }
-}
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
index 9c7504d..a46975fb 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
@@ -285,7 +285,7 @@
             List<BrightnessStateModifier> modifiers = new ArrayList<>();
             modifiers.add(new DisplayDimModifier(context));
             modifiers.add(new BrightnessLowPowerModeModifier());
-            if (flags.isEvenDimmerEnabled()) {
+            if (flags.isEvenDimmerEnabled() && displayDeviceConfig != null) {
                 modifiers.add(new BrightnessLowLuxModifier(handler, listener, context,
                         displayDeviceConfig));
             }
diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
index 572d32e..d084d1c 100644
--- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
@@ -1142,7 +1142,11 @@
                 maxRefreshRate = Math.min(defaultRefreshRate, peakRefreshRate);
             }
 
-            mBrightnessObserver.onRefreshRateSettingChangedLocked(minRefreshRate, maxRefreshRate);
+            // TODO(b/310237068): Make this work for multiple displays
+            if (displayId == Display.DEFAULT_DISPLAY) {
+                mBrightnessObserver.onRefreshRateSettingChangedLocked(minRefreshRate,
+                        maxRefreshRate);
+            }
         }
 
         private void removeRefreshRateSetting(int displayId) {
diff --git a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
index 5563cae..96f32f3 100644
--- a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
+++ b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
@@ -514,12 +514,16 @@
             EventLogTags.writeNotificationAlert(key, buzz ? 1 : 0, beep ? 1 : 0, blink ? 1 : 0,
                     getPolitenessState(record));
         }
-        record.setAudiblyAlerted(buzz || beep);
         if (Flags.politeNotifications()) {
             // Update last alert time
             if (buzz || beep) {
                 mStrategy.setLastNotificationUpdateTimeMs(record, System.currentTimeMillis());
             }
+
+            record.setAudiblyAlerted((buzz || beep)
+                    && getPolitenessState(record) != PolitenessStrategy.POLITE_STATE_MUTED);
+        } else {
+            record.setAudiblyAlerted(buzz || beep);
         }
         return buzzBeepBlinkLoggingCode;
     }
@@ -678,7 +682,7 @@
 
         // The user can choose to apply cooldown for all apps/conversations only from the
         // Settings app
-        if (!mNotificationCooldownApplyToAll && record.getChannel().getConversationId() == null) {
+        if (!mNotificationCooldownApplyToAll && !record.isConversation()) {
             return false;
         }
 
@@ -1203,7 +1207,7 @@
             setLastNotificationUpdateTimeMs(record, 0);
         }
 
-        public final @PolitenessState int getPolitenessState(final NotificationRecord record) {
+        public @PolitenessState int getPolitenessState(final NotificationRecord record) {
             return mVolumeStates.getOrDefault(getChannelKey(record), POLITE_STATE_DEFAULT);
         }
 
@@ -1364,7 +1368,12 @@
 
                 final String key = getChannelKey(record);
                 @PolitenessState final int currState = getPolitenessState(record);
-                @PolitenessState int nextState = getNextState(currState, timeSinceLastNotif);
+                @PolitenessState int nextState;
+                if (Flags.politeNotificationsAttnUpdate()) {
+                    nextState = getNextState(currState, timeSinceLastNotif, record);
+                } else {
+                    nextState = getNextState(currState, timeSinceLastNotif);
+                }
 
                 if (DEBUG) {
                     Log.i(TAG,
@@ -1379,6 +1388,26 @@
             mAppStrategy.onNotificationPosted(record);
         }
 
+        @PolitenessState int getNextState(@PolitenessState final int currState,
+                final long timeSinceLastNotif, final NotificationRecord record) {
+            // Mute all except priority conversations
+            if (!isAvalancheExempted(record)) {
+                return POLITE_STATE_MUTED;
+            }
+            if (isAvalancheExemptedFullVolume(record)) {
+                return POLITE_STATE_DEFAULT;
+            }
+            return getNextState(currState, timeSinceLastNotif);
+        }
+
+        public @PolitenessState int getPolitenessState(final NotificationRecord record) {
+            if (isAvalancheActive()) {
+                return super.getPolitenessState(record);
+            } else {
+                return mAppStrategy.getPolitenessState(record);
+            }
+        }
+
         @Override
         public float getSoundVolume(final NotificationRecord record) {
             if (isAvalancheActive()) {
@@ -1396,13 +1425,27 @@
 
         @Override
         String getChannelKey(final NotificationRecord record) {
-            // If the user explicitly changed the channel notification sound:
-            // handle as a separate channel
-            if (record.getChannel().hasUserSetSound()) {
-                return super.getChannelKey(record);
+            if (isAvalancheActive()) {
+                if (Flags.politeNotificationsAttnUpdate()) {
+                    // Treat high importance conversations independently
+                    if (isAvalancheExempted(record)) {
+                        return super.getChannelKey(record);
+                    } else {
+                        // Use one global key per user
+                        return record.getSbn().getNormalizedUserId() + ":" + COMMON_KEY;
+                    }
+                } else {
+                    // If the user explicitly changed the channel notification sound:
+                    // handle as a separate channel
+                    if (record.getChannel().hasUserSetSound()) {
+                        return super.getChannelKey(record);
+                    } else {
+                        // Use one global key per user
+                        return record.getSbn().getNormalizedUserId() + ":" + COMMON_KEY;
+                    }
+                }
             } else {
-                // Use one global key per user
-                return record.getSbn().getNormalizedUserId() + ":" + COMMON_KEY;
+                return mAppStrategy.getChannelKey(record);
             }
         }
 
@@ -1415,10 +1458,19 @@
         }
 
         long getLastNotificationUpdateTimeMs(final NotificationRecord record) {
-            if (record.getChannel().hasUserSetSound()) {
-                return super.getLastNotificationUpdateTimeMs(record);
+            if (Flags.politeNotificationsAttnUpdate()) {
+                // Mute all except priority conversations
+                if (isAvalancheExempted(record)) {
+                    return super.getLastNotificationUpdateTimeMs(record);
+                } else {
+                    return mLastNotificationTimestamp;
+                }
             } else {
-                return mLastNotificationTimestamp;
+                if (record.getChannel().hasUserSetSound()) {
+                    return super.getLastNotificationUpdateTimeMs(record);
+                } else {
+                    return mLastNotificationTimestamp;
+                }
             }
         }
 
@@ -1445,6 +1497,51 @@
         void setTriggerTimeMs(long timestamp) {
             mLastAvalancheTriggerTimestamp = timestamp;
         }
+
+        private boolean isAvalancheExemptedFullVolume(final NotificationRecord record) {
+            // important conversation
+            if (record.isConversation()
+                    && (record.getImportance() > NotificationManager.IMPORTANCE_DEFAULT
+                    || record.getChannel().isImportantConversation())) {
+                return true;
+            }
+
+            // call notification
+            if (record.getNotification().isStyle(Notification.CallStyle.class)) {
+                return true;
+            }
+
+            // alarm/reminder
+            final String category = record.getNotification().category;
+            if (Notification.CATEGORY_REMINDER.equals(category)
+                    || Notification.CATEGORY_EVENT.equals(category)) {
+                return true;
+            }
+
+            return false;
+        }
+
+        private boolean isAvalancheExempted(final NotificationRecord record) {
+            if (isAvalancheExemptedFullVolume(record)) {
+                return true;
+            }
+
+            // recent conversation
+            if (record.isConversation()
+                    && record.getNotification().when > mLastAvalancheTriggerTimestamp) {
+                return true;
+            }
+
+            if (record.getNotification().fullScreenIntent != null) {
+                return true;
+            }
+
+            if (record.getNotification().isColorized()) {
+                return true;
+            }
+
+            return false;
+        }
     }
 
     //======================  Observers  =============================
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index f48f66f..493c4a0 100755
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -1794,7 +1794,7 @@
             if (Intent.ACTION_LOCALE_CHANGED.equals(intent.getAction())) {
                 // update system notification channels
                 SystemNotificationChannels.createAll(context);
-                mZenModeHelper.updateDefaultZenRules(Binder.getCallingUid());
+                mZenModeHelper.updateZenRulesOnLocaleChange();
                 mPreferencesHelper.onLocaleChanged(context, ActivityManager.getCurrentUser());
             }
         }
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 289faf4..20b7fd4 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -86,6 +86,7 @@
 import android.service.notification.Condition;
 import android.service.notification.ConditionProviderService;
 import android.service.notification.DeviceEffectsApplier;
+import android.service.notification.SystemZenRules;
 import android.service.notification.ZenAdapters;
 import android.service.notification.ZenDeviceEffects;
 import android.service.notification.ZenModeConfig;
@@ -214,7 +215,7 @@
         mNotificationManager = context.getSystemService(NotificationManager.class);
 
         mDefaultConfig = readDefaultConfig(mContext.getResources());
-        updateDefaultAutomaticRuleNames();
+        updateDefaultConfigAutomaticRules();
         if (Flags.modesApi()) {
             updateDefaultAutomaticRulePolicies();
         }
@@ -1020,27 +1021,41 @@
         }
     }
 
-    protected void updateDefaultZenRules(int callingUid) {
-        updateDefaultAutomaticRuleNames();
+    void updateZenRulesOnLocaleChange() {
+        updateDefaultConfigAutomaticRules();
         synchronized (mConfigLock) {
+            if (mConfig == null) {
+                return;
+            }
+            ZenModeConfig config = mConfig.copy();
+            boolean updated = false;
             for (ZenRule defaultRule : mDefaultConfig.automaticRules.values()) {
-                ZenRule currRule = mConfig.automaticRules.get(defaultRule.id);
-                // if default rule wasn't user-modified nor enabled, use localized name
+                ZenRule currRule = config.automaticRules.get(defaultRule.id);
+                // if default rule wasn't user-modified use localized name
                 // instead of previous system name
-                if (currRule != null && !currRule.modified && !currRule.enabled
+                if (currRule != null
+                        && !currRule.modified
+                        && (currRule.zenPolicyUserModifiedFields & AutomaticZenRule.FIELD_NAME) == 0
                         && !defaultRule.name.equals(currRule.name)) {
-                    if (canManageAutomaticZenRule(currRule)) {
-                        if (DEBUG) {
-                            Slog.d(TAG, "Locale change - updating default zen rule name "
-                                    + "from " + currRule.name + " to " + defaultRule.name);
-                        }
-                        // update default rule (if locale changed, name of rule will change)
-                        currRule.name = defaultRule.name;
-                        updateAutomaticZenRule(defaultRule.id, zenRuleToAutomaticZenRule(currRule),
-                                UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI, "locale changed", callingUid);
+                    if (DEBUG) {
+                        Slog.d(TAG, "Locale change - updating default zen rule name "
+                                + "from " + currRule.name + " to " + defaultRule.name);
+                    }
+                    currRule.name = defaultRule.name;
+                    updated = true;
+                }
+            }
+            if (Flags.modesApi() && Flags.modesUi()) {
+                for (ZenRule rule : config.automaticRules.values()) {
+                    if (SystemZenRules.isSystemOwnedRule(rule)) {
+                        updated |= SystemZenRules.updateTriggerDescription(mContext, rule);
                     }
                 }
             }
+            if (updated) {
+                setConfigLocked(config, null, UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI,
+                        "updateZenRulesOnLocaleChange", Process.SYSTEM_UID);
+            }
         }
     }
 
@@ -1631,6 +1646,10 @@
                 reason += ", reset to default rules";
             }
 
+            if (Flags.modesApi() && Flags.modesUi()) {
+                SystemZenRules.maybeUpgradeRules(mContext, config);
+            }
+
             // Resolve user id for settings.
             userId = userId == UserHandle.USER_ALL ? UserHandle.USER_SYSTEM : userId;
             if (config.version < ZenModeConfig.XML_VERSION_ZEN_UPGRADE) {
@@ -2054,7 +2073,7 @@
         }
     }
 
-    private void updateDefaultAutomaticRuleNames() {
+    private void updateDefaultConfigAutomaticRules() {
         for (ZenRule rule : mDefaultConfig.automaticRules.values()) {
             if (ZenModeConfig.EVENTS_DEFAULT_RULE_ID.equals(rule.id)) {
                 rule.name = mContext.getResources()
@@ -2063,6 +2082,9 @@
                 rule.name = mContext.getResources()
                         .getString(R.string.zen_mode_default_every_night_name);
             }
+            if (Flags.modesApi() && Flags.modesUi()) {
+                SystemZenRules.updateTriggerDescription(mContext, rule);
+            }
         }
     }
 
diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig
index 43361ed..afd00af 100644
--- a/services/core/java/com/android/server/notification/flags.aconfig
+++ b/services/core/java/com/android/server/notification/flags.aconfig
@@ -78,4 +78,11 @@
   metadata {
     purpose: PURPOSE_BUGFIX
   }
-}
\ No newline at end of file
+}
+
+flag {
+  name: "polite_notifications_attn_update"
+  namespace: "systemui"
+  description: "This flag controls the polite notification attention behavior updates as per UXR feedback"
+  bug: "270456865"
+}
diff --git a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
index 953300a..d39debb 100644
--- a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
+++ b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
@@ -372,6 +372,7 @@
                             public void onConnected(
                                     @NonNull IOnDeviceIntelligenceService service) {
                                 try {
+                                    service.ready();
                                     service.registerRemoteServices(
                                             getRemoteProcessingService());
                                 } catch (RemoteException ex) {
@@ -538,6 +539,8 @@
                 Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG);
         synchronized (mLock) {
             mTemporaryServiceNames = componentNames;
+            mRemoteInferenceService.unbind();
+            mRemoteOnDeviceIntelligenceService.unbind();
             mRemoteOnDeviceIntelligenceService = null;
             mRemoteInferenceService = null;
             if (mTemporaryHandler == null) {
diff --git a/services/core/java/com/android/server/power/PowerGroup.java b/services/core/java/com/android/server/power/PowerGroup.java
index 9a9c4f2..77bdc45 100644
--- a/services/core/java/com/android/server/power/PowerGroup.java
+++ b/services/core/java/com/android/server/power/PowerGroup.java
@@ -458,9 +458,6 @@
                     mDisplayPowerRequest.dozeScreenStateReason =
                             Display.STATE_REASON_DRAW_WAKE_LOCK;
                 }
-            } else {
-                mDisplayPowerRequest.dozeScreenStateReason =
-                        Display.STATE_REASON_DEFAULT_POLICY;
             }
             mDisplayPowerRequest.dozeScreenBrightness = dozeScreenBrightness;
         } else {
diff --git a/services/core/java/com/android/server/storage/StorageSessionController.java b/services/core/java/com/android/server/storage/StorageSessionController.java
index 5fd787a..b9c9b64 100644
--- a/services/core/java/com/android/server/storage/StorageSessionController.java
+++ b/services/core/java/com/android/server/storage/StorageSessionController.java
@@ -29,6 +29,7 @@
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
 import android.content.pm.UserInfo;
+import android.os.Binder;
 import android.os.IVold;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
@@ -246,17 +247,18 @@
      * Call {@link #onVolumeRemove} to remove the connection without waiting for exit
      */
     public void onVolumeUnmount(VolumeInfo vol) {
-        StorageUserConnection connection = onVolumeRemove(vol);
-
-        Slog.i(TAG, "On volume unmount " + vol);
-        if (connection != null) {
-            String sessionId = vol.getId();
-
-            try {
-                connection.removeSessionAndWait(sessionId);
-            } catch (ExternalStorageServiceException e) {
-                Slog.e(TAG, "Failed to end session for vol with id: " + sessionId, e);
+        String sessionId = vol.getId();
+        final long token = Binder.clearCallingIdentity();
+        try {
+            StorageUserConnection connection = onVolumeRemove(vol);
+            Slog.i(TAG, "On volume unmount " + vol);
+            if (connection != null) {
+              connection.removeSessionAndWait(sessionId);
             }
+        } catch (ExternalStorageServiceException e) {
+            Slog.e(TAG, "Failed to end session for vol with id: " + sessionId, e);
+        } finally {
+            Binder.restoreCallingIdentity(token);
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 6af496f..38f0587a 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -5060,6 +5060,9 @@
                 FrameworkStatsLog.write(FrameworkStatsLog.ACTIVITY_MANAGER_SLEEP_STATE_CHANGED,
                         FrameworkStatsLog.ACTIVITY_MANAGER_SLEEP_STATE_CHANGED__STATE__AWAKE);
                 startTimeTrackingFocusedActivityLocked();
+                if (mTopApp != null) {
+                    mTopApp.addToPendingTop();
+                }
                 mTopProcessState = ActivityManager.PROCESS_STATE_TOP;
                 Slog.d(TAG, "Top Process State changed to PROCESS_STATE_TOP");
                 mTaskSupervisor.comeOutOfSleepIfNeededLocked();
diff --git a/services/core/jni/com_android_server_accessibility_BrailleDisplayConnection.cpp b/services/core/jni/com_android_server_accessibility_BrailleDisplayConnection.cpp
index c337523..180081c 100644
--- a/services/core/jni/com_android_server_accessibility_BrailleDisplayConnection.cpp
+++ b/services/core/jni/com_android_server_accessibility_BrailleDisplayConnection.cpp
@@ -32,10 +32,10 @@
 
 namespace {
 
-// Max size we allow for the result from HIDIOCGRAWUNIQ (Bluetooth address or USB serial number).
-// Copied from linux/hid.h struct hid_device->uniq char array size; the ioctl implementation
-// writes at most this many bytes to the provided buffer.
-constexpr int UNIQ_SIZE_MAX = 64;
+// Max sizes we allow for results from string ioctl calls, copied from UAPI linux/uhid.h.
+// The ioctl implementation writes at most this many bytes to the provided buffer:
+constexpr int NAME_SIZE_MAX = 128; // HIDIOCGRAWNAME (device name)
+constexpr int UNIQ_SIZE_MAX = 64;  // HIDIOCGRAWUNIQ (BT address or USB serial number)
 
 } // anonymous namespace
 
@@ -82,6 +82,16 @@
     return info.bustype;
 }
 
+static jstring com_android_server_accessibility_BrailleDisplayConnection_getHidrawName(
+        JNIEnv* env, jclass /*clazz*/, int fd) {
+    char buf[NAME_SIZE_MAX];
+    if (ioctl(fd, HIDIOCGRAWNAME(NAME_SIZE_MAX), buf) < 0) {
+        return nullptr;
+    }
+    // Local ref is not deleted because it is returned to Java
+    return env->NewStringUTF(buf);
+}
+
 static const JNINativeMethod gMethods[] = {
         {"nativeGetHidrawDescSize", "(I)I",
          (void*)com_android_server_accessibility_BrailleDisplayConnection_getHidrawDescSize},
@@ -91,6 +101,8 @@
          (void*)com_android_server_accessibility_BrailleDisplayConnection_getHidrawUniq},
         {"nativeGetHidrawBusType", "(I)I",
          (void*)com_android_server_accessibility_BrailleDisplayConnection_getHidrawBusType},
+        {"nativeGetHidrawName", "(I)Ljava/lang/String;",
+         (void*)com_android_server_accessibility_BrailleDisplayConnection_getHidrawName},
 };
 
 int register_com_android_server_accessibility_BrailleDisplayConnection(JNIEnv* env) {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index f955b91..c3175d6 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -888,6 +888,8 @@
     private static final String APPLICATION_EXEMPTIONS_FLAG = "application_exemptions";
     private static final boolean DEFAULT_APPLICATION_EXEMPTIONS_FLAG = true;
 
+    private static final int RETRY_COPY_ACCOUNT_ATTEMPTS = 3;
+
     /**
      * For apps targeting U+
      * Enable multiple admins to coexist on the same device.
@@ -21514,13 +21516,26 @@
             Slogf.w(LOG_TAG, "sourceUser and targetUser are the same, won't migrate account.");
             return;
         }
-        copyAccount(targetUser, sourceUser, accountToMigrate, callerPackage);
+
+        if (Flags.copyAccountWithRetryEnabled()) {
+            boolean copySucceeded = false;
+            int retryAttemptsLeft = RETRY_COPY_ACCOUNT_ATTEMPTS;
+            while (!copySucceeded && (retryAttemptsLeft > 0)) {
+                Slogf.i(LOG_TAG, "Copying account. Attempts left : " + retryAttemptsLeft);
+                copySucceeded =
+                        copyAccount(targetUser, sourceUser, accountToMigrate, callerPackage);
+                retryAttemptsLeft--;
+            }
+        } else {
+            copyAccount(targetUser, sourceUser, accountToMigrate, callerPackage);
+        }
         if (!keepAccountMigrated) {
             removeAccount(accountToMigrate, sourceUserId);
         }
+
     }
 
-    private void copyAccount(
+    private boolean copyAccount(
             UserHandle targetUser, UserHandle sourceUser, Account accountToMigrate,
             String callerPackage) {
         final long startTime = SystemClock.elapsedRealtime();
@@ -21538,6 +21553,8 @@
                         DevicePolicyEnums.PLATFORM_PROVISIONING_COPY_ACCOUNT_MS,
                         startTime,
                         callerPackage);
+                Slogf.i(LOG_TAG, "Copy account successful to " + targetUser);
+                return true;
             } else {
                 logCopyAccountStatus(COPY_ACCOUNT_FAILED, callerPackage);
                 Slogf.e(LOG_TAG, "Failed to copy account to " + targetUser);
@@ -21550,6 +21567,7 @@
             logCopyAccountStatus(COPY_ACCOUNT_EXCEPTION, callerPackage);
             Slogf.e(LOG_TAG, "Exception copying account to " + targetUser, e);
         }
+        return false;
     }
 
     private static void logCopyAccountStatus(@CopyAccountStatus int status, String callerPackage) {
diff --git a/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
index dd87572..54de64e 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
@@ -22,7 +22,9 @@
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DOZE;
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_IDLE;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyFloat;
@@ -36,6 +38,9 @@
 
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
+import android.hardware.Sensor;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
 import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest;
 import android.os.Handler;
 import android.os.PowerManager;
@@ -47,8 +52,6 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.internal.os.Clock;
-import com.android.server.display.brightness.LightSensorController;
 import com.android.server.display.brightness.clamper.BrightnessClamperController;
 import com.android.server.testutils.OffsettableClock;
 
@@ -58,6 +61,7 @@
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
 @SmallTest
@@ -65,18 +69,31 @@
 public class AutomaticBrightnessControllerTest {
     private static final float BRIGHTNESS_MIN_FLOAT = 0.0f;
     private static final float BRIGHTNESS_MAX_FLOAT = 1.0f;
+    private static final int LIGHT_SENSOR_RATE = 20;
     private static final int INITIAL_LIGHT_SENSOR_RATE = 20;
+    private static final int BRIGHTENING_LIGHT_DEBOUNCE_CONFIG = 2000;
+    private static final int DARKENING_LIGHT_DEBOUNCE_CONFIG = 4000;
+    private static final int BRIGHTENING_LIGHT_DEBOUNCE_CONFIG_IDLE = 1000;
+    private static final int DARKENING_LIGHT_DEBOUNCE_CONFIG_IDLE = 2000;
     private static final float DOZE_SCALE_FACTOR = 0.54f;
+    private static final boolean RESET_AMBIENT_LUX_AFTER_WARMUP_CONFIG = false;
+    private static final int LIGHT_SENSOR_WARMUP_TIME = 0;
+    private static final int AMBIENT_LIGHT_HORIZON_SHORT = 1000;
+    private static final int AMBIENT_LIGHT_HORIZON_LONG = 2000;
     private static final float EPSILON = 0.001f;
     private OffsettableClock mClock = new OffsettableClock();
     private TestLooper mTestLooper;
     private Context mContext;
     private AutomaticBrightnessController mController;
+    private Sensor mLightSensor;
 
+    @Mock SensorManager mSensorManager;
     @Mock BrightnessMappingStrategy mBrightnessMappingStrategy;
     @Mock BrightnessMappingStrategy mIdleBrightnessMappingStrategy;
     @Mock BrightnessMappingStrategy mDozeBrightnessMappingStrategy;
+    @Mock HysteresisLevels mAmbientBrightnessThresholds;
     @Mock HysteresisLevels mScreenBrightnessThresholds;
+    @Mock HysteresisLevels mAmbientBrightnessThresholdsIdle;
     @Mock HysteresisLevels mScreenBrightnessThresholdsIdle;
     @Mock Handler mNoOpHandler;
     @Mock BrightnessRangeController mBrightnessRangeController;
@@ -84,18 +101,17 @@
     BrightnessClamperController mBrightnessClamperController;
     @Mock BrightnessThrottler mBrightnessThrottler;
 
-    @Mock
-    LightSensorController mLightSensorController;
-
     @Before
     public void setUp() throws Exception {
         // Share classloader to allow package private access.
         System.setProperty("dexmaker.share_classloader", "true");
         MockitoAnnotations.initMocks(this);
 
+        mLightSensor = TestUtils.createSensor(Sensor.TYPE_LIGHT, "Light Sensor");
         mContext = InstrumentationRegistry.getContext();
         setupController(BrightnessMappingStrategy.INVALID_LUX,
-                BrightnessMappingStrategy.INVALID_NITS);
+                BrightnessMappingStrategy.INVALID_NITS, /* applyDebounce= */ false,
+                /* useHorizon= */ true);
     }
 
     @After
@@ -107,7 +123,8 @@
         }
     }
 
-    private void setupController(float userLux, float userNits) {
+    private void setupController(float userLux, float userNits, boolean applyDebounce,
+            boolean useHorizon) {
         mClock = new OffsettableClock.Stopped();
         mTestLooper = new TestLooper(mClock::now);
 
@@ -130,22 +147,25 @@
                     }
 
                     @Override
-                    Clock createClock() {
-                        return new Clock() {
-                            @Override
-                            public long uptimeMillis() {
-                                return mClock.now();
-                            }
-                        };
+                    AutomaticBrightnessController.Clock createClock() {
+                        return mClock::now;
                     }
 
                 }, // pass in test looper instead, pass in offsettable clock
-                () -> { }, mTestLooper.getLooper(),
-                brightnessMappingStrategyMap, BRIGHTNESS_MIN_FLOAT,
-                BRIGHTNESS_MAX_FLOAT, DOZE_SCALE_FACTOR, mScreenBrightnessThresholds,
-                mScreenBrightnessThresholdsIdle,
+                () -> { }, mTestLooper.getLooper(), mSensorManager, mLightSensor,
+                brightnessMappingStrategyMap, LIGHT_SENSOR_WARMUP_TIME, BRIGHTNESS_MIN_FLOAT,
+                BRIGHTNESS_MAX_FLOAT, DOZE_SCALE_FACTOR, LIGHT_SENSOR_RATE,
+                INITIAL_LIGHT_SENSOR_RATE, applyDebounce ? BRIGHTENING_LIGHT_DEBOUNCE_CONFIG : 0,
+                applyDebounce ? DARKENING_LIGHT_DEBOUNCE_CONFIG : 0,
+                applyDebounce ? BRIGHTENING_LIGHT_DEBOUNCE_CONFIG_IDLE : 0,
+                applyDebounce ? DARKENING_LIGHT_DEBOUNCE_CONFIG_IDLE : 0,
+                RESET_AMBIENT_LUX_AFTER_WARMUP_CONFIG,
+                mAmbientBrightnessThresholds, mScreenBrightnessThresholds,
+                mAmbientBrightnessThresholdsIdle, mScreenBrightnessThresholdsIdle,
                 mContext, mBrightnessRangeController, mBrightnessThrottler,
-                userLux, userNits, mLightSensorController, mBrightnessClamperController
+                useHorizon ? AMBIENT_LIGHT_HORIZON_SHORT : 1,
+                useHorizon ? AMBIENT_LIGHT_HORIZON_LONG : 10000, userLux, userNits,
+                mBrightnessClamperController
         );
 
         when(mBrightnessRangeController.getCurrentBrightnessMax()).thenReturn(
@@ -166,15 +186,20 @@
 
     @Test
     public void testNoHysteresisAtMinBrightness() throws Exception {
-        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
-                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
-        verify(mLightSensorController).setListener(listenerCaptor.capture());
-        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
 
         // Set up system to return 0.02f as a brightness value
         float lux1 = 100.0f;
         // Brightness as float (from 0.0f to 1.0f)
         float normalizedBrightness1 = 0.02f;
+        when(mAmbientBrightnessThresholds.getBrighteningThreshold(lux1))
+                .thenReturn(lux1);
+        when(mAmbientBrightnessThresholds.getDarkeningThreshold(lux1))
+                .thenReturn(lux1);
         when(mBrightnessMappingStrategy.getBrightness(eq(lux1), eq(null), anyInt()))
                 .thenReturn(normalizedBrightness1);
 
@@ -185,31 +210,39 @@
                 .thenReturn(1.0f);
 
         // Send new sensor value and verify
-        listener.onAmbientLuxChange(lux1);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux1));
         assertEquals(normalizedBrightness1, mController.getAutomaticScreenBrightness(), EPSILON);
 
         // Set up system to return 0.0f (minimum possible brightness) as a brightness value
         float lux2 = 10.0f;
         float normalizedBrightness2 = 0.0f;
+        when(mAmbientBrightnessThresholds.getBrighteningThreshold(lux2))
+                .thenReturn(lux2);
+        when(mAmbientBrightnessThresholds.getDarkeningThreshold(lux2))
+                .thenReturn(lux2);
         when(mBrightnessMappingStrategy.getBrightness(anyFloat(), eq(null), anyInt()))
                 .thenReturn(normalizedBrightness2);
 
         // Send new sensor value and verify
-        listener.onAmbientLuxChange(lux2);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux2));
         assertEquals(normalizedBrightness2, mController.getAutomaticScreenBrightness(), EPSILON);
     }
 
     @Test
     public void testNoHysteresisAtMaxBrightness() throws Exception {
-        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
-                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
-        verify(mLightSensorController).setListener(listenerCaptor.capture());
-        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
 
         // Set up system to return 0.98f as a brightness value
         float lux1 = 100.0f;
         float normalizedBrightness1 = 0.98f;
-
+        when(mAmbientBrightnessThresholds.getBrighteningThreshold(lux1))
+                .thenReturn(lux1);
+        when(mAmbientBrightnessThresholds.getDarkeningThreshold(lux1))
+                .thenReturn(lux1);
         when(mBrightnessMappingStrategy.getBrightness(eq(lux1), eq(null), anyInt()))
                 .thenReturn(normalizedBrightness1);
 
@@ -220,30 +253,35 @@
                 .thenReturn(1.1f);
 
         // Send new sensor value and verify
-        listener.onAmbientLuxChange(lux1);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux1));
         assertEquals(normalizedBrightness1, mController.getAutomaticScreenBrightness(), EPSILON);
 
 
         // Set up system to return 1.0f as a brightness value (brightness_max)
         float lux2 = 110.0f;
         float normalizedBrightness2 = 1.0f;
+        when(mAmbientBrightnessThresholds.getBrighteningThreshold(lux2))
+                .thenReturn(lux2);
+        when(mAmbientBrightnessThresholds.getDarkeningThreshold(lux2))
+                .thenReturn(lux2);
         when(mBrightnessMappingStrategy.getBrightness(anyFloat(), eq(null), anyInt()))
                 .thenReturn(normalizedBrightness2);
 
         // Send new sensor value and verify
-        listener.onAmbientLuxChange(lux2);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux2));
         assertEquals(normalizedBrightness2, mController.getAutomaticScreenBrightness(), EPSILON);
     }
 
     @Test
     public void testUserAddUserDataPoint() throws Exception {
-        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
-                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
-        verify(mLightSensorController).setListener(listenerCaptor.capture());
-        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
 
         // Sensor reads 1000 lux,
-        listener.onAmbientLuxChange(1000);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1000));
 
         // User sets brightness to 100
         mController.configure(AUTO_BRIGHTNESS_ENABLED, null /* configuration= */,
@@ -260,11 +298,12 @@
     public void testRecalculateSplines() throws Exception {
         // Enabling the light sensor, and setting the ambient lux to 1000
         int currentLux = 1000;
-        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
-                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
-        verify(mLightSensorController).setListener(listenerCaptor.capture());
-        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
-        listener.onAmbientLuxChange(currentLux);
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, currentLux));
 
         // User sets brightness to 0.5f
         when(mBrightnessMappingStrategy.getBrightness(currentLux,
@@ -294,13 +333,14 @@
 
     @Test
     public void testShortTermModelTimesOut() throws Exception {
-        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
-                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
-        verify(mLightSensorController).setListener(listenerCaptor.capture());
-        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
 
         // Sensor reads 123 lux,
-        listener.onAmbientLuxChange(123);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 123));
         // User sets brightness to 100
         mController.configure(AUTO_BRIGHTNESS_ENABLED, /* configuration= */ null,
                 /* brightness= */ 0.5f, /* userChangedBrightness= */ true, /* adjustment= */ 0,
@@ -314,7 +354,7 @@
                 123f, 0.5f)).thenReturn(true);
 
         // Sensor reads 1000 lux,
-        listener.onAmbientLuxChange(1000);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1000));
         mTestLooper.moveTimeForward(
                 mBrightnessMappingStrategy.getShortTermModelTimeout() + 1000);
         mTestLooper.dispatchAll();
@@ -333,13 +373,14 @@
 
     @Test
     public void testShortTermModelDoesntTimeOut() throws Exception {
-        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
-                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
-        verify(mLightSensorController).setListener(listenerCaptor.capture());
-        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
 
         // Sensor reads 123 lux,
-        listener.onAmbientLuxChange(123);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 123));
         // User sets brightness to 100
         mController.configure(AUTO_BRIGHTNESS_ENABLED, null /* configuration= */,
                 0.51f /* brightness= */, true /* userChangedBrightness= */, 0 /* adjustment= */,
@@ -358,7 +399,7 @@
         mTestLooper.dispatchAll();
 
         // Sensor reads 100000 lux,
-        listener.onAmbientLuxChange(678910);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 678910));
         mController.switchMode(AUTO_BRIGHTNESS_MODE_DEFAULT);
 
         // Verify short term model is not reset.
@@ -372,13 +413,14 @@
 
     @Test
     public void testShortTermModelIsRestoredWhenSwitchingWithinTimeout() throws Exception {
-        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
-                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
-        verify(mLightSensorController).setListener(listenerCaptor.capture());
-        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
 
         // Sensor reads 123 lux,
-        listener.onAmbientLuxChange(123);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 123));
         // User sets brightness to 100
         mController.configure(AUTO_BRIGHTNESS_ENABLED, /* configuration= */ null,
                 /* brightness= */ 0.5f, /* userChangedBrightness= */ true, /* adjustment= */ 0,
@@ -398,7 +440,7 @@
                 123f, 0.5f)).thenReturn(true);
 
         // Sensor reads 1000 lux,
-        listener.onAmbientLuxChange(1000);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1000));
         mTestLooper.moveTimeForward(
                 mBrightnessMappingStrategy.getShortTermModelTimeout() + 1000);
         mTestLooper.dispatchAll();
@@ -417,13 +459,14 @@
 
     @Test
     public void testShortTermModelNotRestoredAfterTimeout() throws Exception {
-        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
-                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
-        verify(mLightSensorController).setListener(listenerCaptor.capture());
-        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
 
         // Sensor reads 123 lux,
-        listener.onAmbientLuxChange(123);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 123));
         // User sets brightness to 100
         mController.configure(AUTO_BRIGHTNESS_ENABLED, /* configuration= */ null,
                 /* brightness= */ 0.5f, /* userChangedBrightness= */ true, /* adjustment= */ 0,
@@ -445,7 +488,7 @@
                 123f, 0.5f)).thenReturn(true);
 
         // Sensor reads 1000 lux,
-        listener.onAmbientLuxChange(1000);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1000));
         // Do not fast-forward time.
         mTestLooper.dispatchAll();
 
@@ -463,13 +506,14 @@
 
     @Test
     public void testSwitchBetweenModesNoUserInteractions() throws Exception {
-        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
-                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
-        verify(mLightSensorController).setListener(listenerCaptor.capture());
-        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
 
         // Sensor reads 123 lux,
-        listener.onAmbientLuxChange(123);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 123));
         when(mBrightnessMappingStrategy.getShortTermModelTimeout()).thenReturn(2000L);
         when(mBrightnessMappingStrategy.getUserBrightness()).thenReturn(
                 PowerManager.BRIGHTNESS_INVALID_FLOAT);
@@ -485,7 +529,7 @@
                 BrightnessMappingStrategy.INVALID_LUX);
 
         // Sensor reads 1000 lux,
-        listener.onAmbientLuxChange(1000);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1000));
         // Do not fast-forward time.
         mTestLooper.dispatchAll();
 
@@ -501,19 +545,14 @@
 
     @Test
     public void testSwitchToIdleMappingStrategy() throws Exception {
-        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
-                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
-        verify(mLightSensorController).setListener(listenerCaptor.capture());
-        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
-        clearInvocations(mBrightnessMappingStrategy);
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
 
         // Sensor reads 1000 lux,
-        listener.onAmbientLuxChange(1000);
-
-
-        verify(mBrightnessMappingStrategy).getBrightness(anyFloat(), any(), anyInt());
-
-        clearInvocations(mBrightnessMappingStrategy);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1000));
 
         // User sets brightness to 100
         mController.configure(AUTO_BRIGHTNESS_ENABLED, null /* configuration= */,
@@ -522,19 +561,22 @@
                 /* shouldResetShortTermModel= */ true);
 
         // There should be a user data point added to the mapper.
-        verify(mBrightnessMappingStrategy).addUserDataPoint(/* lux= */ 1000f,
+        verify(mBrightnessMappingStrategy, times(1)).addUserDataPoint(/* lux= */ 1000f,
                 /* brightness= */ 0.5f);
-        verify(mBrightnessMappingStrategy).setBrightnessConfiguration(any());
-        verify(mBrightnessMappingStrategy).getBrightness(anyFloat(), any(), anyInt());
+        verify(mBrightnessMappingStrategy, times(2)).setBrightnessConfiguration(any());
+        verify(mBrightnessMappingStrategy, times(3)).getBrightness(anyFloat(), any(), anyInt());
 
-        clearInvocations(mBrightnessMappingStrategy);
         // Now let's do the same for idle mode
         mController.switchMode(AUTO_BRIGHTNESS_MODE_IDLE);
-
-        verify(mBrightnessMappingStrategy).getMode();
-        verify(mBrightnessMappingStrategy).getShortTermModelTimeout();
-        verify(mBrightnessMappingStrategy).getUserBrightness();
-        verify(mBrightnessMappingStrategy).getUserLux();
+        // Called once when switching,
+        // setAmbientLux() is called twice and once in updateAutoBrightness(),
+        // nextAmbientLightBrighteningTransition() and nextAmbientLightDarkeningTransition() are
+        // called twice each.
+        verify(mBrightnessMappingStrategy, times(8)).getMode();
+        // Called when switching.
+        verify(mBrightnessMappingStrategy, times(1)).getShortTermModelTimeout();
+        verify(mBrightnessMappingStrategy, times(1)).getUserBrightness();
+        verify(mBrightnessMappingStrategy, times(1)).getUserLux();
 
         // Ensure, after switching, original BMS is not used anymore
         verifyNoMoreInteractions(mBrightnessMappingStrategy);
@@ -546,25 +588,154 @@
                 /* shouldResetShortTermModel= */ true);
 
         // Ensure we use the correct mapping strategy
-        verify(mIdleBrightnessMappingStrategy).addUserDataPoint(/* lux= */ 1000f,
+        verify(mIdleBrightnessMappingStrategy, times(1)).addUserDataPoint(/* lux= */ 1000f,
                 /* brightness= */ 0.5f);
     }
 
     @Test
+    public void testAmbientLightHorizon() throws Exception {
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
+
+        long increment = 500;
+        // set autobrightness to low
+        // t = 0
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0));
+
+        // t = 500
+        mClock.fastForward(increment);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0));
+
+        // t = 1000
+        mClock.fastForward(increment);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0));
+        assertEquals(0.0f, mController.getAmbientLux(), EPSILON);
+
+        // t = 1500
+        mClock.fastForward(increment);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0));
+        assertEquals(0.0f, mController.getAmbientLux(), EPSILON);
+
+        // t = 2000
+        // ensure that our reading is at 0.
+        mClock.fastForward(increment);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0));
+        assertEquals(0.0f, mController.getAmbientLux(), EPSILON);
+
+        // t = 2500
+        // first 10000 lux sensor event reading
+        mClock.fastForward(increment);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000));
+        assertTrue(mController.getAmbientLux() > 0.0f);
+        assertTrue(mController.getAmbientLux() < 10000.0f);
+
+        // t = 3000
+        // lux reading should still not yet be 10000.
+        mClock.fastForward(increment);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000));
+        assertTrue(mController.getAmbientLux() > 0.0f);
+        assertTrue(mController.getAmbientLux() < 10000.0f);
+
+        // t = 3500
+        mClock.fastForward(increment);
+        // lux has been high (10000) for 1000ms.
+        // lux reading should be 10000
+        // short horizon (ambient lux) is high, long horizon is still not high
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000));
+        assertEquals(10000.0f, mController.getAmbientLux(), EPSILON);
+
+        // t = 4000
+        // stay high
+        mClock.fastForward(increment);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000));
+        assertEquals(10000.0f, mController.getAmbientLux(), EPSILON);
+
+        // t = 4500
+        Mockito.clearInvocations(mBrightnessMappingStrategy);
+        mClock.fastForward(increment);
+        // short horizon is high, long horizon is high too
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000));
+        verify(mBrightnessMappingStrategy, times(1)).getBrightness(10000, null, -1);
+        assertEquals(10000.0f, mController.getAmbientLux(), EPSILON);
+
+        // t = 5000
+        mClock.fastForward(increment);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0));
+        assertTrue(mController.getAmbientLux() > 0.0f);
+        assertTrue(mController.getAmbientLux() < 10000.0f);
+
+        // t = 5500
+        mClock.fastForward(increment);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0));
+        assertTrue(mController.getAmbientLux() > 0.0f);
+        assertTrue(mController.getAmbientLux() < 10000.0f);
+
+        // t = 6000
+        mClock.fastForward(increment);
+        // ambient lux goes to 0
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0));
+        assertEquals(0.0f, mController.getAmbientLux(), EPSILON);
+
+        // only the values within the horizon should be kept
+        assertArrayEquals(new float[] {10000, 10000, 0, 0, 0}, mController.getLastSensorValues(),
+                EPSILON);
+        assertArrayEquals(new long[] {4000, 4500, 5000, 5500, 6000},
+                mController.getLastSensorTimestamps());
+    }
+
+    @Test
+    public void testHysteresisLevels() {
+        float[] ambientBrighteningThresholds = {50, 100};
+        float[] ambientDarkeningThresholds = {10, 20};
+        float[] ambientThresholdLevels = {0, 500};
+        float ambientDarkeningMinChangeThreshold = 3.0f;
+        float ambientBrighteningMinChangeThreshold = 1.5f;
+        HysteresisLevels hysteresisLevels = new HysteresisLevels(ambientBrighteningThresholds,
+                ambientDarkeningThresholds, ambientThresholdLevels, ambientThresholdLevels,
+                ambientDarkeningMinChangeThreshold, ambientBrighteningMinChangeThreshold);
+
+        // test low, activate minimum change thresholds.
+        assertEquals(1.5f, hysteresisLevels.getBrighteningThreshold(0.0f), EPSILON);
+        assertEquals(0f, hysteresisLevels.getDarkeningThreshold(0.0f), EPSILON);
+        assertEquals(1f, hysteresisLevels.getDarkeningThreshold(4.0f), EPSILON);
+
+        // test max
+        // epsilon is x2 here, since the next floating point value about 20,000 is 0.0019531 greater
+        assertEquals(20000f, hysteresisLevels.getBrighteningThreshold(10000.0f), EPSILON * 2);
+        assertEquals(8000f, hysteresisLevels.getDarkeningThreshold(10000.0f), EPSILON);
+
+        // test just below threshold
+        assertEquals(748.5f, hysteresisLevels.getBrighteningThreshold(499f), EPSILON);
+        assertEquals(449.1f, hysteresisLevels.getDarkeningThreshold(499f), EPSILON);
+
+        // test at (considered above) threshold
+        assertEquals(1000f, hysteresisLevels.getBrighteningThreshold(500f), EPSILON);
+        assertEquals(400f, hysteresisLevels.getDarkeningThreshold(500f), EPSILON);
+    }
+
+    @Test
     public void testBrightnessGetsThrottled() throws Exception {
-        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
-                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
-        verify(mLightSensorController).setListener(listenerCaptor.capture());
-        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
 
         // Set up system to return max brightness at 100 lux
         final float normalizedBrightness = BRIGHTNESS_MAX_FLOAT;
         final float lux = 100.0f;
+        when(mAmbientBrightnessThresholds.getBrighteningThreshold(lux))
+                .thenReturn(lux);
+        when(mAmbientBrightnessThresholds.getDarkeningThreshold(lux))
+                .thenReturn(lux);
         when(mBrightnessMappingStrategy.getBrightness(eq(lux), eq(null), anyInt()))
                 .thenReturn(normalizedBrightness);
 
         // Sensor reads 100 lux. We should get max brightness.
-        listener.onAmbientLuxChange(lux);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux));
         assertEquals(BRIGHTNESS_MAX_FLOAT, mController.getAutomaticScreenBrightness(), 0.0f);
         assertEquals(BRIGHTNESS_MAX_FLOAT, mController.getRawAutomaticScreenBrightness(), 0.0f);
 
@@ -592,6 +763,94 @@
     }
 
     @Test
+    public void testGetSensorReadings() throws Exception {
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
+
+        // Choose values such that the ring buffer's capacity is extended and the buffer is pruned
+        int increment = 11;
+        int lux = 5000;
+        for (int i = 0; i < 1000; i++) {
+            lux += increment;
+            mClock.fastForward(increment);
+            listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, lux));
+        }
+
+        int valuesCount = (int) Math.ceil((double) AMBIENT_LIGHT_HORIZON_LONG / increment + 1);
+        float[] sensorValues = mController.getLastSensorValues();
+        long[] sensorTimestamps = mController.getLastSensorTimestamps();
+
+        // Only the values within the horizon should be kept
+        assertEquals(valuesCount, sensorValues.length);
+        assertEquals(valuesCount, sensorTimestamps.length);
+
+        long sensorTimestamp = mClock.now();
+        for (int i = valuesCount - 1; i >= 1; i--) {
+            assertEquals(lux, sensorValues[i], EPSILON);
+            assertEquals(sensorTimestamp, sensorTimestamps[i]);
+            lux -= increment;
+            sensorTimestamp -= increment;
+        }
+        assertEquals(lux, sensorValues[0], EPSILON);
+        assertEquals(mClock.now() - AMBIENT_LIGHT_HORIZON_LONG, sensorTimestamps[0]);
+    }
+
+    @Test
+    public void testGetSensorReadingsFullBuffer() throws Exception {
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
+        int initialCapacity = 150;
+
+        // Choose values such that the ring buffer is pruned
+        int increment1 = 200;
+        int lux = 5000;
+        for (int i = 0; i < 20; i++) {
+            lux += increment1;
+            mClock.fastForward(increment1);
+            listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, lux));
+        }
+
+        int valuesCount = (int) Math.ceil((double) AMBIENT_LIGHT_HORIZON_LONG / increment1 + 1);
+
+        // Choose values such that the buffer becomes full
+        int increment2 = 1;
+        for (int i = 0; i < initialCapacity - valuesCount; i++) {
+            lux += increment2;
+            mClock.fastForward(increment2);
+            listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, lux));
+        }
+
+        float[] sensorValues = mController.getLastSensorValues();
+        long[] sensorTimestamps = mController.getLastSensorTimestamps();
+
+        // The buffer should be full
+        assertEquals(initialCapacity, sensorValues.length);
+        assertEquals(initialCapacity, sensorTimestamps.length);
+
+        long sensorTimestamp = mClock.now();
+        for (int i = initialCapacity - 1; i >= 1; i--) {
+            assertEquals(lux, sensorValues[i], EPSILON);
+            assertEquals(sensorTimestamp, sensorTimestamps[i]);
+
+            if (i >= valuesCount) {
+                lux -= increment2;
+                sensorTimestamp -= increment2;
+            } else {
+                lux -= increment1;
+                sensorTimestamp -= increment1;
+            }
+        }
+        assertEquals(lux, sensorValues[0], EPSILON);
+        assertEquals(mClock.now() - AMBIENT_LIGHT_HORIZON_LONG, sensorTimestamps[0]);
+    }
+
+    @Test
     public void testResetShortTermModelWhenConfigChanges() {
         when(mBrightnessMappingStrategy.setBrightnessConfiguration(any())).thenReturn(true);
 
@@ -616,22 +875,179 @@
         float userNits = 500;
         float userBrightness = 0.3f;
         when(mBrightnessMappingStrategy.getBrightnessFromNits(userNits)).thenReturn(userBrightness);
-        setupController(userLux, userNits);
+        setupController(userLux, userNits, /* applyDebounce= */ true,
+                /* useHorizon= */ false);
         verify(mBrightnessMappingStrategy).addUserDataPoint(userLux, userBrightness);
     }
 
     @Test
+    public void testBrighteningLightDebounce() throws Exception {
+        clearInvocations(mSensorManager);
+        setupController(BrightnessMappingStrategy.INVALID_LUX,
+                BrightnessMappingStrategy.INVALID_NITS, /* applyDebounce= */ true,
+                /* useHorizon= */ false);
+
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
+
+        // t = 0
+        // Initial lux
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500));
+        assertEquals(500, mController.getAmbientLux(), EPSILON);
+
+        // t = 1000
+        // Lux isn't steady yet
+        mClock.fastForward(1000);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200));
+        assertEquals(500, mController.getAmbientLux(), EPSILON);
+
+        // t = 1500
+        // Lux isn't steady yet
+        mClock.fastForward(500);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200));
+        assertEquals(500, mController.getAmbientLux(), EPSILON);
+
+        // t = 2500
+        // Lux is steady now
+        mClock.fastForward(1000);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200));
+        assertEquals(1200, mController.getAmbientLux(), EPSILON);
+    }
+
+    @Test
+    public void testDarkeningLightDebounce() throws Exception {
+        clearInvocations(mSensorManager);
+        when(mAmbientBrightnessThresholds.getBrighteningThreshold(anyFloat()))
+                .thenReturn(10000f);
+        when(mAmbientBrightnessThresholds.getDarkeningThreshold(anyFloat()))
+                .thenReturn(10000f);
+        setupController(BrightnessMappingStrategy.INVALID_LUX,
+                BrightnessMappingStrategy.INVALID_NITS, /* applyDebounce= */ true,
+                /* useHorizon= */ false);
+
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
+
+        // t = 0
+        // Initial lux
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200));
+        assertEquals(1200, mController.getAmbientLux(), EPSILON);
+
+        // t = 2000
+        // Lux isn't steady yet
+        mClock.fastForward(2000);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500));
+        assertEquals(1200, mController.getAmbientLux(), EPSILON);
+
+        // t = 2500
+        // Lux isn't steady yet
+        mClock.fastForward(500);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500));
+        assertEquals(1200, mController.getAmbientLux(), EPSILON);
+
+        // t = 4500
+        // Lux is steady now
+        mClock.fastForward(2000);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500));
+        assertEquals(500, mController.getAmbientLux(), EPSILON);
+    }
+
+    @Test
+    public void testBrighteningLightDebounceIdle() throws Exception {
+        clearInvocations(mSensorManager);
+        setupController(BrightnessMappingStrategy.INVALID_LUX,
+                BrightnessMappingStrategy.INVALID_NITS, /* applyDebounce= */ true,
+                /* useHorizon= */ false);
+
+        mController.switchMode(AUTO_BRIGHTNESS_MODE_IDLE);
+
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
+
+        // t = 0
+        // Initial lux
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500));
+        assertEquals(500, mController.getAmbientLux(), EPSILON);
+
+        // t = 500
+        // Lux isn't steady yet
+        mClock.fastForward(500);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200));
+        assertEquals(500, mController.getAmbientLux(), EPSILON);
+
+        // t = 1500
+        // Lux is steady now
+        mClock.fastForward(1000);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200));
+        assertEquals(1200, mController.getAmbientLux(), EPSILON);
+    }
+
+    @Test
+    public void testDarkeningLightDebounceIdle() throws Exception {
+        clearInvocations(mSensorManager);
+        when(mAmbientBrightnessThresholdsIdle.getBrighteningThreshold(anyFloat()))
+                .thenReturn(10000f);
+        when(mAmbientBrightnessThresholdsIdle.getDarkeningThreshold(anyFloat()))
+                .thenReturn(10000f);
+        setupController(BrightnessMappingStrategy.INVALID_LUX,
+                BrightnessMappingStrategy.INVALID_NITS, /* applyDebounce= */ true,
+                /* useHorizon= */ false);
+
+        mController.switchMode(AUTO_BRIGHTNESS_MODE_IDLE);
+
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
+
+        // t = 0
+        // Initial lux
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200));
+        assertEquals(1200, mController.getAmbientLux(), EPSILON);
+
+        // t = 1000
+        // Lux isn't steady yet
+        mClock.fastForward(1000);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500));
+        assertEquals(1200, mController.getAmbientLux(), EPSILON);
+
+        // t = 2500
+        // Lux is steady now
+        mClock.fastForward(1500);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500));
+        assertEquals(500, mController.getAmbientLux(), EPSILON);
+    }
+
+    @Test
     public void testBrightnessBasedOnLastObservedLux() throws Exception {
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
+
         // Set up system to return 0.3f as a brightness value
         float lux = 100.0f;
         // Brightness as float (from 0.0f to 1.0f)
         float normalizedBrightness = 0.3f;
-        when(mLightSensorController.getLastObservedLux()).thenReturn(lux);
+        when(mAmbientBrightnessThresholds.getBrighteningThreshold(lux)).thenReturn(lux);
+        when(mAmbientBrightnessThresholds.getDarkeningThreshold(lux)).thenReturn(lux);
         when(mBrightnessMappingStrategy.getBrightness(eq(lux), /* packageName= */ eq(null),
                 /* category= */ anyInt())).thenReturn(normalizedBrightness);
         when(mBrightnessThrottler.getBrightnessCap()).thenReturn(BRIGHTNESS_MAX_FLOAT);
 
         // Send a new sensor value, disable the sensor and verify
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux));
         mController.configure(AUTO_BRIGHTNESS_DISABLED, /* configuration= */ null,
                 /* brightness= */ 0, /* userChangedBrightness= */ false, /* adjustment= */ 0,
                 /* userChanged= */ false, DisplayPowerRequest.POLICY_BRIGHT, Display.STATE_ON,
@@ -643,19 +1059,21 @@
 
     @Test
     public void testAutoBrightnessInDoze() throws Exception {
-        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
-                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
-        verify(mLightSensorController).setListener(listenerCaptor.capture());
-        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
 
         // Set up system to return 0.3f as a brightness value
         float lux = 100.0f;
         // Brightness as float (from 0.0f to 1.0f)
         float normalizedBrightness = 0.3f;
+        when(mAmbientBrightnessThresholds.getBrighteningThreshold(lux)).thenReturn(lux);
+        when(mAmbientBrightnessThresholds.getDarkeningThreshold(lux)).thenReturn(lux);
         when(mBrightnessMappingStrategy.getBrightness(eq(lux), /* packageName= */ eq(null),
                 /* category= */ anyInt())).thenReturn(normalizedBrightness);
         when(mBrightnessThrottler.getBrightnessCap()).thenReturn(BRIGHTNESS_MAX_FLOAT);
-        when(mLightSensorController.getLastObservedLux()).thenReturn(lux);
 
         // Set policy to DOZE
         mController.configure(AUTO_BRIGHTNESS_ENABLED, /* configuration= */ null,
@@ -664,7 +1082,7 @@
                 /* shouldResetShortTermModel= */ true);
 
         // Send a new sensor value
-        listener.onAmbientLuxChange(lux);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux));
 
         // The brightness should be scaled by the doze factor
         assertEquals(normalizedBrightness * DOZE_SCALE_FACTOR,
@@ -677,19 +1095,21 @@
 
     @Test
     public void testAutoBrightnessInDoze_ShouldNotScaleIfUsingDozeCurve() throws Exception {
-        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
-                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
-        verify(mLightSensorController).setListener(listenerCaptor.capture());
-        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
 
         // Set up system to return 0.3f as a brightness value
         float lux = 100.0f;
         // Brightness as float (from 0.0f to 1.0f)
         float normalizedBrightness = 0.3f;
+        when(mAmbientBrightnessThresholds.getBrighteningThreshold(lux)).thenReturn(lux);
+        when(mAmbientBrightnessThresholds.getDarkeningThreshold(lux)).thenReturn(lux);
         when(mDozeBrightnessMappingStrategy.getBrightness(eq(lux), /* packageName= */ eq(null),
                 /* category= */ anyInt())).thenReturn(normalizedBrightness);
         when(mBrightnessThrottler.getBrightnessCap()).thenReturn(BRIGHTNESS_MAX_FLOAT);
-        when(mLightSensorController.getLastObservedLux()).thenReturn(lux);
 
         // Switch mode to DOZE
         mController.switchMode(AUTO_BRIGHTNESS_MODE_DOZE);
@@ -701,7 +1121,7 @@
                 /* shouldResetShortTermModel= */ true);
 
         // Send a new sensor value
-        listener.onAmbientLuxChange(lux);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux));
 
         // The brightness should not be scaled by the doze factor
         assertEquals(normalizedBrightness,
@@ -713,19 +1133,21 @@
 
     @Test
     public void testAutoBrightnessInDoze_ShouldNotScaleIfScreenOn() throws Exception {
-        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
-                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
-        verify(mLightSensorController).setListener(listenerCaptor.capture());
-        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
 
         // Set up system to return 0.3f as a brightness value
         float lux = 100.0f;
         // Brightness as float (from 0.0f to 1.0f)
         float normalizedBrightness = 0.3f;
+        when(mAmbientBrightnessThresholds.getBrighteningThreshold(lux)).thenReturn(lux);
+        when(mAmbientBrightnessThresholds.getDarkeningThreshold(lux)).thenReturn(lux);
         when(mBrightnessMappingStrategy.getBrightness(eq(lux), /* packageName= */ eq(null),
                 /* category= */ anyInt())).thenReturn(normalizedBrightness);
         when(mBrightnessThrottler.getBrightnessCap()).thenReturn(BRIGHTNESS_MAX_FLOAT);
-        when(mLightSensorController.getLastObservedLux()).thenReturn(lux);
 
         // Set policy to DOZE
         mController.configure(AUTO_BRIGHTNESS_ENABLED, /* configuration= */ null,
@@ -734,7 +1156,7 @@
                 /* shouldResetShortTermModel= */ true);
 
         // Send a new sensor value
-        listener.onAmbientLuxChange(lux);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux));
 
         // The brightness should not be scaled by the doze factor
         assertEquals(normalizedBrightness,
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
index db2a1f4..afb87d1 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -28,6 +28,7 @@
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isA;
@@ -78,8 +79,6 @@
 import com.android.server.display.RampAnimator.DualRampAnimator;
 import com.android.server.display.brightness.BrightnessEvent;
 import com.android.server.display.brightness.BrightnessReason;
-import com.android.server.display.brightness.LightSensorController;
-import com.android.server.display.brightness.TestUtilsKt;
 import com.android.server.display.brightness.clamper.BrightnessClamperController;
 import com.android.server.display.brightness.clamper.HdrClamper;
 import com.android.server.display.color.ColorDisplayService;
@@ -1167,19 +1166,30 @@
                 any(AutomaticBrightnessController.Callbacks.class),
                 any(Looper.class),
                 eq(mSensorManagerMock),
+                /* lightSensor= */ any(),
                 /* brightnessMappingStrategyMap= */ any(SparseArray.class),
+                /* lightSensorWarmUpTime= */ anyInt(),
                 /* brightnessMin= */ anyFloat(),
                 /* brightnessMax= */ anyFloat(),
                 /* dozeScaleFactor */ anyFloat(),
+                /* lightSensorRate= */ anyInt(),
+                /* initialLightSensorRate= */ anyInt(),
+                /* brighteningLightDebounceConfig */ anyLong(),
+                /* darkeningLightDebounceConfig */ anyLong(),
+                /* brighteningLightDebounceConfigIdle= */ anyLong(),
+                /* darkeningLightDebounceConfigIdle= */ anyLong(),
+                /* resetAmbientLuxAfterWarmUpConfig= */ anyBoolean(),
+                any(HysteresisLevels.class),
+                any(HysteresisLevels.class),
                 any(HysteresisLevels.class),
                 any(HysteresisLevels.class),
                 eq(mContext),
                 any(BrightnessRangeController.class),
                 any(BrightnessThrottler.class),
+                /* ambientLightHorizonShort= */ anyInt(),
+                /* ambientLightHorizonLong= */ anyInt(),
                 eq(lux),
                 eq(nits),
-                eq(DISPLAY_ID),
-                any(LightSensorController.LightSensorControllerConfig.class),
                 any(BrightnessClamperController.class)
         );
     }
@@ -2148,22 +2158,22 @@
         }
 
         @Override
-        LightSensorController.LightSensorControllerConfig getLightSensorControllerConfig(
-                Context context, DisplayDeviceConfig displayDeviceConfig) {
-            return TestUtilsKt.createLightSensorControllerConfig();
-        }
-
-        @Override
         AutomaticBrightnessController getAutomaticBrightnessController(
                 AutomaticBrightnessController.Callbacks callbacks, Looper looper,
-                SensorManager sensorManager,
+                SensorManager sensorManager, Sensor lightSensor,
                 SparseArray<BrightnessMappingStrategy> brightnessMappingStrategyMap,
-                float brightnessMin, float brightnessMax, float dozeScaleFactor,
+                int lightSensorWarmUpTime, float brightnessMin, float brightnessMax,
+                float dozeScaleFactor, int lightSensorRate, int initialLightSensorRate,
+                long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
+                long brighteningLightDebounceConfigIdle, long darkeningLightDebounceConfigIdle,
+                boolean resetAmbientLuxAfterWarmUpConfig,
+                HysteresisLevels ambientBrightnessThresholds,
                 HysteresisLevels screenBrightnessThresholds,
+                HysteresisLevels ambientBrightnessThresholdsIdle,
                 HysteresisLevels screenBrightnessThresholdsIdle, Context context,
                 BrightnessRangeController brightnessRangeController,
-                BrightnessThrottler brightnessThrottler, float userLux, float userNits,
-                int displayId, LightSensorController.LightSensorControllerConfig config,
+                BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
+                int ambientLightHorizonLong, float userLux, float userNits,
                 BrightnessClamperController brightnessClamperController) {
             return mAutomaticBrightnessController;
         }
@@ -2176,12 +2186,18 @@
         }
 
         @Override
-        HysteresisLevels getBrightnessThresholdsIdleHysteresisLevels(DisplayDeviceConfig ddc) {
+        HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+                float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+                float[] darkeningThresholdLevels, float minDarkeningThreshold,
+                float minBrighteningThreshold) {
             return mHysteresisLevels;
         }
 
         @Override
-        HysteresisLevels getBrightnessThresholdsHysteresisLevels(DisplayDeviceConfig ddc) {
+        HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+                float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+                float[] darkeningThresholdLevels, float minDarkeningThreshold,
+                float minBrighteningThreshold, boolean potentialOldBrightnessRange) {
             return mHysteresisLevels;
         }
 
diff --git a/services/tests/displayservicetests/src/com/android/server/display/HysteresisLevelsTest.kt b/services/tests/displayservicetests/src/com/android/server/display/HysteresisLevelsTest.kt
deleted file mode 100644
index 02d6946..0000000
--- a/services/tests/displayservicetests/src/com/android/server/display/HysteresisLevelsTest.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.display
-
-import androidx.test.filters.SmallTest
-import com.android.server.display.brightness.createHysteresisLevels
-import kotlin.test.assertEquals
-import org.junit.Test
-
-private const val FLOAT_TOLERANCE = 0.001f
-@SmallTest
-class HysteresisLevelsTest {
-    @Test
-    fun `test hysteresis levels`() {
-        val hysteresisLevels = createHysteresisLevels(
-            brighteningThresholdsPercentages = floatArrayOf(50f, 100f),
-            darkeningThresholdsPercentages = floatArrayOf(10f, 20f),
-            brighteningThresholdLevels = floatArrayOf(0f, 500f),
-            darkeningThresholdLevels = floatArrayOf(0f, 500f),
-            minDarkeningThreshold = 3f,
-            minBrighteningThreshold = 1.5f
-        )
-
-        // test low, activate minimum change thresholds.
-        assertEquals(1.5f, hysteresisLevels.getBrighteningThreshold(0.0f), FLOAT_TOLERANCE)
-        assertEquals(0f, hysteresisLevels.getDarkeningThreshold(0.0f), FLOAT_TOLERANCE)
-        assertEquals(1f, hysteresisLevels.getDarkeningThreshold(4.0f), FLOAT_TOLERANCE)
-
-        // test max
-        // epsilon is x2 here, since the next floating point value about 20,000 is 0.0019531 greater
-        assertEquals(
-            20000f, hysteresisLevels.getBrighteningThreshold(10000.0f), FLOAT_TOLERANCE * 2)
-        assertEquals(8000f, hysteresisLevels.getDarkeningThreshold(10000.0f), FLOAT_TOLERANCE)
-
-        // test just below threshold
-        assertEquals(748.5f, hysteresisLevels.getBrighteningThreshold(499f), FLOAT_TOLERANCE)
-        assertEquals(449.1f, hysteresisLevels.getDarkeningThreshold(499f), FLOAT_TOLERANCE)
-
-        // test at (considered above) threshold
-        assertEquals(1000f, hysteresisLevels.getBrighteningThreshold(500f), FLOAT_TOLERANCE)
-        assertEquals(400f, hysteresisLevels.getDarkeningThreshold(500f), FLOAT_TOLERANCE)
-    }
-}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/AmbientLightRingBufferTest.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/AmbientLightRingBufferTest.kt
deleted file mode 100644
index 5fe9178..0000000
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/AmbientLightRingBufferTest.kt
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.display.brightness
-
-import androidx.test.filters.SmallTest
-import com.android.internal.os.Clock
-import com.android.server.display.brightness.LightSensorController.AmbientLightRingBuffer
-import com.google.common.truth.Truth.assertThat
-import org.junit.Assert.assertThrows
-import org.junit.Test
-import org.mockito.kotlin.mock
-
-
-private const val BUFFER_INITIAL_CAPACITY = 3
-
-@SmallTest
-class AmbientLightRingBufferTest {
-
-    private val buffer = AmbientLightRingBuffer(BUFFER_INITIAL_CAPACITY, mock<Clock>())
-
-    @Test
-    fun `test created empty`() {
-        assertThat(buffer.size()).isEqualTo(0)
-    }
-
-    @Test
-    fun `test push to empty buffer`() {
-        buffer.push(1000, 0.5f)
-
-        assertThat(buffer.size()).isEqualTo(1)
-        assertThat(buffer.getLux(0)).isEqualTo(0.5f)
-        assertThat(buffer.getTime(0)).isEqualTo(1000)
-    }
-
-    @Test
-    fun `test prune keeps youngest outside horizon and sets time to horizon`() {
-        buffer.push(1000, 0.5f)
-        buffer.push(2000, 0.6f)
-        buffer.push(3000, 0.7f)
-
-        buffer.prune(2500)
-
-        assertThat(buffer.size()).isEqualTo(2)
-
-        assertThat(buffer.getLux(0)).isEqualTo(0.6f)
-        assertThat(buffer.getTime(0)).isEqualTo(2500)
-    }
-
-    @Test
-    fun `test prune keeps inside horizon`() {
-        buffer.push(1000, 0.5f)
-        buffer.push(2000, 0.6f)
-        buffer.push(3000, 0.7f)
-
-        buffer.prune(2500)
-
-        assertThat(buffer.size()).isEqualTo(2)
-
-        assertThat(buffer.getLux(1)).isEqualTo(0.7f)
-        assertThat(buffer.getTime(1)).isEqualTo(3000)
-    }
-
-
-    @Test
-    fun `test pushes correctly after prune`() {
-        buffer.push(1000, 0.5f)
-        buffer.push(2000, 0.6f)
-        buffer.push(3000, 0.7f)
-        buffer.prune(2500)
-
-        buffer.push(4000, 0.8f)
-
-        assertThat(buffer.size()).isEqualTo(3)
-
-        assertThat(buffer.getLux(0)).isEqualTo(0.6f)
-        assertThat(buffer.getTime(0)).isEqualTo(2500)
-        assertThat(buffer.getLux(1)).isEqualTo(0.7f)
-        assertThat(buffer.getTime(1)).isEqualTo(3000)
-        assertThat(buffer.getLux(2)).isEqualTo(0.8f)
-        assertThat(buffer.getTime(2)).isEqualTo(4000)
-    }
-
-    @Test
-    fun `test increase buffer size`() {
-        buffer.push(1000, 0.5f)
-        buffer.push(2000, 0.6f)
-        buffer.push(3000, 0.7f)
-
-        buffer.push(4000, 0.8f)
-
-        assertThat(buffer.size()).isEqualTo(4)
-
-        assertThat(buffer.getLux(0)).isEqualTo(0.5f)
-        assertThat(buffer.getTime(0)).isEqualTo(1000)
-        assertThat(buffer.getLux(1)).isEqualTo(0.6f)
-        assertThat(buffer.getTime(1)).isEqualTo(2000)
-        assertThat(buffer.getLux(2)).isEqualTo(0.7f)
-        assertThat(buffer.getTime(2)).isEqualTo(3000)
-        assertThat(buffer.getLux(3)).isEqualTo(0.8f)
-        assertThat(buffer.getTime(3)).isEqualTo(4000)
-    }
-
-    @Test
-    fun `test buffer clear`() {
-        buffer.push(1000, 0.5f)
-        buffer.push(2000, 0.6f)
-        buffer.push(3000, 0.7f)
-
-        buffer.clear()
-
-        assertThat(buffer.size()).isEqualTo(0)
-        assertThrows(ArrayIndexOutOfBoundsException::class.java) {
-            buffer.getLux(0)
-        }
-    }
-}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/LightSensorControllerTest.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/LightSensorControllerTest.kt
deleted file mode 100644
index 966134a..0000000
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/LightSensorControllerTest.kt
+++ /dev/null
@@ -1,432 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.display.brightness
-
-import android.hardware.Sensor
-import android.hardware.SensorEvent
-import android.hardware.SensorEventListener
-import android.os.Handler
-import androidx.test.filters.SmallTest
-import com.android.internal.os.Clock
-import com.android.server.display.TestUtils
-import com.android.server.display.brightness.LightSensorController.Injector
-import com.android.server.display.brightness.LightSensorController.LightSensorControllerConfig
-import com.android.server.testutils.OffsettableClock
-import com.android.server.testutils.TestHandler
-import com.google.common.truth.Truth.assertThat
-import kotlin.math.ceil
-import kotlin.test.assertEquals
-import org.junit.Assert.assertArrayEquals
-import org.junit.Assert.assertTrue
-import org.junit.Test
-
-private const val FLOAT_TOLERANCE = 0.001f
-
-@SmallTest
-class LightSensorControllerTest {
-
-    private val testHandler = TestHandler(null)
-    private val testInjector = TestInjector()
-
-    @Test
-    fun `test ambient light horizon`() {
-        val lightSensorController = LightSensorController(
-            createLightSensorControllerConfig(
-                lightSensorWarmUpTimeConfig = 0, // no warmUp time, can use first event
-                brighteningLightDebounceConfig = 0,
-                darkeningLightDebounceConfig = 0,
-                ambientLightHorizonShort = 1000,
-                ambientLightHorizonLong = 2000
-            ), testInjector, testHandler)
-
-        var reportedAmbientLux = 0f
-        lightSensorController.setListener { lux ->
-            reportedAmbientLux = lux
-        }
-        lightSensorController.enableLightSensorIfNeeded()
-
-        assertThat(testInjector.sensorEventListener).isNotNull()
-
-        val timeIncrement = 500L
-        // set ambient lux to low
-        // t = 0
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(0f))
-
-        // t = 500
-        //
-        testInjector.clock.fastForward(timeIncrement)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(0f))
-
-        // t = 1000
-        testInjector.clock.fastForward(timeIncrement)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(0f))
-        assertEquals(0f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // t = 1500
-        testInjector.clock.fastForward(timeIncrement)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(0f))
-        assertEquals(0f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // t = 2000
-        // ensure that our reading is at 0.
-        testInjector.clock.fastForward(timeIncrement)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(0f))
-        assertEquals(0f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // t = 2500
-        // first 10000 lux sensor event reading
-        testInjector.clock.fastForward(timeIncrement)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(10_000f))
-        assertTrue(reportedAmbientLux > 0f)
-        assertTrue(reportedAmbientLux < 10_000f)
-
-        // t = 3000
-        // lux reading should still not yet be 10000.
-        testInjector.clock.fastForward(timeIncrement)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(10_000f))
-        assertTrue(reportedAmbientLux > 0)
-        assertTrue(reportedAmbientLux < 10_000f)
-
-        // t = 3500
-        testInjector.clock.fastForward(timeIncrement)
-        // at short horizon,  first value outside will be used in calculation (t = 2000)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(10_000f))
-        assertTrue(reportedAmbientLux > 0f)
-        assertTrue(reportedAmbientLux < 10_000f)
-
-        // t = 4000
-        // lux has been high (10000) for more than 1000ms.
-        testInjector.clock.fastForward(timeIncrement)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(10_000f))
-        assertEquals(10_000f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // t = 4500
-        testInjector.clock.fastForward(timeIncrement)
-        // short horizon is high, long horizon is high too
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(10_000f))
-        assertEquals(10_000f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // t = 5000
-        testInjector.clock.fastForward(timeIncrement)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(0f))
-        assertTrue(reportedAmbientLux > 0f)
-        assertTrue(reportedAmbientLux < 10_000f)
-
-        // t = 5500
-        testInjector.clock.fastForward(timeIncrement)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(0f))
-        assertTrue(reportedAmbientLux > 0f)
-        assertTrue(reportedAmbientLux < 10_000f)
-
-        // t = 6000
-        testInjector.clock.fastForward(timeIncrement)
-        // at short horizon, first value outside will be used in calculation (t = 4500)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(0f))
-        assertTrue(reportedAmbientLux > 0f)
-        assertTrue(reportedAmbientLux < 10_000f)
-
-        // t = 6500
-        testInjector.clock.fastForward(timeIncrement)
-        // ambient lux goes to 0
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(0f))
-        assertEquals(0f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // only the values within the horizon should be kept
-        assertArrayEquals(floatArrayOf(10_000f, 0f, 0f, 0f, 0f),
-            lightSensorController.lastSensorValues, FLOAT_TOLERANCE)
-        assertArrayEquals(longArrayOf(4_500, 5_000, 5_500, 6_000, 6_500),
-            lightSensorController.lastSensorTimestamps)
-    }
-
-    @Test
-    fun `test brightening debounce`() {
-        val lightSensorController = LightSensorController(
-            createLightSensorControllerConfig(
-                lightSensorWarmUpTimeConfig = 0, // no warmUp time, can use first event
-                brighteningLightDebounceConfig = 1500,
-                ambientLightHorizonShort = 0, // only last value will be used for lux calculation
-                ambientLightHorizonLong = 10_000,
-                // brightening threshold is set to previous lux value
-                ambientBrightnessThresholds = createHysteresisLevels(
-                    brighteningThresholdLevels = floatArrayOf(),
-                    brighteningThresholdsPercentages = floatArrayOf(),
-                    minBrighteningThreshold = 0f
-                )
-            ), testInjector, testHandler)
-        lightSensorController.setIdleMode(false)
-
-        var reportedAmbientLux = 0f
-        lightSensorController.setListener { lux ->
-            reportedAmbientLux = lux
-        }
-        lightSensorController.enableLightSensorIfNeeded()
-
-        assertThat(testInjector.sensorEventListener).isNotNull()
-
-        // t0 (0)
-        // Initial lux, initial brightening threshold
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(1200f))
-        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // t1 (1000)
-        // Lux increase, first brightening event
-        testInjector.clock.fastForward(1000)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(1800f))
-        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // t2 (2000) (t2 - t1 < brighteningLightDebounceConfig)
-        // Lux increase, but isn't steady yet
-        testInjector.clock.fastForward(1000)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(2000f))
-        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // t3 (3000) (t3 - t1 < brighteningLightDebounceConfig)
-        // Lux increase, but isn't steady yet
-        testInjector.clock.fastForward(1000)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(2200f))
-        assertEquals(2200f, reportedAmbientLux, FLOAT_TOLERANCE)
-    }
-
-    @Test
-    fun `test sensor readings`() {
-        val ambientLightHorizonLong = 2_500
-        val lightSensorController = LightSensorController(
-            createLightSensorControllerConfig(
-                ambientLightHorizonLong = ambientLightHorizonLong
-            ), testInjector, testHandler)
-        lightSensorController.setListener { }
-        lightSensorController.setIdleMode(false)
-        lightSensorController.enableLightSensorIfNeeded()
-
-        // Choose values such that the ring buffer's capacity is extended and the buffer is pruned
-        val increment = 11
-        var lux = 5000
-        for (i in 0 until 1000) {
-            lux += increment
-            testInjector.clock.fastForward(increment.toLong())
-            testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(lux.toFloat()))
-        }
-
-        val valuesCount = ceil(ambientLightHorizonLong.toDouble() / increment + 1).toInt()
-        val sensorValues = lightSensorController.lastSensorValues
-        val sensorTimestamps = lightSensorController.lastSensorTimestamps
-
-        // Only the values within the horizon should be kept
-        assertEquals(valuesCount, sensorValues.size)
-        assertEquals(valuesCount, sensorTimestamps.size)
-
-        var sensorTimestamp = testInjector.clock.now()
-        for (i in valuesCount - 1 downTo 1) {
-            assertEquals(lux.toFloat(), sensorValues[i], FLOAT_TOLERANCE)
-            assertEquals(sensorTimestamp, sensorTimestamps[i])
-            lux -= increment
-            sensorTimestamp -= increment
-        }
-        assertEquals(lux.toFloat(), sensorValues[0], FLOAT_TOLERANCE)
-        assertEquals(testInjector.clock.now() - ambientLightHorizonLong, sensorTimestamps[0])
-    }
-
-    @Test
-    fun `test darkening debounce`() {
-        val lightSensorController = LightSensorController(
-            createLightSensorControllerConfig(
-                lightSensorWarmUpTimeConfig = 0, // no warmUp time, can use first event
-                darkeningLightDebounceConfig = 1500,
-                ambientLightHorizonShort = 0, // only last value will be used for lux calculation
-                ambientLightHorizonLong = 10_000,
-                // darkening threshold is set to previous lux value
-                ambientBrightnessThresholds = createHysteresisLevels(
-                    darkeningThresholdLevels = floatArrayOf(),
-                    darkeningThresholdsPercentages = floatArrayOf(),
-                    minDarkeningThreshold = 0f
-                )
-            ), testInjector, testHandler)
-
-        lightSensorController.setIdleMode(false)
-
-        var reportedAmbientLux = 0f
-        lightSensorController.setListener { lux ->
-            reportedAmbientLux = lux
-        }
-        lightSensorController.enableLightSensorIfNeeded()
-
-        assertThat(testInjector.sensorEventListener).isNotNull()
-
-        // t0 (0)
-        // Initial lux, initial darkening threshold
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(1200f))
-        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // t1 (1000)
-        // Lux decreased, first darkening event
-        testInjector.clock.fastForward(1000)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(800f))
-        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // t2 (2000) (t2 - t1 < darkeningLightDebounceConfig)
-        // Lux decreased, but isn't steady yet
-        testInjector.clock.fastForward(1000)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(500f))
-        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // t3 (3000) (t3 - t1 < darkeningLightDebounceConfig)
-        // Lux decreased, but isn't steady yet
-        testInjector.clock.fastForward(1000)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(400f))
-        assertEquals(400f, reportedAmbientLux, FLOAT_TOLERANCE)
-    }
-
-    @Test
-    fun `test brightening debounce in idle mode`() {
-        val lightSensorController = LightSensorController(
-            createLightSensorControllerConfig(
-                lightSensorWarmUpTimeConfig = 0, // no warmUp time, can use first event
-                brighteningLightDebounceConfigIdle = 1500,
-                ambientLightHorizonShort = 0, // only last value will be used for lux calculation
-                ambientLightHorizonLong = 10_000,
-                // brightening threshold is set to previous lux value
-                ambientBrightnessThresholdsIdle = createHysteresisLevels(
-                    brighteningThresholdLevels = floatArrayOf(),
-                    brighteningThresholdsPercentages = floatArrayOf(),
-                    minBrighteningThreshold = 0f
-                )
-            ), testInjector, testHandler)
-        lightSensorController.setIdleMode(true)
-
-        var reportedAmbientLux = 0f
-        lightSensorController.setListener { lux ->
-            reportedAmbientLux = lux
-        }
-        lightSensorController.enableLightSensorIfNeeded()
-
-        assertThat(testInjector.sensorEventListener).isNotNull()
-
-        // t0 (0)
-        // Initial lux, initial brightening threshold
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(1200f))
-        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // t1 (1000)
-        // Lux increase, first brightening event
-        testInjector.clock.fastForward(1000)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(1800f))
-        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // t2 (2000) (t2 - t1 < brighteningLightDebounceConfigIdle)
-        // Lux increase, but isn't steady yet
-        testInjector.clock.fastForward(1000)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(2000f))
-        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // t3 (3000) (t3 - t1 < brighteningLightDebounceConfigIdle)
-        // Lux increase, but isn't steady yet
-        testInjector.clock.fastForward(1000)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(2200f))
-        assertEquals(2200f, reportedAmbientLux, FLOAT_TOLERANCE)
-    }
-
-    @Test
-    fun `test darkening debounce in idle mode`() {
-        val lightSensorController = LightSensorController(
-            createLightSensorControllerConfig(
-                lightSensorWarmUpTimeConfig = 0, // no warmUp time, can use first event
-                darkeningLightDebounceConfigIdle = 1500,
-                ambientLightHorizonShort = 0, // only last value will be used for lux calculation
-                ambientLightHorizonLong = 10_000,
-                // darkening threshold is set to previous lux value
-                ambientBrightnessThresholdsIdle = createHysteresisLevels(
-                    darkeningThresholdLevels = floatArrayOf(),
-                    darkeningThresholdsPercentages = floatArrayOf(),
-                    minDarkeningThreshold = 0f
-                )
-            ), testInjector, testHandler)
-
-        lightSensorController.setIdleMode(true)
-
-        var reportedAmbientLux = 0f
-        lightSensorController.setListener { lux ->
-            reportedAmbientLux = lux
-        }
-        lightSensorController.enableLightSensorIfNeeded()
-
-        assertThat(testInjector.sensorEventListener).isNotNull()
-
-        // t0 (0)
-        // Initial lux, initial darkening threshold
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(1200f))
-        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // t1 (1000)
-        // Lux decreased, first darkening event
-        testInjector.clock.fastForward(1000)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(800f))
-        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // t2 (2000) (t2 - t1 < darkeningLightDebounceConfigIdle)
-        // Lux decreased, but isn't steady yet
-        testInjector.clock.fastForward(1000)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(500f))
-        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // t3 (3000) (t3 - t1 < darkeningLightDebounceConfigIdle)
-        // Lux decreased, but isn't steady yet
-        testInjector.clock.fastForward(1000)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(400f))
-        assertEquals(400f, reportedAmbientLux, FLOAT_TOLERANCE)
-    }
-
-
-    private fun sensorEvent(value: Float) = SensorEvent(
-        testInjector.testSensor, 0, 0, floatArrayOf(value)
-    )
-
-    private class TestInjector : Injector {
-        val testSensor: Sensor = TestUtils.createSensor(Sensor.TYPE_LIGHT, "Light Sensor")
-        val clock: OffsettableClock = OffsettableClock.Stopped()
-
-        var sensorEventListener: SensorEventListener? = null
-        override fun getClock(): Clock {
-            return object : Clock() {
-                override fun uptimeMillis(): Long {
-                    return clock.now()
-                }
-            }
-        }
-
-        override fun getLightSensor(config: LightSensorControllerConfig): Sensor {
-            return testSensor
-        }
-
-        override fun registerLightSensorListener(
-            listener: SensorEventListener,
-            sensor: Sensor,
-            rate: Int,
-            handler: Handler
-        ): Boolean {
-            sensorEventListener = listener
-            return true
-        }
-
-        override fun unregisterLightSensorListener(listener: SensorEventListener) {
-            sensorEventListener = null
-        }
-
-        override fun getTag(): String {
-            return "LightSensorControllerTest"
-        }
-    }
-}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/TestUtils.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/TestUtils.kt
deleted file mode 100644
index 1328f5f..0000000
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/TestUtils.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.display.brightness
-
-import com.android.server.display.HysteresisLevels
-import com.android.server.display.config.SensorData
-
-@JvmOverloads
-fun createLightSensorControllerConfig(
-    initialSensorRate: Int = 1,
-    normalSensorRate: Int = 2,
-    resetAmbientLuxAfterWarmUpConfig: Boolean = true,
-    ambientLightHorizonShort: Int = 1,
-    ambientLightHorizonLong: Int = 10_000,
-    lightSensorWarmUpTimeConfig: Int = 0,
-    weightingIntercept: Int = 10_000,
-    ambientBrightnessThresholds: HysteresisLevels = createHysteresisLevels(),
-    ambientBrightnessThresholdsIdle: HysteresisLevels = createHysteresisLevels(),
-    brighteningLightDebounceConfig: Long = 100_000,
-    darkeningLightDebounceConfig: Long = 100_000,
-    brighteningLightDebounceConfigIdle: Long = 100_000,
-    darkeningLightDebounceConfigIdle: Long = 100_000,
-    ambientLightSensor: SensorData = SensorData()
-) = LightSensorController.LightSensorControllerConfig(
-    initialSensorRate,
-    normalSensorRate,
-    resetAmbientLuxAfterWarmUpConfig,
-    ambientLightHorizonShort,
-    ambientLightHorizonLong,
-    lightSensorWarmUpTimeConfig,
-    weightingIntercept,
-    ambientBrightnessThresholds,
-    ambientBrightnessThresholdsIdle,
-    brighteningLightDebounceConfig,
-    darkeningLightDebounceConfig,
-    brighteningLightDebounceConfigIdle,
-    darkeningLightDebounceConfigIdle,
-    ambientLightSensor
-)
-
-fun createHysteresisLevels(
-    brighteningThresholdsPercentages: FloatArray = floatArrayOf(),
-    darkeningThresholdsPercentages: FloatArray = floatArrayOf(),
-    brighteningThresholdLevels: FloatArray = floatArrayOf(),
-    darkeningThresholdLevels: FloatArray = floatArrayOf(),
-    minDarkeningThreshold: Float = 0f,
-    minBrighteningThreshold: Float = 0f,
-    potentialOldBrightnessRange: Boolean = false
-) = HysteresisLevels(
-    brighteningThresholdsPercentages,
-    darkeningThresholdsPercentages,
-    brighteningThresholdLevels,
-    darkeningThresholdLevels,
-    minDarkeningThreshold,
-    minBrighteningThreshold,
-    potentialOldBrightnessRange
-)
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
index 3a59c84..fbc38a2 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java
@@ -1349,6 +1349,84 @@
     }
 
     @Test
+    public void testLockFps_DisplayWithOneMode() throws Exception {
+        when(mDisplayManagerFlags.isBackUpSmoothDisplayAndForcePeakRefreshRateEnabled())
+                .thenReturn(true);
+        DisplayModeDirector director =
+                new DisplayModeDirector(mContext, mHandler, mInjector, mDisplayManagerFlags);
+        director.getBrightnessObserver().setDefaultDisplayState(Display.STATE_ON);
+
+        Display.Mode[] modes1 = new Display.Mode[] {
+                new Display.Mode(/* modeId= */ 1, /* width= */ 1280, /* height= */ 720,
+                        /* refreshRate= */ 60),
+                new Display.Mode(/* modeId= */ 2, /* width= */ 1280, /* height= */ 720,
+                        /* refreshRate= */ 90),
+        };
+        Display.Mode[] modes2 = new Display.Mode[] {
+                new Display.Mode(/* modeId= */ 1, /* width= */ 1280, /* height= */ 720,
+                        /* refreshRate= */ 60),
+        };
+        SparseArray<Display.Mode[]> supportedModesByDisplay = new SparseArray<>();
+        supportedModesByDisplay.put(DISPLAY_ID, modes1);
+        supportedModesByDisplay.put(DISPLAY_ID_2, modes2);
+
+        final FakeDeviceConfig config = mInjector.getDeviceConfig();
+        config.setRefreshRateInLowZone(90);
+        config.setLowDisplayBrightnessThresholds(new int[] { 10 });
+        config.setLowAmbientBrightnessThresholds(new int[] { 20 });
+
+        Sensor lightSensor = createLightSensor();
+        SensorManager sensorManager = createMockSensorManager(lightSensor);
+
+        director.start(sensorManager);
+        director.injectSupportedModesByDisplay(supportedModesByDisplay);
+        director.getSettingsObserver().setDefaultRefreshRate(90);
+
+        setPeakRefreshRate(Float.POSITIVE_INFINITY);
+
+        ArgumentCaptor<DisplayListener> displayListenerCaptor =
+                ArgumentCaptor.forClass(DisplayListener.class);
+        verify(mInjector).registerDisplayListener(displayListenerCaptor.capture(),
+                any(Handler.class),
+                eq(DisplayManager.EVENT_FLAG_DISPLAY_CHANGED
+                        | DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS));
+        DisplayListener displayListener = displayListenerCaptor.getValue();
+
+        ArgumentCaptor<SensorEventListener> sensorListenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        Mockito.verify(sensorManager, Mockito.timeout(TimeUnit.SECONDS.toMillis(1)))
+                .registerListener(
+                        sensorListenerCaptor.capture(),
+                        eq(lightSensor),
+                        anyInt(),
+                        any(Handler.class));
+        SensorEventListener sensorListener = sensorListenerCaptor.getValue();
+
+        setBrightness(10, 10, displayListener);
+        // Sensor reads 20 lux
+        sensorListener.onSensorChanged(TestUtils.createSensorEvent(lightSensor, /* lux= */ 20));
+
+        Vote vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE);
+        assertVoteForPhysicalRefreshRate(vote, /* fps= */ 90);
+        vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH);
+        assertThat(vote).isNotNull();
+        assertThat(vote).isInstanceOf(DisableRefreshRateSwitchingVote.class);
+        DisableRefreshRateSwitchingVote disableVote = (DisableRefreshRateSwitchingVote) vote;
+        assertThat(disableVote.mDisableRefreshRateSwitching).isTrue();
+
+        // We expect DisplayModeDirector to act on BrightnessInfo.adjustedBrightness; set only this
+        // parameter to the necessary threshold
+        setBrightness(10, 125, displayListener);
+        // Sensor reads 1000 lux
+        sensorListener.onSensorChanged(TestUtils.createSensorEvent(lightSensor, /* lux= */ 1000));
+
+        vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE);
+        assertThat(vote).isNull();
+        vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH);
+        assertThat(vote).isNull();
+    }
+
+    @Test
     public void testLockFpsForHighZone() throws Exception {
         DisplayModeDirector director =
                 createDirectorFromRefreshRateArray(new float[] {60.f, 90.f}, 0);
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
index 872ac40..4c7a8fe 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/MockingOomAdjusterTests.java
@@ -479,15 +479,7 @@
         sService.mWakefulness.set(PowerManagerInternal.WAKEFULNESS_AWAKE);
         updateOomAdj(app);
 
-        final int expectedAdj;
-        if (sService.mConstants.ENABLE_NEW_OOMADJ) {
-            // A cached empty process can be at best a level higher than the min cached adj.
-            expectedAdj = sFirstCachedAdj;
-        } else {
-            // This is wrong but legacy behavior is going to be removed and not worth fixing.
-            expectedAdj = CACHED_APP_MIN_ADJ;
-        }
-
+        final int expectedAdj = sFirstCachedAdj;
         assertProcStates(app, PROCESS_STATE_CACHED_EMPTY, expectedAdj,
                 SCHED_GROUP_BACKGROUND);
     }
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java b/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java
index 344e2c2..69a98ac 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java
@@ -27,6 +27,7 @@
 import static org.mockito.Mockito.when;
 
 import android.accessibilityservice.BrailleDisplayController;
+import android.accessibilityservice.IBrailleDisplayController;
 import android.content.Context;
 import android.os.Bundle;
 import android.os.IBinder;
@@ -174,6 +175,17 @@
         }
 
         @Test
+        public void defaultNativeScanner_getName_returnsName() {
+            String name = "My Braille Display";
+            when(mNativeInterface.getHidrawName(anyInt())).thenReturn(name);
+
+            BrailleDisplayConnection.BrailleDisplayScanner scanner =
+                    BrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
+
+            assertThat(scanner.getName(NULL_PATH)).isEqualTo(name);
+        }
+
+        @Test
         public void write_bypassesServiceSideCheckWithLargeBuffer_disconnects() {
             Mockito.doNothing().when(mBrailleDisplayConnection).disconnect();
             mBrailleDisplayConnection.write(
@@ -201,6 +213,38 @@
             verify(mBrailleDisplayConnection).disconnect();
         }
 
+        @Test
+        public void connect_unableToGetUniq_usesNameFallback() throws Exception {
+            try {
+                IBrailleDisplayController controller =
+                        Mockito.mock(IBrailleDisplayController.class);
+                final Path path = Path.of("/dev/null");
+                final String macAddress = "00:11:22:33:AA:BB";
+                final String name = "My Braille Display";
+                final byte[] descriptor = {0x05, 0x41};
+                Bundle bd = new Bundle();
+                bd.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH,
+                        path.toString());
+                bd.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR,
+                        descriptor);
+                bd.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_NAME, name);
+                bd.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH, true);
+                bd.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, null);
+                BrailleDisplayConnection.BrailleDisplayScanner scanner =
+                        mBrailleDisplayConnection.setTestData(List.of(bd));
+                // Validate that the test data is set up correctly before attempting connection:
+                assertThat(scanner.getUniqueId(path)).isNull();
+                assertThat(scanner.getName(path)).isEqualTo(name);
+
+                mBrailleDisplayConnection.connectLocked(
+                        macAddress, name, BrailleDisplayConnection.BUS_BLUETOOTH, controller);
+
+                verify(controller).onConnected(eq(mBrailleDisplayConnection), eq(descriptor));
+            } finally {
+                mBrailleDisplayConnection.disconnect();
+            }
+        }
+
         // BrailleDisplayConnection#setTestData() is used to enable CTS testing with
         // test Braille display data, but its own implementation should also be tested
         // so that issues in this helper don't cause confusing failures in CTS.
@@ -220,6 +264,9 @@
             String uniq1 = "uniq1", uniq2 = "uniq2";
             bd1.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, uniq1);
             bd2.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, uniq2);
+            String name1 = "name1", name2 = "name2";
+            bd1.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_NAME, name1);
+            bd2.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_NAME, name2);
             int bus1 = BrailleDisplayConnection.BUS_USB, bus2 =
                     BrailleDisplayConnection.BUS_BLUETOOTH;
             bd1.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH,
@@ -235,6 +282,8 @@
             expect.that(scanner.getDeviceReportDescriptor(path2)).isEqualTo(desc2);
             expect.that(scanner.getUniqueId(path1)).isEqualTo(uniq1);
             expect.that(scanner.getUniqueId(path2)).isEqualTo(uniq2);
+            expect.that(scanner.getName(path1)).isEqualTo(name1);
+            expect.that(scanner.getName(path2)).isEqualTo(name2);
             expect.that(scanner.getDeviceBusType(path1)).isEqualTo(bus1);
             expect.that(scanner.getDeviceBusType(path2)).isEqualTo(bus2);
         }
diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
index 643dcec..0a2a855 100644
--- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -123,6 +123,7 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
@@ -678,6 +679,87 @@
     }
 
     /**
+     * Test that, when exceeding the maximum number of running users, a profile of the current user
+     * is not stopped.
+     */
+    @Test
+    public void testStoppingExcessRunningUsersAfterSwitch_currentProfileNotStopped()
+            throws Exception {
+        mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true,
+                /* maxRunningUsers= */ 5, /* delayUserDataLocking= */ false);
+
+        final int PARENT_ID = 200;
+        final int PROFILE1_ID = 201;
+        final int PROFILE2_ID = 202;
+        final int FG_USER_ID = 300;
+        final int BG_USER_ID = 400;
+
+        setUpUser(PARENT_ID, 0).profileGroupId = PARENT_ID;
+        setUpUser(PROFILE1_ID, UserInfo.FLAG_PROFILE).profileGroupId = PARENT_ID;
+        setUpUser(PROFILE2_ID, UserInfo.FLAG_PROFILE).profileGroupId = PARENT_ID;
+        setUpUser(FG_USER_ID, 0).profileGroupId = FG_USER_ID;
+        setUpUser(BG_USER_ID, 0).profileGroupId = UserInfo.NO_PROFILE_GROUP_ID;
+        mUserController.onSystemReady(); // To set the profileGroupIds in UserController.
+
+        assertEquals(newHashSet(
+                SYSTEM_USER_ID),
+                new HashSet<>(mUserController.getRunningUsersLU()));
+
+        int numberOfUserSwitches = 1;
+        addForegroundUserAndContinueUserSwitch(PARENT_ID, UserHandle.USER_SYSTEM,
+                numberOfUserSwitches, false);
+        mUserController.finishUserSwitch(mUserStates.get(PARENT_ID));
+        waitForHandlerToComplete(mInjector.mHandler, HANDLER_WAIT_TIME_MS);
+        assertTrue(mUserController.canStartMoreUsers());
+        assertEquals(newHashSet(
+                SYSTEM_USER_ID, PARENT_ID),
+                new HashSet<>(mUserController.getRunningUsersLU()));
+
+        assertThat(mUserController.startProfile(PROFILE1_ID, true, null)).isTrue();
+        assertEquals(newHashSet(
+                SYSTEM_USER_ID, PROFILE1_ID, PARENT_ID),
+                new HashSet<>(mUserController.getRunningUsersLU()));
+
+        numberOfUserSwitches++;
+        addForegroundUserAndContinueUserSwitch(FG_USER_ID, PARENT_ID,
+                numberOfUserSwitches, false);
+        mUserController.finishUserSwitch(mUserStates.get(FG_USER_ID));
+        waitForHandlerToComplete(mInjector.mHandler, HANDLER_WAIT_TIME_MS);
+        assertTrue(mUserController.canStartMoreUsers());
+        assertEquals(newHashSet(
+                SYSTEM_USER_ID, PROFILE1_ID, PARENT_ID, FG_USER_ID),
+                new HashSet<>(mUserController.getRunningUsersLU()));
+
+        mUserController.startUser(BG_USER_ID, USER_START_MODE_BACKGROUND);
+        assertEquals(newHashSet(
+                SYSTEM_USER_ID, PROFILE1_ID, PARENT_ID, BG_USER_ID, FG_USER_ID),
+                new HashSet<>(mUserController.getRunningUsersLU()));
+
+        // Now we exceed the maxRunningUsers parameter (of 5):
+        assertThat(mUserController.startProfile(PROFILE2_ID, true, null)).isTrue();
+        // Currently, starting a profile doesn't trigger evaluating whether we've exceeded max, so
+        // we expect no users to be stopped. This policy may change in the future. Log but no fail.
+        if (!newHashSet(SYSTEM_USER_ID, PROFILE1_ID, BG_USER_ID, PROFILE2_ID, PARENT_ID, FG_USER_ID)
+                .equals(new HashSet<>(mUserController.getRunningUsersLU()))) {
+            Log.w(TAG, "Starting a profile that exceeded max running users didn't lead to "
+                    + "expectations: " + mUserController.getRunningUsersLU());
+        }
+
+        numberOfUserSwitches++;
+        addForegroundUserAndContinueUserSwitch(PARENT_ID, FG_USER_ID,
+                numberOfUserSwitches, false);
+        mUserController.finishUserSwitch(mUserStates.get(PARENT_ID));
+        waitForHandlerToComplete(mInjector.mHandler, HANDLER_WAIT_TIME_MS);
+        // We've now done a user switch and should notice that we've exceeded the maximum number of
+        // users. The oldest background user should be stopped (BG_USER); even though PROFILE1 was
+        // older, it should not be stopped since it's a profile of the (new) current user.
+        assertFalse(mUserController.canStartMoreUsers());
+        assertEquals(newHashSet(
+                SYSTEM_USER_ID, PROFILE1_ID, PROFILE2_ID, FG_USER_ID, PARENT_ID),
+                new HashSet<>(mUserController.getRunningUsersLU()));
+    }
+
+    /**
      * Test that, in getRunningUsersLU, parents come after their profile, even if the profile was
      * started afterwards.
      */
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 5e0806d..7d90a8b 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
@@ -2004,7 +2004,7 @@
                         mRunningAppsChangedCallback,
                         params,
                         new DisplayManagerGlobal(mIDisplayManager),
-                        new VirtualCameraController(DEVICE_POLICY_DEFAULT));
+                        new VirtualCameraController(DEVICE_POLICY_DEFAULT, virtualDeviceId));
         mVdms.addVirtualDevice(virtualDeviceImpl);
         assertThat(virtualDeviceImpl.getAssociationId()).isEqualTo(mAssociationInfo.getId());
         assertThat(virtualDeviceImpl.getPersistentDeviceId())
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
index 9ca1df0..4505a8b 100644
--- 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
@@ -29,6 +29,7 @@
 
 import static org.junit.Assert.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -62,6 +63,7 @@
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 public class VirtualCameraControllerTest {
 
+    private static final int DEVICE_ID = 5;
     private static final String CAMERA_NAME_1 = "Virtual camera 1";
     private static final int CAMERA_WIDTH_1 = 100;
     private static final int CAMERA_HEIGHT_1 = 200;
@@ -91,8 +93,9 @@
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
         mVirtualCameraController = new VirtualCameraController(mVirtualCameraServiceMock,
-                DEVICE_POLICY_CUSTOM);
-        when(mVirtualCameraServiceMock.registerCamera(any(), any())).thenReturn(true);
+                DEVICE_POLICY_CUSTOM, DEVICE_ID);
+        when(mVirtualCameraServiceMock.registerCamera(any(), any(), anyInt())).thenReturn(true);
+        when(mVirtualCameraServiceMock.getCameraId(any())).thenReturn(0);
     }
 
     @After
@@ -109,7 +112,10 @@
 
         ArgumentCaptor<VirtualCameraConfiguration> configurationCaptor =
                 ArgumentCaptor.forClass(VirtualCameraConfiguration.class);
-        verify(mVirtualCameraServiceMock).registerCamera(any(), configurationCaptor.capture());
+        ArgumentCaptor<Integer> deviceIdCaptor = ArgumentCaptor.forClass(Integer.class);
+        verify(mVirtualCameraServiceMock).registerCamera(any(), configurationCaptor.capture(),
+                deviceIdCaptor.capture());
+        assertThat(deviceIdCaptor.getValue()).isEqualTo(DEVICE_ID);
         VirtualCameraConfiguration virtualCameraConfiguration = configurationCaptor.getValue();
         assertThat(virtualCameraConfiguration.supportedStreamConfigs.length).isEqualTo(1);
         assertVirtualCameraConfiguration(virtualCameraConfiguration, CAMERA_WIDTH_1,
@@ -146,8 +152,11 @@
 
         ArgumentCaptor<VirtualCameraConfiguration> configurationCaptor =
                 ArgumentCaptor.forClass(VirtualCameraConfiguration.class);
+        ArgumentCaptor<Integer> deviceIdCaptor = ArgumentCaptor.forClass(Integer.class);
         verify(mVirtualCameraServiceMock, times(2)).registerCamera(any(),
-                configurationCaptor.capture());
+                configurationCaptor.capture(), deviceIdCaptor.capture());
+        List<Integer> deviceIds = deviceIdCaptor.getAllValues();
+        assertThat(deviceIds).containsExactly(DEVICE_ID, DEVICE_ID);
         List<VirtualCameraConfiguration> virtualCameraConfigurations =
                 configurationCaptor.getAllValues();
         assertThat(virtualCameraConfigurations).hasSize(2);
@@ -177,8 +186,8 @@
     @Test
     public void registerCamera_withDefaultCameraPolicy_throwsException(int lensFacing) {
         mVirtualCameraController.close();
-        mVirtualCameraController = new VirtualCameraController(
-                mVirtualCameraServiceMock, DEVICE_POLICY_DEFAULT);
+        mVirtualCameraController = new VirtualCameraController(mVirtualCameraServiceMock,
+                DEVICE_POLICY_DEFAULT, DEVICE_ID);
 
         assertThrows(IllegalArgumentException.class,
                 () -> mVirtualCameraController.registerCamera(createVirtualCameraConfig(
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
index 1591a96..4405a20 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
@@ -180,25 +180,26 @@
 
         // Test that only one clone user can be created
         final int mainUserId = mainUser.getIdentifier();
-        UserInfo userInfo = createProfileForUser("Clone user1",
+        UserInfo cloneProfileUser = createProfileForUser("Clone user1",
                 UserManager.USER_TYPE_PROFILE_CLONE,
                 mainUserId);
-        assertThat(userInfo).isNotNull();
-        UserInfo userInfo2 = createProfileForUser("Clone user2",
+        assertThat(cloneProfileUser).isNotNull();
+        UserInfo cloneProfileUser2 = createProfileForUser("Clone user2",
                 UserManager.USER_TYPE_PROFILE_CLONE,
                 mainUserId);
-        assertThat(userInfo2).isNull();
+        assertThat(cloneProfileUser2).isNull();
 
-        final Context userContext = mContext.createPackageContextAsUser("system", 0,
-                UserHandle.of(userInfo.id));
-        assertThat(userContext.getSystemService(
+        final Context profileUserContest = mContext.createPackageContextAsUser("system", 0,
+                UserHandle.of(cloneProfileUser.id));
+        final UserManager profileUM = UserManager.get(profileUserContest);
+        assertThat(profileUserContest.getSystemService(
                 UserManager.class).isMediaSharedWithParent()).isTrue();
-        assertThat(Settings.Secure.getInt(userContext.getContentResolver(),
+        assertThat(Settings.Secure.getInt(profileUserContest.getContentResolver(),
                 Settings.Secure.USER_SETUP_COMPLETE, 0)).isEqualTo(1);
 
         List<UserInfo> list = mUserManager.getUsers();
         List<UserInfo> cloneUsers = list.stream().filter(
-                user -> (user.id == userInfo.id && user.name.equals("Clone user1")
+                user -> (user.id == cloneProfileUser.id && user.name.equals("Clone user1")
                         && user.isCloneProfile()))
                 .collect(Collectors.toList());
         assertThat(cloneUsers.size()).isEqualTo(1);
@@ -206,7 +207,7 @@
         // Check that the new clone user has the expected properties (relative to the defaults)
         // provided that the test caller has the necessary permissions.
         UserProperties cloneUserProperties =
-                mUserManager.getUserProperties(UserHandle.of(userInfo.id));
+                mUserManager.getUserProperties(UserHandle.of(cloneProfileUser.id));
         assertThat(typeProps.getUseParentsContacts())
                 .isEqualTo(cloneUserProperties.getUseParentsContacts());
         assertThat(typeProps.getShowInLauncher())
@@ -226,15 +227,15 @@
         assertThrows(SecurityException.class, cloneUserProperties::getAlwaysVisible);
         assertThat(typeProps.getProfileApiVisibility()).isEqualTo(
                 cloneUserProperties.getProfileApiVisibility());
-        compareDrawables(mUserManager.getUserBadge(),
+        compareDrawables(profileUM.getUserBadge(),
                 Resources.getSystem().getDrawable(userTypeDetails.getBadgePlain()));
 
         // Verify clone user parent
         assertThat(mUserManager.getProfileParent(mainUserId)).isNull();
-        UserInfo parentProfileInfo = mUserManager.getProfileParent(userInfo.id);
+        UserInfo parentProfileInfo = mUserManager.getProfileParent(cloneProfileUser.id);
         assertThat(parentProfileInfo).isNotNull();
         assertThat(mainUserId).isEqualTo(parentProfileInfo.id);
-        removeUser(userInfo.id);
+        removeUser(cloneProfileUser.id);
         assertThat(mUserManager.getProfileParent(mainUserId)).isNull();
     }
 
@@ -322,19 +323,22 @@
 
         // Test that only one private profile  can be created
         final int mainUserId = mainUser.getIdentifier();
-        UserInfo userInfo = createProfileForUser("Private profile1",
+        UserInfo privateProfileUser = createProfileForUser("Private profile1",
                 UserManager.USER_TYPE_PROFILE_PRIVATE,
                 mainUserId);
-        assertThat(userInfo).isNotNull();
-        UserInfo userInfo2 = createProfileForUser("Private profile2",
+        assertThat(privateProfileUser).isNotNull();
+        UserInfo privateProfileUser2 = createProfileForUser("Private profile2",
                 UserManager.USER_TYPE_PROFILE_PRIVATE,
                 mainUserId);
-        assertThat(userInfo2).isNull();
+        assertThat(privateProfileUser2).isNull();
+        final UserManager profileUM = UserManager.get(
+                mContext.createPackageContextAsUser("android", 0,
+                        UserHandle.of(privateProfileUser.id)));
 
         // Check that the new private profile has the expected properties (relative to the defaults)
         // provided that the test caller has the necessary permissions.
         UserProperties privateProfileUserProperties =
-                mUserManager.getUserProperties(UserHandle.of(userInfo.id));
+                mUserManager.getUserProperties(UserHandle.of(privateProfileUser.id));
         assertThat(typeProps.getShowInLauncher())
                 .isEqualTo(privateProfileUserProperties.getShowInLauncher());
         assertThrows(SecurityException.class, privateProfileUserProperties::getStartWithParent);
@@ -356,17 +360,17 @@
                 privateProfileUserProperties.getProfileApiVisibility());
         assertThat(typeProps.areItemsRestrictedOnHomeScreen())
                 .isEqualTo(privateProfileUserProperties.areItemsRestrictedOnHomeScreen());
-        compareDrawables(mUserManager.getUserBadge(),
+        compareDrawables(profileUM.getUserBadge(),
                 Resources.getSystem().getDrawable(userTypeDetails.getBadgePlain()));
 
         // Verify private profile parent
         assertThat(mUserManager.getProfileParent(mainUserId)).isNull();
-        UserInfo parentProfileInfo = mUserManager.getProfileParent(userInfo.id);
+        UserInfo parentProfileInfo = mUserManager.getProfileParent(privateProfileUser.id);
         assertThat(parentProfileInfo).isNotNull();
         assertThat(mainUserId).isEqualTo(parentProfileInfo.id);
-        removeUser(userInfo.id);
+        removeUser(privateProfileUser.id);
         assertThat(mUserManager.getProfileParent(mainUserId)).isNull();
-        assertThat(mUserManager.getProfileLabel()).isEqualTo(
+        assertThat(profileUM.getProfileLabel()).isEqualTo(
                 Resources.getSystem().getString(userTypeDetails.getLabel(0)));
     }
 
@@ -964,10 +968,13 @@
         assertThat(userTypeDetails.getName()).isEqualTo(UserManager.USER_TYPE_PROFILE_MANAGED);
 
         int mainUserId = mUserManager.getMainUser().getIdentifier();
-        UserInfo userInfo = createProfileForUser("Managed",
+        UserInfo managedProfileUser = createProfileForUser("Managed",
                 UserManager.USER_TYPE_PROFILE_MANAGED, mainUserId);
-        assertThat(userInfo).isNotNull();
-        final int userId = userInfo.id;
+        assertThat(managedProfileUser).isNotNull();
+        final int userId = managedProfileUser.id;
+        final UserManager profileUM = UserManager.get(
+                mContext.createPackageContextAsUser("android", 0,
+                        UserHandle.of(managedProfileUser.id)));
 
         assertThat(mUserManager.hasBadge(userId)).isEqualTo(userTypeDetails.hasBadge());
         assertThat(mUserManager.getUserIconBadgeResId(userId))
@@ -978,10 +985,10 @@
                 .isEqualTo(userTypeDetails.getBadgeNoBackground());
         assertThat(mUserManager.getUserStatusBarIconResId(userId))
                 .isEqualTo(userTypeDetails.getStatusBarIcon());
-        compareDrawables(mUserManager.getUserBadge(),
+        compareDrawables(profileUM.getUserBadge(),
                 Resources.getSystem().getDrawable(userTypeDetails.getBadgePlain()));
 
-        final int badgeIndex = userInfo.profileBadge;
+        final int badgeIndex = managedProfileUser.profileBadge;
         assertThat(mUserManager.getUserBadgeColor(userId)).isEqualTo(
                 Resources.getSystem().getColor(userTypeDetails.getBadgeColor(badgeIndex), null));
         assertThat(mUserManager.getUserBadgeDarkColor(userId)).isEqualTo(
@@ -1013,10 +1020,10 @@
 
         // Create an actual user (of this user type) and get its properties.
         int mainUserId = mUserManager.getMainUser().getIdentifier();
-        final UserInfo userInfo = createProfileForUser("Managed",
+        final UserInfo managedProfileUser = createProfileForUser("Managed",
                 UserManager.USER_TYPE_PROFILE_MANAGED, mainUserId);
-        assertThat(userInfo).isNotNull();
-        final int userId = userInfo.id;
+        assertThat(managedProfileUser).isNotNull();
+        final int userId = managedProfileUser.id;
         final UserProperties userProps = mUserManager.getUserProperties(UserHandle.of(userId));
 
         // Check that this new user has the expected properties (relative to the defaults)
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
index acac63c..0d6fdc9 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
@@ -65,6 +65,7 @@
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.PackageManager;
+import android.content.pm.ShortcutInfo;
 import android.content.pm.UserInfo;
 import android.graphics.Color;
 import android.graphics.drawable.Icon;
@@ -212,7 +213,9 @@
         // TODO (b/291907312): remove feature flag
         // Disable feature flags by default. Tests should enable as needed.
         mSetFlagsRule.disableFlags(Flags.FLAG_POLITE_NOTIFICATIONS,
-                Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS, Flags.FLAG_VIBRATE_WHILE_UNLOCKED);
+                Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS,
+                Flags.FLAG_VIBRATE_WHILE_UNLOCKED,
+                Flags.FLAG_POLITE_NOTIFICATIONS_ATTN_UPDATE);
 
         mService = spy(new NotificationManagerService(getContext(), mNotificationRecordLogger,
             mNotificationInstanceIdSequence));
@@ -410,8 +413,8 @@
             boolean defaultSound, boolean defaultLights, String groupKey, int groupAlertBehavior,
             boolean isLeanback, UserHandle userHandle) {
         return getNotificationRecord(id, insistent, once, noisy, buzzy, lights, defaultVibration,
-                defaultSound, defaultLights, groupKey, groupAlertBehavior, isLeanback, userHandle,
-                mPkg);
+                defaultSound, defaultLights, groupKey, groupAlertBehavior, isLeanback, false,
+                userHandle, mPkg);
     }
 
     private NotificationRecord getNotificationRecord(int id,
@@ -419,6 +422,16 @@
             boolean noisy, boolean buzzy, boolean lights, boolean defaultVibration,
             boolean defaultSound, boolean defaultLights, String groupKey, int groupAlertBehavior,
             boolean isLeanback, UserHandle userHandle, String packageName) {
+        return getNotificationRecord(id, insistent, once, noisy, buzzy, lights, defaultVibration,
+                defaultSound, defaultLights, groupKey, groupAlertBehavior, isLeanback, false,
+                userHandle, packageName);
+    }
+
+    private NotificationRecord getNotificationRecord(int id,
+            boolean insistent, boolean once,
+            boolean noisy, boolean buzzy, boolean lights, boolean defaultVibration,
+            boolean defaultSound, boolean defaultLights, String groupKey, int groupAlertBehavior,
+            boolean isLeanback, boolean isConversation, UserHandle userHandle, String packageName) {
 
         final Builder builder = new Builder(getContext())
             .setContentTitle("foo")
@@ -426,6 +439,10 @@
             .setPriority(Notification.PRIORITY_HIGH)
             .setOnlyAlertOnce(once);
 
+        if (isConversation) {
+            builder.setStyle(new Notification.MessagingStyle("test user"));
+        }
+
         int defaults = 0;
         if (noisy) {
             if (defaultSound) {
@@ -485,6 +502,19 @@
         return r;
     }
 
+    private NotificationRecord getConversationNotificationRecord(int id,
+            boolean insistent, boolean once,
+            boolean noisy, boolean buzzy, boolean lights, boolean defaultVibration,
+            boolean defaultSound, boolean defaultLights, String groupKey, int groupAlertBehavior,
+            boolean isLeanback, UserHandle userHandle, String packageName, String shortcutId) {
+        NotificationRecord r = getNotificationRecord(id, insistent, once, noisy, buzzy, lights,
+                defaultVibration, defaultSound, defaultLights, groupKey, groupAlertBehavior,
+                isLeanback, true, userHandle, packageName);
+        ShortcutInfo.Builder sb = new ShortcutInfo.Builder(getContext());
+        r.setShortcutInfo(sb.setId(shortcutId).build());
+        return r;
+    }
+
     //
     // Convenience functions for interacting with mocks
     //
@@ -2057,12 +2087,14 @@
 
         // set up internal state
         mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+        assertNotEquals(-1, r.getLastAudiblyAlertedMs());
         Mockito.reset(mRingtonePlayer);
 
         // update should beep at 50% volume
         r.isUpdate = true;
         mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
         verifyBeepVolume(0.5f);
+        assertNotEquals(-1, r.getLastAudiblyAlertedMs());
 
         // 2nd update should beep at 0% volume
         Mockito.reset(mRingtonePlayer);
@@ -2070,7 +2102,7 @@
         verifyBeepVolume(0.0f);
 
         verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt());
-        assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+        assertEquals(-1, r.getLastAudiblyAlertedMs());
     }
 
     @Test
@@ -2091,6 +2123,7 @@
 
         // set up internal state
         mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+        assertNotEquals(-1, r.getLastAudiblyAlertedMs());
         Mockito.reset(mRingtonePlayer);
 
         // Use different package for next notifications
@@ -2101,6 +2134,7 @@
         // update should beep at 50% volume
         mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS);
         verifyBeepVolume(0.5f);
+        assertNotEquals(-1, r2.getLastAudiblyAlertedMs());
 
         // Use different package for next notifications
         NotificationRecord r3 = getNotificationRecord(mId, false /* insistent */, false /* once */,
@@ -2113,7 +2147,7 @@
         verifyBeepVolume(0.0f);
 
         verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt());
-        assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+        assertEquals(-1, r3.getLastAudiblyAlertedMs());
     }
 
     @Test
@@ -2158,6 +2192,117 @@
     }
 
     @Test
+    public void testBeepVolume_politeNotif_AvalancheStrategy_AttnUpdate() throws Exception {
+        mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
+        mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
+        mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS_ATTN_UPDATE);
+        TestableFlagResolver flagResolver = new TestableFlagResolver();
+        flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50);
+        flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0);
+        initAttentionHelper(flagResolver);
+
+        // Trigger avalanche trigger intent
+        final Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        intent.putExtra("state", false);
+        mAvalancheBroadcastReceiver.onReceive(getContext(), intent);
+
+        NotificationRecord r = getBeepyNotification();
+
+        // set up internal state
+        mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+        assertEquals(-1, r.getLastAudiblyAlertedMs());
+        Mockito.reset(mRingtonePlayer);
+
+        // Use different package for next notifications
+        NotificationRecord r2 = getNotificationRecord(mId, false /* insistent */, false /* once */,
+                true /* noisy */, false /* buzzy*/, false /* lights */, true, true,
+                false, null, Notification.GROUP_ALERT_ALL, false, mUser, "anotherPkg");
+
+        // update should beep at 0% volume
+        mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS);
+        assertEquals(-1, r2.getLastAudiblyAlertedMs());
+        verifyBeepVolume(0.0f);
+
+        // Use different package for next notifications
+        NotificationRecord r3 = getNotificationRecord(mId, false /* insistent */, false /* once */,
+                true /* noisy */, false /* buzzy*/, false /* lights */, true, true,
+                false, null, Notification.GROUP_ALERT_ALL, false, mUser, "yetAnotherPkg");
+
+        // 2nd update should beep at 0% volume
+        Mockito.reset(mRingtonePlayer);
+        mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS);
+        verifyBeepVolume(0.0f);
+
+        verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt());
+        assertEquals(-1, r3.getLastAudiblyAlertedMs());
+    }
+
+    @Test
+    public void testBeepVolume_politeNotif_AvalancheStrategy_exempt_AttnUpdate()
+            throws Exception {
+        mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
+        mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
+        mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS_ATTN_UPDATE);
+        TestableFlagResolver flagResolver = new TestableFlagResolver();
+        flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50);
+        flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0);
+        initAttentionHelper(flagResolver);
+
+        // Trigger avalanche trigger intent
+        final Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+        intent.putExtra("state", false);
+        mAvalancheBroadcastReceiver.onReceive(getContext(), intent);
+
+        NotificationRecord r = getBeepyNotification();
+        r.getNotification().category = Notification.CATEGORY_EVENT;
+
+        // Should beep at 100% volume
+        mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+        verifyBeepVolume(1.0f);
+        assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+        Mockito.reset(mRingtonePlayer);
+
+        // Use different package for next notifications
+        NotificationRecord r2 = getConversationNotificationRecord(mId, false /* insistent */,
+                false /* once */, true /* noisy */, false /* buzzy*/, false /* lights */, true,
+                true, false, null, Notification.GROUP_ALERT_ALL, false, mUser, "anotherPkg",
+                "shortcut");
+
+        // Should beep at 100% volume
+        mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS);
+        assertNotEquals(-1, r2.getLastAudiblyAlertedMs());
+        verifyBeepVolume(1.0f);
+
+        // Use different package for next notifications
+        mChannel = new NotificationChannel("test3", "test3", IMPORTANCE_DEFAULT);
+        NotificationRecord r3 = getNotificationRecord(mId, false /* insistent */, false /* once */,
+                true /* noisy */, false /* buzzy*/, false /* lights */, true, true,
+                false, null, Notification.GROUP_ALERT_ALL, false, mUser, "yetAnotherPkg");
+
+        r3.getNotification().category = Notification.CATEGORY_REMINDER;
+
+        // Should beep at 100% volume
+        Mockito.reset(mRingtonePlayer);
+        mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS);
+        assertNotEquals(-1, r3.getLastAudiblyAlertedMs());
+        verifyBeepVolume(1.0f);
+
+        // Same package as r3 for next notifications
+        NotificationRecord r4 = getConversationNotificationRecord(mId, false /* insistent */,
+                false /* once */, true /* noisy */, false /* buzzy*/, false /* lights */, true,
+                true, false, null, Notification.GROUP_ALERT_ALL, false, mUser, "yetAnotherPkg",
+                "shortcut");
+
+        // 2nd update should beep at 50% volume
+        Mockito.reset(mRingtonePlayer);
+        mAttentionHelper.buzzBeepBlinkLocked(r4, DEFAULT_SIGNALS);
+        verifyBeepVolume(0.5f);
+
+        verify(mAccessibilityService, times(4)).sendAccessibilityEvent(any(), anyInt());
+        assertNotEquals(-1, r4.getLastAudiblyAlertedMs());
+    }
+
+    @Test
     public void testBeepVolume_politeNotif_applyPerApp() throws Exception {
         mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
         mSetFlagsRule.disableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
@@ -2181,11 +2326,13 @@
         // update should beep at 50% volume
         NotificationRecord r2 = getBeepyNotification();
         mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS);
+        assertNotEquals(-1, r2.getLastAudiblyAlertedMs());
         verifyBeepVolume(0.5f);
 
         // 2nd update should beep at 0% volume
         Mockito.reset(mRingtonePlayer);
         mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS);
+        assertEquals(-1, r2.getLastAudiblyAlertedMs());
         verifyBeepVolume(0.0f);
 
         // Use different package for next notifications
@@ -2199,7 +2346,7 @@
         verifyBeepVolume(1.0f);
 
         verify(mAccessibilityService, times(4)).sendAccessibilityEvent(any(), anyInt());
-        assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+        assertNotEquals(-1, r3.getLastAudiblyAlertedMs());
     }
 
     @Test
@@ -2227,11 +2374,13 @@
         // update should beep at 100% volume
         NotificationRecord r2 = getBeepyNotification();
         mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS);
+        assertNotEquals(-1, r2.getLastAudiblyAlertedMs());
         verifyBeepVolume(1.0f);
 
         // 2nd update should beep at 50% volume
         Mockito.reset(mRingtonePlayer);
         mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS);
+        assertNotEquals(-1, r2.getLastAudiblyAlertedMs());
         verifyBeepVolume(0.5f);
 
         // Use different package for next notifications
@@ -2246,7 +2395,7 @@
         verifyBeepVolume(1.0f);
 
         verify(mAccessibilityService, times(4)).sendAccessibilityEvent(any(), anyInt());
-        assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+        assertNotEquals(-1, r3.getLastAudiblyAlertedMs());
     }
 
     @Test
@@ -2355,12 +2504,14 @@
 
         // set up internal state
         mAttentionHelper.buzzBeepBlinkLocked(r, WORK_PROFILE_SIGNALS);
+        assertNotEquals(-1, r.getLastAudiblyAlertedMs());
         Mockito.reset(mRingtonePlayer);
 
         // update should beep at 50% volume
         r.isUpdate = true;
         mAttentionHelper.buzzBeepBlinkLocked(r, WORK_PROFILE_SIGNALS);
         verifyBeepVolume(0.5f);
+        assertNotEquals(-1, r.getLastAudiblyAlertedMs());
 
         // 2nd update should beep at 0% volume
         Mockito.reset(mRingtonePlayer);
@@ -2368,7 +2519,7 @@
         verifyBeepVolume(0.0f);
 
         verify(mAccessibilityService, times(3)).sendAccessibilityEvent(any(), anyInt());
-        assertNotEquals(-1, r.getLastAudiblyAlertedMs());
+        assertEquals(-1, r.getLastAudiblyAlertedMs());
     }
 
     @Test
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index ef879ee..39a962d 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -5681,8 +5681,7 @@
         mService.mLocaleChangeReceiver.onReceive(mContext,
                 new Intent(Intent.ACTION_LOCALE_CHANGED));
 
-        verify(mZenModeHelper, times(1)).updateDefaultZenRules(
-                anyInt());
+        verify(mZenModeHelper).updateZenRulesOnLocaleChange();
     }
 
     private void simulateNotificationTimeoutBroadcast(String notificationKey) {
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/SystemZenRulesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/SystemZenRulesTest.java
new file mode 100644
index 0000000..e782461
--- /dev/null
+++ b/services/tests/uiservicestests/src/com/android/server/notification/SystemZenRulesTest.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2024 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.notification;
+
+import static android.service.notification.SystemZenRules.getTriggerDescriptionForScheduleTime;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.AutomaticZenRule;
+import android.content.res.Configuration;
+import android.os.LocaleList;
+import android.service.notification.SystemZenRules;
+import android.service.notification.ZenModeConfig;
+import android.service.notification.ZenModeConfig.EventInfo;
+import android.service.notification.ZenModeConfig.ScheduleInfo;
+import android.service.notification.ZenModeConfig.ZenRule;
+
+import com.android.internal.R;
+import com.android.server.UiServiceTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Calendar;
+import java.util.Locale;
+
+public class SystemZenRulesTest extends UiServiceTestCase {
+
+    private static final ScheduleInfo SCHEDULE_INFO;
+    private static final EventInfo EVENT_INFO;
+
+    static {
+        SCHEDULE_INFO = new ScheduleInfo();
+        SCHEDULE_INFO.days = new int[] { Calendar.WEDNESDAY };
+        SCHEDULE_INFO.startHour = 8;
+        SCHEDULE_INFO.endHour = 9;
+        EVENT_INFO = new EventInfo();
+        EVENT_INFO.calendarId = 1L;
+        EVENT_INFO.calName = "myCalendar";
+    }
+
+    @Before
+    public void setUp() {
+        Configuration config = new Configuration();
+        config.setLocales(new LocaleList(Locale.US));
+        mContext.getOrCreateTestableResources().overrideConfiguration(config);
+        mContext.getOrCreateTestableResources().addOverride(
+                R.string.zen_mode_trigger_summary_range_symbol_combination, "%1$s-%2$s");
+        mContext.getOrCreateTestableResources().addOverride(
+                R.string.zen_mode_trigger_summary_divider_text, ",");
+    }
+
+    @Test
+    public void maybeUpgradeRules_oldSystemRules_upgraded() {
+        ZenModeConfig config = new ZenModeConfig();
+        ZenRule timeRule = new ZenRule();
+        timeRule.pkg = SystemZenRules.PACKAGE_ANDROID;
+        timeRule.conditionId = ZenModeConfig.toScheduleConditionId(SCHEDULE_INFO);
+        config.automaticRules.put("time", timeRule);
+        ZenRule calendarRule = new ZenRule();
+        calendarRule.pkg = SystemZenRules.PACKAGE_ANDROID;
+        calendarRule.conditionId = ZenModeConfig.toEventConditionId(EVENT_INFO);
+        config.automaticRules.put("calendar", calendarRule);
+
+        SystemZenRules.maybeUpgradeRules(mContext, config);
+
+        assertThat(timeRule.type).isEqualTo(AutomaticZenRule.TYPE_SCHEDULE_TIME);
+        assertThat(timeRule.triggerDescription).isNotEmpty();
+        assertThat(calendarRule.type).isEqualTo(AutomaticZenRule.TYPE_SCHEDULE_CALENDAR);
+        assertThat(timeRule.triggerDescription).isNotEmpty();
+    }
+
+    @Test
+    public void maybeUpgradeRules_newSystemRules_untouched() {
+        ZenModeConfig config = new ZenModeConfig();
+        ZenRule timeRule = new ZenRule();
+        timeRule.pkg = SystemZenRules.PACKAGE_ANDROID;
+        timeRule.type = AutomaticZenRule.TYPE_SCHEDULE_TIME;
+        timeRule.conditionId = ZenModeConfig.toScheduleConditionId(SCHEDULE_INFO);
+        config.automaticRules.put("time", timeRule);
+        ZenRule original = timeRule.copy();
+
+        SystemZenRules.maybeUpgradeRules(mContext, config);
+
+        assertThat(timeRule).isEqualTo(original);
+    }
+
+    @Test
+    public void maybeUpgradeRules_appOwnedRules_untouched() {
+        ZenModeConfig config = new ZenModeConfig();
+        ZenRule timeRule = new ZenRule();
+        timeRule.pkg = "some_other_package";
+        timeRule.type = AutomaticZenRule.TYPE_SCHEDULE_TIME;
+        timeRule.conditionId = ZenModeConfig.toScheduleConditionId(SCHEDULE_INFO);
+        config.automaticRules.put("time", timeRule);
+        ZenRule original = timeRule.copy();
+
+        SystemZenRules.maybeUpgradeRules(mContext, config);
+
+        assertThat(timeRule).isEqualTo(original);
+    }
+
+    @Test
+    public void getTriggerDescriptionForScheduleTime_noOrSingleDays() {
+        // Test various cases for grouping and not-grouping of days.
+        ScheduleInfo scheduleInfo = new ScheduleInfo();
+        scheduleInfo.startHour = 10;
+        scheduleInfo.endHour = 16;
+
+        // No days
+        scheduleInfo.days = new int[]{};
+        assertThat(getTriggerDescriptionForScheduleTime(mContext, scheduleInfo)).isNull();
+
+        // A single day at the beginning of the week
+        scheduleInfo.days = new int[]{Calendar.SUNDAY};
+        assertThat(getTriggerDescriptionForScheduleTime(mContext, scheduleInfo))
+                .isEqualTo("Sun,10:00 AM-4:00 PM");
+
+        // A single day in the middle of the week
+        scheduleInfo.days = new int[]{Calendar.THURSDAY};
+        assertThat(getTriggerDescriptionForScheduleTime(mContext, scheduleInfo))
+                .isEqualTo("Thu,10:00 AM-4:00 PM");
+
+        // A single day at the end of the week
+        scheduleInfo.days = new int[]{Calendar.SATURDAY};
+        assertThat(getTriggerDescriptionForScheduleTime(mContext, scheduleInfo))
+                .isEqualTo("Sat,10:00 AM-4:00 PM");
+    }
+
+    @Test
+    public void getTriggerDescriptionForScheduleTime_oneGroup() {
+        ScheduleInfo scheduleInfo = new ScheduleInfo();
+        scheduleInfo.startHour = 10;
+        scheduleInfo.endHour = 16;
+
+        // The whole week
+        scheduleInfo.days = new int[] {Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY,
+                Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY, Calendar.SATURDAY};
+        assertThat(getTriggerDescriptionForScheduleTime(mContext, scheduleInfo))
+                .isEqualTo("Sun-Sat,10:00 AM-4:00 PM");
+
+        // Various cases of one big group
+        // Sunday through Thursday
+        scheduleInfo.days = new int[] {Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY,
+                Calendar.WEDNESDAY, Calendar.THURSDAY};
+        assertThat(getTriggerDescriptionForScheduleTime(mContext, scheduleInfo))
+                .isEqualTo("Sun-Thu,10:00 AM-4:00 PM");
+
+        // Wednesday through Saturday
+        scheduleInfo.days = new int[] {Calendar.WEDNESDAY, Calendar.THURSDAY,
+                Calendar.FRIDAY, Calendar.SATURDAY};
+        assertThat(getTriggerDescriptionForScheduleTime(mContext, scheduleInfo))
+                .isEqualTo("Wed-Sat,10:00 AM-4:00 PM");
+
+        // Monday through Friday
+        scheduleInfo.days = new int[] {Calendar.MONDAY, Calendar.TUESDAY,
+                Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY};
+        assertThat(getTriggerDescriptionForScheduleTime(mContext, scheduleInfo))
+                .isEqualTo("Mon-Fri,10:00 AM-4:00 PM");
+    }
+
+    @Test
+    public void getTriggerDescriptionForScheduleTime_mixedDays() {
+        ScheduleInfo scheduleInfo = new ScheduleInfo();
+        scheduleInfo.startHour = 10;
+        scheduleInfo.endHour = 16;
+
+        // cases combining groups and single days scattered around
+        scheduleInfo.days = new int[] {Calendar.SUNDAY, Calendar.TUESDAY,
+                Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.SATURDAY};
+        assertThat(getTriggerDescriptionForScheduleTime(mContext, scheduleInfo))
+                .isEqualTo("Sun,Tue-Thu,Sat,10:00 AM-4:00 PM");
+
+        scheduleInfo.days = new int[] {Calendar.SUNDAY, Calendar.MONDAY, Calendar.TUESDAY,
+                Calendar.WEDNESDAY, Calendar.FRIDAY, Calendar.SATURDAY};
+        assertThat(getTriggerDescriptionForScheduleTime(mContext, scheduleInfo))
+                .isEqualTo("Sun-Wed,Fri-Sat,10:00 AM-4:00 PM");
+
+        scheduleInfo.days = new int[] {Calendar.MONDAY, Calendar.WEDNESDAY,
+                Calendar.FRIDAY, Calendar.SATURDAY};
+        assertThat(getTriggerDescriptionForScheduleTime(mContext, scheduleInfo))
+                .isEqualTo("Mon,Wed,Fri-Sat,10:00 AM-4:00 PM");
+    }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index 6f07472..3df52c7 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -136,6 +136,7 @@
 import android.provider.Settings.Global;
 import android.service.notification.Condition;
 import android.service.notification.DeviceEffectsApplier;
+import android.service.notification.SystemZenRules;
 import android.service.notification.ZenAdapters;
 import android.service.notification.ZenDeviceEffects;
 import android.service.notification.ZenModeConfig;
@@ -193,6 +194,7 @@
 import java.time.ZoneOffset;
 import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
+import java.util.Calendar;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Objects;
@@ -1087,6 +1089,11 @@
         mZenModeHelper.mConfig.manualRule.enabled = true;
 
         ZenModeConfig expected = mZenModeHelper.mConfig.copy();
+        if (Flags.modesUi()) {
+            // Reading the configuration will upgrade it, so for equality comparison also upgrade
+            // the expected value.
+            SystemZenRules.maybeUpgradeRules(mContext, expected);
+        }
 
         ByteArrayOutputStream baos = writeXmlAndPurge(null);
         TypedXmlPullParser parser = getParserForByteStream(baos);
@@ -1404,6 +1411,13 @@
         mZenModeHelper.readXml(parser, true, 11);
 
         ZenModeConfig actual = mZenModeHelper.mConfigs.get(10);
+        if (Flags.modesUi()) {
+            // Reading the configuration will upgrade it, so for equality comparison also upgrade
+            // the expected value.
+            SystemZenRules.maybeUpgradeRules(mContext, config10);
+            SystemZenRules.maybeUpgradeRules(mContext, config11);
+        }
+
         assertEquals(
                 "Config mismatch: current vs expected: "
                         + new ZenModeDiff.ConfigDiff(actual, config10), config10, actual);
@@ -1483,6 +1497,11 @@
         mZenModeHelper.mConfig.automaticRules = automaticRules;
 
         ZenModeConfig expected = mZenModeHelper.mConfig.copy();
+        if (Flags.modesUi()) {
+            // Reading the configuration will upgrade it, so for equality comparison also upgrade
+            // the expected value.
+            SystemZenRules.maybeUpgradeRules(mContext, expected);
+        }
 
         ByteArrayOutputStream baos = writeXmlAndPurge(null);
         TypedXmlPullParser parser = Xml.newFastPullParser();
@@ -1494,7 +1513,8 @@
         ZenModeConfig.ZenRule original = expected.automaticRules.get(ruleId);
         ZenModeConfig.ZenRule current = mZenModeHelper.mConfig.automaticRules.get(ruleId);
 
-        assertEquals("Automatic rules mismatch", original, current);
+        assertEquals("Automatic rules mismatch: current vs expected: "
+                + new ZenModeDiff.RuleDiff(original, current), original, current);
     }
 
     @Test
@@ -1524,6 +1544,11 @@
         mZenModeHelper.mConfig.automaticRules = automaticRules;
 
         ZenModeConfig expected = mZenModeHelper.mConfig.copy();
+        if (Flags.modesUi()) {
+            // Reading the configuration will upgrade it, so for equality comparison also upgrade
+            // the expected value.
+            SystemZenRules.maybeUpgradeRules(mContext, expected);
+        }
 
         ByteArrayOutputStream baos = writeXmlAndPurgeForUser(null, UserHandle.USER_SYSTEM);
         TypedXmlPullParser parser = getParserForByteStream(baos);
@@ -1532,7 +1557,8 @@
         ZenModeConfig.ZenRule original = expected.automaticRules.get(ruleId);
         ZenModeConfig.ZenRule current = mZenModeHelper.mConfig.automaticRules.get(ruleId);
 
-        assertEquals("Automatic rules mismatch", original, current);
+        assertEquals("Automatic rules mismatch: current vs expected: "
+                + new ZenModeDiff.RuleDiff(original, current), original, current);
     }
 
     @Test
@@ -2112,7 +2138,7 @@
         ZenModeConfig config = new ZenModeConfig();
         config.automaticRules = new ArrayMap<>();
         mZenModeHelper.mConfig = config;
-        mZenModeHelper.updateDefaultZenRules(Process.SYSTEM_UID); // shouldn't throw null pointer
+        mZenModeHelper.updateZenRulesOnLocaleChange(); // shouldn't throw null pointer
         mZenModeHelper.pullRules(events); // shouldn't throw null pointer
     }
 
@@ -2137,33 +2163,7 @@
         autoRules.put(SCHEDULE_DEFAULT_RULE_ID, updatedDefaultRule);
         mZenModeHelper.mConfig.automaticRules = autoRules;
 
-        mZenModeHelper.updateDefaultZenRules(Process.SYSTEM_UID);
-        assertEquals(updatedDefaultRule,
-                mZenModeHelper.mConfig.automaticRules.get(SCHEDULE_DEFAULT_RULE_ID));
-    }
-
-    @Test
-    public void testDoNotUpdateEnabledDefaultAutoRule() {
-        // mDefaultConfig is set to default config in setup by getDefaultConfigParser
-        when(mContext.checkCallingPermission(anyString()))
-                .thenReturn(PERMISSION_GRANTED);
-
-        // shouldn't update the rule that's enabled
-        ZenModeConfig.ZenRule updatedDefaultRule = new ZenModeConfig.ZenRule();
-        updatedDefaultRule.enabled = true;
-        updatedDefaultRule.modified = false;
-        updatedDefaultRule.creationTime = 0;
-        updatedDefaultRule.id = SCHEDULE_DEFAULT_RULE_ID;
-        updatedDefaultRule.name = "Schedule Default Rule";
-        updatedDefaultRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
-        updatedDefaultRule.conditionId = ZenModeConfig.toScheduleConditionId(new ScheduleInfo());
-        updatedDefaultRule.component = new ComponentName("android", "ScheduleConditionProvider");
-
-        ArrayMap<String, ZenModeConfig.ZenRule> autoRules = new ArrayMap<>();
-        autoRules.put(SCHEDULE_DEFAULT_RULE_ID, updatedDefaultRule);
-        mZenModeHelper.mConfig.automaticRules = autoRules;
-
-        mZenModeHelper.updateDefaultZenRules(Process.SYSTEM_UID);
+        mZenModeHelper.updateZenRulesOnLocaleChange();
         assertEquals(updatedDefaultRule,
                 mZenModeHelper.mConfig.automaticRules.get(SCHEDULE_DEFAULT_RULE_ID));
     }
@@ -2177,27 +2177,35 @@
 
         // will update rule that is not enabled and modified
         ZenModeConfig.ZenRule customDefaultRule = new ZenModeConfig.ZenRule();
+        customDefaultRule.pkg = SystemZenRules.PACKAGE_ANDROID;
         customDefaultRule.enabled = false;
         customDefaultRule.modified = false;
         customDefaultRule.creationTime = 0;
         customDefaultRule.id = SCHEDULE_DEFAULT_RULE_ID;
         customDefaultRule.name = "Schedule Default Rule";
         customDefaultRule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
-        customDefaultRule.conditionId = ZenModeConfig.toScheduleConditionId(new ScheduleInfo());
+        ScheduleInfo scheduleInfo = new ScheduleInfo();
+        scheduleInfo.days = new int[] { Calendar.SUNDAY };
+        scheduleInfo.startHour = 18;
+        scheduleInfo.endHour = 19;
+        customDefaultRule.conditionId = ZenModeConfig.toScheduleConditionId(scheduleInfo);
         customDefaultRule.component = new ComponentName("android", "ScheduleConditionProvider");
 
         ArrayMap<String, ZenModeConfig.ZenRule> autoRules = new ArrayMap<>();
         autoRules.put(SCHEDULE_DEFAULT_RULE_ID, customDefaultRule);
         mZenModeHelper.mConfig.automaticRules = autoRules;
 
-        mZenModeHelper.updateDefaultZenRules(Process.SYSTEM_UID);
+        mZenModeHelper.updateZenRulesOnLocaleChange();
         ZenModeConfig.ZenRule ruleAfterUpdating =
                 mZenModeHelper.mConfig.automaticRules.get(SCHEDULE_DEFAULT_RULE_ID);
         assertEquals(customDefaultRule.enabled, ruleAfterUpdating.enabled);
         assertEquals(customDefaultRule.modified, ruleAfterUpdating.modified);
         assertEquals(customDefaultRule.id, ruleAfterUpdating.id);
         assertEquals(customDefaultRule.conditionId, ruleAfterUpdating.conditionId);
-        assertFalse(Objects.equals(defaultRuleName, ruleAfterUpdating.name)); // update name
+        assertNotEquals(defaultRuleName, ruleAfterUpdating.name); // update name
+        if (Flags.modesUi()) {
+            assertThat(ruleAfterUpdating.triggerDescription).isNotEmpty(); // update trigger desc
+        }
     }
 
     @Test
diff --git a/tests/testables/OWNERS b/tests/testables/OWNERS
new file mode 100644
index 0000000..a6f1632
--- /dev/null
+++ b/tests/testables/OWNERS
@@ -0,0 +1 @@
+file:/packages/SystemUI/OWNERS
\ No newline at end of file
diff --git a/tests/testables/src/android/testing/TestableResources.java b/tests/testables/src/android/testing/TestableResources.java
index 0ec106e..384a21e 100644
--- a/tests/testables/src/android/testing/TestableResources.java
+++ b/tests/testables/src/android/testing/TestableResources.java
@@ -26,6 +26,8 @@
 
 import org.mockito.invocation.InvocationOnMock;
 
+import java.util.Arrays;
+
 /**
  * Provides a version of Resources that defaults to all existing resources, but can have ids
  * changed to return specific values.
@@ -103,6 +105,15 @@
                     if (index >= 0) {
                         Object value = mOverrides.valueAt(index);
                         if (value == null) throw new Resources.NotFoundException();
+                        // Support for Resources.getString(resId, Object... formatArgs)
+                        if (value instanceof String
+                                && invocationOnMock.getMethod().getName().equals("getString")
+                                && invocationOnMock.getArguments().length > 1) {
+                            value = String.format(mResources.getConfiguration().getLocales().get(0),
+                                    (String) value,
+                                    Arrays.copyOfRange(invocationOnMock.getArguments(), 1,
+                                            invocationOnMock.getArguments().length));
+                        }
                         return value;
                     }
                 } catch (Resources.NotFoundException e) {