Merge "Remove unneeded getUserOrDefault" into main
diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java
index c1c96ea..014e4660 100644
--- a/core/java/android/app/WallpaperManager.java
+++ b/core/java/android/app/WallpaperManager.java
@@ -285,6 +285,14 @@
     public static final String COMMAND_UNFREEZE = "android.wallpaper.unfreeze";
 
     /**
+     * Command for {@link #sendWallpaperCommand}: in sendWallpaperCommand put extra to this command
+     * to give the bounds of space between the bottom of notifications and the top of shortcuts
+     * @hide
+     */
+    public static final String COMMAND_LOCKSCREEN_LAYOUT_CHANGED =
+            "android.wallpaper.lockscreen_layout_changed";
+
+    /**
      * Extra passed back from setWallpaper() giving the new wallpaper's assigned ID.
      * @hide
      */
diff --git a/core/java/android/app/wearable/flags.aconfig b/core/java/android/app/wearable/flags.aconfig
index b68bafe..534f461 100644
--- a/core/java/android/app/wearable/flags.aconfig
+++ b/core/java/android/app/wearable/flags.aconfig
@@ -38,4 +38,12 @@
     namespace: "machine_learning"
     description: "This flag enables the APIs related to hotword in WearableSensingManager and WearableSensingService."
     bug: "310055381"
+}
+
+flag {
+    name: "enable_concurrent_wearable_connections"
+    is_exported: true
+    namespace: "machine_learning"
+    description: "This flag enables the APIs for providing multiple concurrent connections to the WearableSensingService."
+    bug: "358133158"
 }
\ No newline at end of file
diff --git a/core/java/android/os/SystemClock.java b/core/java/android/os/SystemClock.java
index dfc591b..8ef62e3 100644
--- a/core/java/android/os/SystemClock.java
+++ b/core/java/android/os/SystemClock.java
@@ -62,7 +62,7 @@
  *     sleep (CPU off, display dark, device waiting for external input),
  *     but is not affected by clock scaling, idle, or other power saving
  *     mechanisms.  This is the basis for most interval timing
- *     such as {@link Thread#sleep(long) Thread.sleep(millls)},
+ *     such as {@link Thread#sleep(long) Thread.sleep(millis)},
  *     {@link Object#wait(long) Object.wait(millis)}, and
  *     {@link System#nanoTime System.nanoTime()}.  This clock is guaranteed
  *     to be monotonic, and is suitable for interval timing when the
diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index 06820cd..d7b5211 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -46,6 +46,7 @@
 import android.app.PendingIntent;
 import android.app.RemoteInput;
 import android.appwidget.AppWidgetHostView;
+import android.appwidget.flags.Flags;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.ComponentName;
 import android.content.Context;
@@ -4119,6 +4120,71 @@
         public void visitUris(@NonNull Consumer<Uri> visitor) {
             mNestedViews.visitUris(visitor);
         }
+
+        @Override
+        public boolean canWriteToProto() {
+            return true;
+        }
+
+        @Override
+        public void writeToProto(ProtoOutputStream out, Context context, Resources appResources) {
+            if (!Flags.remoteViewsProto()) return;
+            final long token = out.start(RemoteViewsProto.Action.VIEW_GROUP_ADD_ACTION);
+            out.write(RemoteViewsProto.ViewGroupAddAction.VIEW_ID,
+                    appResources.getResourceName(mViewId));
+            out.write(RemoteViewsProto.ViewGroupAddAction.INDEX, mIndex);
+            out.write(RemoteViewsProto.ViewGroupAddAction.STABLE_ID, mStableId);
+            long rvToken = out.start(RemoteViewsProto.ViewGroupAddAction.NESTED_VIEWS);
+            mNestedViews.writePreviewToProto(context, out);
+            out.end(rvToken);
+            out.end(token);
+        }
+    }
+
+    private PendingResources<Action> createViewGroupActionAddFromProto(ProtoInputStream in)
+            throws Exception {
+        final LongSparseArray<Object> values = new LongSparseArray<>();
+
+        final long token = in.start(RemoteViewsProto.Action.VIEW_GROUP_ADD_ACTION);
+        while (in.nextField() != NO_MORE_FIELDS) {
+            switch (in.getFieldNumber()) {
+                case (int) RemoteViewsProto.ViewGroupAddAction.VIEW_ID:
+                    values.put(RemoteViewsProto.ViewGroupAddAction.VIEW_ID,
+                            in.readString(RemoteViewsProto.ViewGroupAddAction.VIEW_ID));
+                    break;
+                case (int) RemoteViewsProto.ViewGroupAddAction.NESTED_VIEWS:
+                    final long nvToken = in.start(RemoteViewsProto.ViewGroupAddAction.NESTED_VIEWS);
+                    values.put(RemoteViewsProto.ViewGroupAddAction.NESTED_VIEWS,
+                            createFromProto(in));
+                    in.end(nvToken);
+                    break;
+                case (int) RemoteViewsProto.ViewGroupAddAction.INDEX:
+                    values.put(RemoteViewsProto.ViewGroupAddAction.INDEX,
+                            in.readInt(RemoteViewsProto.ViewGroupAddAction.INDEX));
+                    break;
+                case (int) RemoteViewsProto.ViewGroupAddAction.STABLE_ID:
+                    values.put(RemoteViewsProto.ViewGroupAddAction.STABLE_ID,
+                            in.readInt(RemoteViewsProto.ViewGroupAddAction.STABLE_ID));
+                    break;
+                default:
+                    Log.w(LOG_TAG, "Unhandled field while reading RemoteViews proto!\n"
+                            + ProtoUtils.currentFieldToString(in));
+            }
+        }
+        in.end(token);
+
+        checkContainsKeys(values, new long[]{RemoteViewsProto.ViewGroupAddAction.VIEW_ID,
+                RemoteViewsProto.ViewGroupAddAction.NESTED_VIEWS});
+
+        return (context, resources, rootData, depth) -> {
+            int viewId = getAsIdentifier(resources, values,
+                    RemoteViewsProto.ViewGroupAddAction.VIEW_ID);
+            return new ViewGroupActionAdd(viewId, ((PendingResources<RemoteViews>) values.get(
+                    RemoteViewsProto.ViewGroupAddAction.NESTED_VIEWS)).create(context, resources,
+                    rootData, depth),
+                    (int) values.get(RemoteViewsProto.ViewGroupAddAction.INDEX, 0),
+                    (int) values.get(RemoteViewsProto.ViewGroupAddAction.STABLE_ID, 0));
+        };
     }
 
     /**
@@ -4241,6 +4307,60 @@
         public int mergeBehavior() {
             return MERGE_APPEND;
         }
+
+        @Override
+        public boolean canWriteToProto() {
+            return true;
+        }
+
+        @Override
+        public void writeToProto(ProtoOutputStream out, Context context, Resources appResources) {
+            final long token = out.start(RemoteViewsProto.Action.VIEW_GROUP_REMOVE_ACTION);
+            out.write(RemoteViewsProto.ViewGroupRemoveAction.VIEW_ID,
+                    appResources.getResourceName(mViewId));
+            if (mViewIdToKeep != REMOVE_ALL_VIEWS_ID) {
+                out.write(RemoteViewsProto.ViewGroupRemoveAction.VIEW_ID_TO_KEEP,
+                        appResources.getResourceName(mViewIdToKeep));
+            }
+            out.end(token);
+        }
+
+        public static PendingResources<Action> createFromProto(ProtoInputStream in)
+                throws Exception {
+            final LongSparseArray<Object> values = new LongSparseArray<>();
+
+            final long token = in.start(RemoteViewsProto.Action.VIEW_GROUP_REMOVE_ACTION);
+            while (in.nextField() != NO_MORE_FIELDS) {
+                switch (in.getFieldNumber()) {
+                    case (int) RemoteViewsProto.ViewGroupRemoveAction.VIEW_ID:
+                        values.put(RemoteViewsProto.ViewGroupRemoveAction.VIEW_ID,
+                                in.readString(RemoteViewsProto.ViewGroupRemoveAction.VIEW_ID));
+                        break;
+                    case (int) RemoteViewsProto.ViewGroupRemoveAction.VIEW_ID_TO_KEEP:
+                        values.put(RemoteViewsProto.ViewGroupRemoveAction.VIEW_ID_TO_KEEP,
+                                in.readString(
+                                        RemoteViewsProto.ViewGroupRemoveAction.VIEW_ID_TO_KEEP));
+                        break;
+                    default:
+                        Log.w(LOG_TAG, "Unhandled field while reading RemoteViews proto!\n"
+                                + ProtoUtils.currentFieldToString(in));
+                }
+            }
+            in.end(token);
+
+            checkContainsKeys(values, new long[]{RemoteViewsProto.ViewGroupRemoveAction.VIEW_ID});
+
+            return (context, resources, rootData, depth) -> {
+                int viewId = getAsIdentifier(resources, values,
+                        RemoteViewsProto.ViewGroupRemoveAction.VIEW_ID);
+                int viewIdToKeep = (values.indexOfKey(
+                        RemoteViewsProto.ViewGroupRemoveAction.VIEW_ID_TO_KEEP) >= 0)
+                        ? getAsIdentifier(resources, values,
+                        RemoteViewsProto.ViewGroupRemoveAction.VIEW_ID_TO_KEEP)
+                        : REMOVE_ALL_VIEWS_ID;
+                return new ViewGroupActionRemove(viewId, viewIdToKeep);
+            };
+        }
     }
 
     /**
@@ -4497,6 +4617,200 @@
                 visitIconUri(mI4, visitor);
             }
         }
+
+        @Override
+        public boolean canWriteToProto() {
+            return true;
+        }
+
+        @Override
+        public void writeToProto(ProtoOutputStream out, Context context,
+                Resources appResources) { // rebase
+            final long token = out.start(RemoteViewsProto.Action.TEXT_VIEW_DRAWABLE_ACTION);
+            out.write(RemoteViewsProto.TextViewDrawableAction.VIEW_ID,
+                    appResources.getResourceName(mViewId));
+            out.write(RemoteViewsProto.TextViewDrawableAction.IS_RELATIVE, mIsRelative);
+            if (mUseIcons) {
+                long iconsToken = out.start(RemoteViewsProto.TextViewDrawableAction.ICONS);
+                if (mI1 != null) {
+                    writeIconToProto(out, appResources, mI1,
+                            RemoteViewsProto.TextViewDrawableAction.Icons.ONE);
+                }
+                if (mI2 != null) {
+                    writeIconToProto(out, appResources, mI2,
+                            RemoteViewsProto.TextViewDrawableAction.Icons.TWO);
+                }
+                if (mI3 != null) {
+                    writeIconToProto(out, appResources, mI3,
+                            RemoteViewsProto.TextViewDrawableAction.Icons.THREE);
+                }
+                if (mI4 != null) {
+                    writeIconToProto(out, appResources, mI4,
+                            RemoteViewsProto.TextViewDrawableAction.Icons.FOUR);
+                }
+                out.end(iconsToken);
+            } else {
+                long resourcesToken = out.start(RemoteViewsProto.TextViewDrawableAction.RESOURCES);
+                if (mD1 != 0) {
+                    out.write(RemoteViewsProto.TextViewDrawableAction.Resources.ONE,
+                            appResources.getResourceName(mD1));
+                }
+                if (mD2 != 0) {
+                    out.write(RemoteViewsProto.TextViewDrawableAction.Resources.TWO,
+                            appResources.getResourceName(mD2));
+                }
+                if (mD3 != 0) {
+                    out.write(RemoteViewsProto.TextViewDrawableAction.Resources.THREE,
+                            appResources.getResourceName(mD3));
+                }
+                if (mD4 != 0) {
+                    out.write(RemoteViewsProto.TextViewDrawableAction.Resources.FOUR,
+                            appResources.getResourceName(mD4));
+                }
+                out.end(resourcesToken);
+            }
+            out.end(token);
+        }
+
+        public static PendingResources<Action> createFromProto(ProtoInputStream in)
+                throws Exception {
+            final LongSparseArray<Object> values = new LongSparseArray<>();
+
+            values.put(RemoteViewsProto.TextViewDrawableAction.ICONS,
+                    new SparseArray<PendingResources<Icon>>());
+            values.put(RemoteViewsProto.TextViewDrawableAction.RESOURCES,
+                    new SparseArray<String>());
+            final long token = in.start(RemoteViewsProto.Action.TEXT_VIEW_DRAWABLE_ACTION);
+            while (in.nextField() != NO_MORE_FIELDS) {
+                switch (in.getFieldNumber()) {
+                    case (int) RemoteViewsProto.TextViewDrawableAction.VIEW_ID:
+                        values.put(RemoteViewsProto.TextViewDrawableAction.VIEW_ID,
+                                in.readString(RemoteViewsProto.TextViewDrawableAction.VIEW_ID));
+                        break;
+                    case (int) RemoteViewsProto.TextViewDrawableAction.IS_RELATIVE:
+                        values.put(RemoteViewsProto.TextViewDrawableAction.IS_RELATIVE,
+                                in.readBoolean(
+                                        RemoteViewsProto.TextViewDrawableAction.IS_RELATIVE));
+                        break;
+                    case (int) RemoteViewsProto.TextViewDrawableAction.RESOURCES:
+                        final long resourcesToken = in.start(
+                                RemoteViewsProto.TextViewDrawableAction.RESOURCES);
+                        while (in.nextField() != NO_MORE_FIELDS) {
+                            switch (in.getFieldNumber()) {
+                                case (int) RemoteViewsProto.TextViewDrawableAction.Resources.ONE:
+                                    ((SparseArray<String>) values.get(
+                                            RemoteViewsProto.TextViewDrawableAction.RESOURCES)).put(
+                                            1, in.readString(
+                                                    RemoteViewsProto
+                                                            .TextViewDrawableAction.Resources.ONE));
+                                    break;
+                                case (int) RemoteViewsProto.TextViewDrawableAction.Resources.TWO:
+                                    ((SparseArray<String>) values.get(
+                                            RemoteViewsProto.TextViewDrawableAction.RESOURCES)).put(
+                                            2, in.readString(
+                                                    RemoteViewsProto
+                                                            .TextViewDrawableAction.Resources.TWO));
+                                    break;
+                                case (int) RemoteViewsProto.TextViewDrawableAction.Resources.THREE:
+                                    ((SparseArray<String>) values.get(
+                                            RemoteViewsProto.TextViewDrawableAction.RESOURCES)).put(
+                                            3, in.readString(
+                                                    RemoteViewsProto
+                                                            .TextViewDrawableAction
+                                                            .Resources.THREE));
+                                    break;
+                                case (int) RemoteViewsProto.TextViewDrawableAction.Resources.FOUR:
+                                    ((SparseArray<String>) values.get(
+                                            RemoteViewsProto.TextViewDrawableAction.RESOURCES)).put(
+                                            4, in.readString(
+                                                    RemoteViewsProto
+                                                            .TextViewDrawableAction
+                                                            .Resources.FOUR));
+                                    break;
+                                default:
+                                    Log.w(LOG_TAG,
+                                            "Unhandled field while reading RemoteViews proto!\n"
+                                                    + ProtoUtils.currentFieldToString(in));
+                            }
+                        }
+                        in.end(resourcesToken);
+                        break;
+                    case (int) RemoteViewsProto.TextViewDrawableAction.ICONS:
+                        final long iconsToken = in.start(
+                                RemoteViewsProto.TextViewDrawableAction.ICONS);
+                        while (in.nextField() != NO_MORE_FIELDS) {
+                            switch (in.getFieldNumber()) {
+                                case (int) RemoteViewsProto.TextViewDrawableAction.Icons.ONE:
+                                    ((SparseArray<PendingResources<Icon>>) values.get(
+                                            RemoteViewsProto.TextViewDrawableAction.ICONS)).put(1,
+                                            createIconFromProto(in,
+                                                    RemoteViewsProto
+                                                            .TextViewDrawableAction.Icons.ONE));
+                                    break;
+                                case (int) RemoteViewsProto.TextViewDrawableAction.Icons.TWO:
+                                    ((SparseArray<PendingResources<Icon>>) values.get(
+                                            RemoteViewsProto.TextViewDrawableAction.ICONS)).put(2,
+                                            createIconFromProto(in,
+                                                    RemoteViewsProto
+                                                            .TextViewDrawableAction.Icons.TWO));
+                                    break;
+                                case (int) RemoteViewsProto.TextViewDrawableAction.Icons.THREE:
+                                    ((SparseArray<PendingResources<Icon>>) values.get(
+                                            RemoteViewsProto.TextViewDrawableAction.ICONS)).put(3,
+                                            createIconFromProto(in,
+                                                    RemoteViewsProto
+                                                            .TextViewDrawableAction.Icons.THREE));
+                                    break;
+                                case (int) RemoteViewsProto.TextViewDrawableAction.Icons.FOUR:
+                                    ((SparseArray<PendingResources<Icon>>) values.get(
+                                            RemoteViewsProto.TextViewDrawableAction.ICONS)).put(4,
+                                            createIconFromProto(in,
+                                                    RemoteViewsProto
+                                                            .TextViewDrawableAction.Icons.FOUR));
+                                    break;
+                                default:
+                                    Log.w(LOG_TAG,
+                                            "Unhandled field while reading RemoteViews proto!\n"
+                                                    + ProtoUtils.currentFieldToString(in));
+                            }
+                        }
+                        in.end(iconsToken);
+                        break;
+                    default:
+                        Log.w(LOG_TAG, "Unhandled field while reading RemoteViews proto!\n"
+                                + ProtoUtils.currentFieldToString(in));
+                }
+            }
+            in.end(token);
+
+            checkContainsKeys(values, new long[]{RemoteViewsProto.TextViewDrawableAction.VIEW_ID});
+
+            return (context, resources, rootData, depth) -> {
+                int viewId = getAsIdentifier(resources, values,
+                        RemoteViewsProto.TextViewDrawableAction.VIEW_ID);
+                SparseArray<PendingResources<Icon>> icons =
+                        (SparseArray<PendingResources<Icon>>) values.get(
+                                RemoteViewsProto.TextViewDrawableAction.ICONS);
+                SparseArray<String> resArray = (SparseArray<String>) values.get(
+                        RemoteViewsProto.TextViewDrawableAction.RESOURCES);
+                boolean isRelative = (boolean) values.get(
+                        RemoteViewsProto.TextViewDrawableAction.IS_RELATIVE, false);
+                if (icons.size() > 0) {
+                    return new TextViewDrawableAction(viewId, isRelative,
+                            icons.get(1).create(context, resources, rootData, depth),
+                            icons.get(2).create(context, resources, rootData, depth),
+                            icons.get(3).create(context, resources, rootData, depth),
+                            icons.get(4).create(context, resources, rootData, depth));
+                } else {
+                    int first = resArray.contains(1) ? getAsIdentifier(resources, resArray, 1) : 0;
+                    int second = resArray.contains(2) ? getAsIdentifier(resources, resArray, 2) : 0;
+                    int third = resArray.contains(3) ? getAsIdentifier(resources, resArray, 3) : 0;
+                    int fourth = resArray.contains(4) ? getAsIdentifier(resources, resArray, 4) : 0;
+                    return new TextViewDrawableAction(viewId, isRelative, first, second, third,
+                            fourth);
+                }
+            };
+        }
     }
 
     /**
@@ -4535,6 +4849,58 @@
         public int getActionTag() {
             return TEXT_VIEW_SIZE_ACTION_TAG;
         }
+
+        @Override
+        public boolean canWriteToProto() {
+            return true;
+        }
+
+        @Override
+        public void writeToProto(ProtoOutputStream out, Context context, Resources appResources) {
+            final long token = out.start(RemoteViewsProto.Action.TEXT_VIEW_SIZE_ACTION);
+            out.write(RemoteViewsProto.TextViewSizeAction.VIEW_ID,
+                    appResources.getResourceName(mViewId));
+            out.write(RemoteViewsProto.TextViewSizeAction.UNITS, mUnits);
+            out.write(RemoteViewsProto.TextViewSizeAction.SIZE, mSize);
+            out.end(token);
+        }
+
+        public static PendingResources<Action> createFromProto(ProtoInputStream in)
+                throws Exception {
+            final LongSparseArray<Object> values = new LongSparseArray<>();
+
+            final long token = in.start(RemoteViewsProto.Action.TEXT_VIEW_SIZE_ACTION);
+            while (in.nextField() != NO_MORE_FIELDS) {
+                switch (in.getFieldNumber()) {
+                    case (int) RemoteViewsProto.TextViewSizeAction.VIEW_ID:
+                        values.put(RemoteViewsProto.TextViewSizeAction.VIEW_ID,
+                                in.readString(RemoteViewsProto.TextViewSizeAction.VIEW_ID));
+                        break;
+                    case (int) RemoteViewsProto.TextViewSizeAction.UNITS:
+                        values.put(RemoteViewsProto.TextViewSizeAction.UNITS,
+                                in.readInt(RemoteViewsProto.TextViewSizeAction.UNITS));
+                        break;
+                    case (int) RemoteViewsProto.TextViewSizeAction.SIZE:
+                        values.put(RemoteViewsProto.TextViewSizeAction.SIZE,
+                                in.readFloat(RemoteViewsProto.TextViewSizeAction.SIZE));
+                        break;
+                    default:
+                        Log.w(LOG_TAG, "Unhandled field while reading RemoteViews proto!\n"
+                                + ProtoUtils.currentFieldToString(in));
+                }
+            }
+            in.end(token);
+
+            checkContainsKeys(values, new long[]{RemoteViewsProto.TextViewSizeAction.VIEW_ID});
+
+            return (context, resources, rootData, depth) -> {
+                int viewId = getAsIdentifier(resources, values,
+                        RemoteViewsProto.TextViewSizeAction.VIEW_ID);
+                return new TextViewSizeAction(viewId,
+                        (int) values.get(RemoteViewsProto.TextViewSizeAction.UNITS, 0),
+                        (float) values.get(RemoteViewsProto.TextViewSizeAction.SIZE, 0));
+            };
+        }
     }
 
     /**
@@ -4579,6 +4945,70 @@
         public int getActionTag() {
             return VIEW_PADDING_ACTION_TAG;
         }
+
+        @Override
+        public boolean canWriteToProto() {
+            return true;
+        }
+
+        @Override
+        public void writeToProto(ProtoOutputStream out, Context context, Resources appResources) {
+            final long token = out.start(RemoteViewsProto.Action.VIEW_PADDING_ACTION);
+            out.write(RemoteViewsProto.ViewPaddingAction.VIEW_ID,
+                    appResources.getResourceName(mViewId));
+            out.write(RemoteViewsProto.ViewPaddingAction.LEFT, mLeft);
+            out.write(RemoteViewsProto.ViewPaddingAction.RIGHT, mRight);
+            out.write(RemoteViewsProto.ViewPaddingAction.TOP, mTop);
+            out.write(RemoteViewsProto.ViewPaddingAction.BOTTOM, mBottom);
+            out.end(token);
+        }
+
+        public static PendingResources<Action> createFromProto(ProtoInputStream in)
+                throws Exception {
+            final LongSparseArray<Object> values = new LongSparseArray<>();
+
+            final long token = in.start(RemoteViewsProto.Action.VIEW_PADDING_ACTION);
+            while (in.nextField() != NO_MORE_FIELDS) {
+                switch (in.getFieldNumber()) {
+                    case (int) RemoteViewsProto.ViewPaddingAction.VIEW_ID:
+                        values.put(RemoteViewsProto.ViewPaddingAction.VIEW_ID,
+                                in.readString(RemoteViewsProto.ViewPaddingAction.VIEW_ID));
+                        break;
+                    case (int) RemoteViewsProto.ViewPaddingAction.LEFT:
+                        values.put(RemoteViewsProto.ViewPaddingAction.LEFT,
+                                in.readInt(RemoteViewsProto.ViewPaddingAction.LEFT));
+                        break;
+                    case (int) RemoteViewsProto.ViewPaddingAction.RIGHT:
+                        values.put(RemoteViewsProto.ViewPaddingAction.RIGHT,
+                                in.readInt(RemoteViewsProto.ViewPaddingAction.RIGHT));
+                        break;
+                    case (int) RemoteViewsProto.ViewPaddingAction.TOP:
+                        values.put(RemoteViewsProto.ViewPaddingAction.TOP,
+                                in.readInt(RemoteViewsProto.ViewPaddingAction.TOP));
+                        break;
+                    case (int) RemoteViewsProto.ViewPaddingAction.BOTTOM:
+                        values.put(RemoteViewsProto.ViewPaddingAction.BOTTOM,
+                                in.readInt(RemoteViewsProto.ViewPaddingAction.BOTTOM));
+                        break;
+                    default:
+                        Log.w(LOG_TAG, "Unhandled field while reading RemoteViews proto!\n"
+                                + ProtoUtils.currentFieldToString(in));
+                }
+            }
+            in.end(token);
+
+            checkContainsKeys(values, new long[]{RemoteViewsProto.ViewPaddingAction.VIEW_ID});
+
+            return (context, resources, rootData, depth) -> {
+                int viewId = getAsIdentifier(resources, values,
+                        RemoteViewsProto.ViewPaddingAction.VIEW_ID);
+                return new ViewPaddingAction(viewId,
+                        (int) values.get(RemoteViewsProto.ViewPaddingAction.LEFT, 0),
+                        (int) values.get(RemoteViewsProto.ViewPaddingAction.TOP, 0),
+                        (int) values.get(RemoteViewsProto.ViewPaddingAction.RIGHT, 0),
+                        (int) values.get(RemoteViewsProto.ViewPaddingAction.BOTTOM, 0));
+            };
+        }
     }
 
     /**
@@ -5241,6 +5671,69 @@
         public int getActionTag() {
             return SET_VIEW_OUTLINE_RADIUS_TAG;
         }
+
+        @Override
+        public boolean canWriteToProto() {
+            return true;
+        }
+
+        @Override
+        public void writeToProto(ProtoOutputStream out, Context context, Resources appResources) {
+            final long token = out.start(
+                    RemoteViewsProto.Action.SET_VIEW_OUTLINE_PREFERRED_RADIUS_ACTION);
+            out.write(RemoteViewsProto.SetViewOutlinePreferredRadiusAction.VIEW_ID,
+                    appResources.getResourceName(mViewId));
+            out.write(RemoteViewsProto.SetViewOutlinePreferredRadiusAction.VALUE_TYPE, mValueType);
+            out.write(RemoteViewsProto.SetViewOutlinePreferredRadiusAction.VALUE, mValue);
+            out.end(token);
+        }
+
+        public static PendingResources<Action> createFromProto(ProtoInputStream in)
+                throws Exception {
+            final LongSparseArray<Object> values = new LongSparseArray<>();
+
+            final long token = in.start(
+                    RemoteViewsProto.Action.SET_VIEW_OUTLINE_PREFERRED_RADIUS_ACTION);
+            while (in.nextField() != NO_MORE_FIELDS) {
+                switch (in.getFieldNumber()) {
+                    case (int) RemoteViewsProto.SetViewOutlinePreferredRadiusAction.VIEW_ID:
+                        values.put(RemoteViewsProto.SetViewOutlinePreferredRadiusAction.VIEW_ID,
+                                in.readString(
+                                        RemoteViewsProto
+                                                .SetViewOutlinePreferredRadiusAction.VIEW_ID));
+                        break;
+                    case (int) RemoteViewsProto.SetViewOutlinePreferredRadiusAction.VALUE_TYPE:
+                        values.put(RemoteViewsProto.SetViewOutlinePreferredRadiusAction.VALUE_TYPE,
+                                in.readInt(
+                                        RemoteViewsProto
+                                                .SetViewOutlinePreferredRadiusAction.VALUE_TYPE));
+                        break;
+                    case (int) RemoteViewsProto.SetViewOutlinePreferredRadiusAction.VALUE:
+                        values.put(RemoteViewsProto.SetViewOutlinePreferredRadiusAction.VALUE,
+                                in.readInt(
+                                        RemoteViewsProto
+                                                .SetViewOutlinePreferredRadiusAction.VALUE));
+                        break;
+                    default:
+                        Log.w(LOG_TAG, "Unhandled field while reading RemoteViews proto!\n"
+                                + ProtoUtils.currentFieldToString(in));
+                }
+            }
+            in.end(token);
+
+            checkContainsKeys(values,
+                    new long[]{RemoteViewsProto.SetViewOutlinePreferredRadiusAction.VIEW_ID,
+                            RemoteViewsProto.SetViewOutlinePreferredRadiusAction.VALUE_TYPE});
+
+            return (context, resources, rootData, depth) -> {
+                int viewId = getAsIdentifier(resources, values,
+                        RemoteViewsProto.SetViewOutlinePreferredRadiusAction.VIEW_ID);
+                return new SetViewOutlinePreferredRadiusAction(viewId,
+                        (int) values.get(RemoteViewsProto.SetViewOutlinePreferredRadiusAction.VALUE,
+                                0), (int) values.get(
+                        RemoteViewsProto.SetViewOutlinePreferredRadiusAction.VALUE_TYPE));
+            };
+        }
     }
 
     /**
@@ -5324,6 +5817,46 @@
         public int getActionTag() {
             return SET_DRAW_INSTRUCTION_TAG;
         }
+
+        @Override
+        public boolean canWriteToProto() {
+            return drawDataParcel();
+        }
+
+        @Override
+        public void writeToProto(ProtoOutputStream out, Context context, Resources appResources) {
+            if (!drawDataParcel()) return;
+            final long token = out.start(RemoteViewsProto.Action.SET_DRAW_INSTRUCTION_ACTION);
+            if (mInstructions != null) {
+                for (byte[] bytes : mInstructions.mInstructions) {
+                    out.write(RemoteViewsProto.SetDrawInstructionAction.INSTRUCTIONS, bytes);
+                }
+            }
+            out.end(token);
+        }
+    }
+
+    @FlaggedApi(FLAG_DRAW_DATA_PARCEL)
+    private PendingResources<Action> createSetDrawInstructionActionFromProto(ProtoInputStream in)
+            throws Exception {
+        List<byte[]> instructions = new ArrayList<byte[]>();
+
+        final long token = in.start(RemoteViewsProto.Action.SET_DRAW_INSTRUCTION_ACTION);
+        while (in.nextField() != NO_MORE_FIELDS) {
+            switch (in.getFieldNumber()) {
+                case (int) RemoteViewsProto.SetDrawInstructionAction.INSTRUCTIONS:
+                    instructions.add(
+                            in.readBytes(RemoteViewsProto.SetDrawInstructionAction.INSTRUCTIONS));
+                    break;
+                default:
+                    Log.w(LOG_TAG, "Unhandled field while reading RemoteViews proto!\n"
+                            + ProtoUtils.currentFieldToString(in));
+            }
+        }
+        in.end(token);
+
+        return (context, resources, rootData, depth) -> new SetDrawInstructionAction(
+                new DrawInstructions.Builder(instructions).build());
     }
 
     /**
@@ -9604,12 +10137,20 @@
             }
             if (ref.mMode == MODE_NORMAL) {
                 rv.setIdealSize(ref.mIdealSize);
+                boolean hasDrawInstructionAction = false;
                 for (PendingResources<Action> pendingAction : ref.mActions) {
                     Action action = pendingAction.create(appContext, appResources, rootData, depth);
                     if (action != null) {
+                        if (action instanceof SetDrawInstructionAction) {
+                            hasDrawInstructionAction = true;
+                        }
                         rv.addAction(action);
                     }
                 }
+                if (rv.mHasDrawInstructions && !hasDrawInstructionAction) {
+                    throw new InvalidProtoException(
+                            "RemoteViews proto is missing DrawInstructions");
+                }
                 return rv;
             } else if (ref.mMode == MODE_HAS_SIZED_REMOTEVIEWS) {
                 List<RemoteViews> sizedViews = new ArrayList<>();
@@ -9685,6 +10226,23 @@
                 return rv.createSetRemoteCollectionItemListAdapterActionFromProto(in);
             case (int) RemoteViewsProto.Action.SET_RIPPLE_DRAWABLE_COLOR_ACTION:
                 return SetRippleDrawableColor.createFromProto(in);
+            case (int) RemoteViewsProto.Action.SET_VIEW_OUTLINE_PREFERRED_RADIUS_ACTION:
+                return SetViewOutlinePreferredRadiusAction.createFromProto(in);
+            case (int) RemoteViewsProto.Action.TEXT_VIEW_DRAWABLE_ACTION:
+                return TextViewDrawableAction.createFromProto(in);
+            case (int) RemoteViewsProto.Action.TEXT_VIEW_SIZE_ACTION:
+                return TextViewSizeAction.createFromProto(in);
+            case (int) RemoteViewsProto.Action.VIEW_GROUP_ADD_ACTION:
+                return rv.createViewGroupActionAddFromProto(in);
+            case (int) RemoteViewsProto.Action.VIEW_GROUP_REMOVE_ACTION:
+                return ViewGroupActionRemove.createFromProto(in);
+            case (int) RemoteViewsProto.Action.VIEW_PADDING_ACTION:
+                return ViewPaddingAction.createFromProto(in);
+            case (int) RemoteViewsProto.Action.SET_DRAW_INSTRUCTION_ACTION:
+                if (!drawDataParcel()) {
+                    return null;
+                }
+                return rv.createSetDrawInstructionActionFromProto(in);
             default:
                 throw new RuntimeException("Unhandled field while reading Action proto!\n"
                         + ProtoUtils.currentFieldToString(in));
diff --git a/core/proto/android/widget/remoteviews.proto b/core/proto/android/widget/remoteviews.proto
index f477d32..6a987a4 100644
--- a/core/proto/android/widget/remoteviews.proto
+++ b/core/proto/android/widget/remoteviews.proto
@@ -32,6 +32,8 @@
  *
  * Do not change the tag number or type of any fields in order to maintain compatibility with
  * previous versions. If a field is deleted, use `reserved` to mark its tag number.
+ *
+ * Next tag: 17
  */
 message RemoteViewsProto {
     option (android.msg_privacy).dest = DEST_AUTOMATIC;
@@ -290,6 +292,7 @@
         }
     }
 
+    // Next tag: 23
     message Action {
         oneof action {
             AttributeReflectionAction attribute_reflection_action = 1;
@@ -307,6 +310,13 @@
             SetRadioGroupCheckedAction set_radio_group_checked_action = 13;
             SetRemoteCollectionItemListAdapterAction set_remote_collection_item_list_adapter_action = 14;
             SetRippleDrawableColorAction set_ripple_drawable_color_action = 15;
+            SetViewOutlinePreferredRadiusAction set_view_outline_preferred_radius_action = 16;
+            TextViewDrawableAction text_view_drawable_action = 17;
+            TextViewSizeAction text_view_size_action = 18;
+            ViewGroupAddAction view_group_add_action = 19;
+            ViewGroupRemoveAction view_group_remove_action = 20;
+            ViewPaddingAction view_padding_action = 21;
+            SetDrawInstructionAction set_draw_instruction_action = 22;
         }
     }
 
@@ -428,6 +438,65 @@
         optional string view_id = 1;
         optional android.content.res.ColorStateListProto color_state_list = 2;
     }
+
+    message SetViewOutlinePreferredRadiusAction {
+        optional string view_id = 1;
+        optional int32 value_type = 2;
+        optional int32 value = 3;
+    }
+
+    message TextViewDrawableAction {
+        optional string view_id = 1;
+        optional bool is_relative = 2;
+        oneof drawables {
+            Resources resources = 3;
+            Icons icons = 4;
+        };
+
+        message Resources {
+            optional string one = 1;
+            optional string two = 2;
+            optional string three = 3;
+            optional string four = 4;
+        }
+
+        message Icons {
+            optional Icon one = 1;
+            optional Icon two = 2;
+            optional Icon three = 3;
+            optional Icon four = 4;
+        }
+    }
+
+    message TextViewSizeAction {
+        optional string view_id = 1;
+        optional int32 units = 2;
+        optional float size = 3;
+    }
+
+    message ViewGroupAddAction {
+        optional string view_id = 1;
+        optional RemoteViewsProto nested_views = 2;
+        optional int32 index = 3;
+        optional int32 stableId = 4;
+    }
+
+    message ViewGroupRemoveAction {
+        optional string view_id = 1;
+        optional string view_id_to_keep = 2;
+    }
+
+    message ViewPaddingAction {
+        optional string view_id = 1;
+        optional int32 left = 2;
+        optional int32 right = 3;
+        optional int32 top = 4;
+        optional int32 bottom = 5;
+    }
+
+    message SetDrawInstructionAction {
+        repeated bytes instructions = 1;
+    }
 }
 
 
diff --git a/libs/hwui/pipeline/skia/BackdropFilterDrawable.cpp b/libs/hwui/pipeline/skia/BackdropFilterDrawable.cpp
index e81cbfb..c0ef4b14 100644
--- a/libs/hwui/pipeline/skia/BackdropFilterDrawable.cpp
+++ b/libs/hwui/pipeline/skia/BackdropFilterDrawable.cpp
@@ -29,37 +29,6 @@
 namespace uirenderer {
 namespace skiapipeline {
 
-BackdropFilterDrawable::~BackdropFilterDrawable() {}
-
-bool BackdropFilterDrawable::prepareToDraw(SkCanvas* canvas, const RenderProperties& properties,
-                                           int backdropImageWidth, int backdropImageHeight) {
-    // the drawing bounds for blurred content.
-    mDstBounds.setWH(properties.getWidth(), properties.getHeight());
-
-    float alphaMultiplier = 1.0f;
-    RenderNodeDrawable::setViewProperties(properties, canvas, &alphaMultiplier, true);
-
-    // get proper subset for previous content.
-    canvas->getTotalMatrix().mapRect(&mImageSubset, mDstBounds);
-    SkRect imageSubset(mImageSubset);
-    // ensure the subset is inside bounds of previous content.
-    if (!mImageSubset.intersect(SkRect::MakeWH(backdropImageWidth, backdropImageHeight))) {
-        return false;
-    }
-
-    // correct the drawing bounds if subset was changed.
-    if (mImageSubset != imageSubset) {
-        SkMatrix inverse;
-        if (canvas->getTotalMatrix().invert(&inverse)) {
-            inverse.mapRect(&mDstBounds, mImageSubset);
-        }
-    }
-
-    // follow the alpha from the target RenderNode.
-    mPaint.setAlpha(properties.layerProperties().alpha() * alphaMultiplier);
-    return true;
-}
-
 void BackdropFilterDrawable::onDraw(SkCanvas* canvas) {
     const RenderProperties& properties = mTargetRenderNode->properties();
     auto* backdropFilter = properties.layerProperties().getBackdropImageFilter();
@@ -68,27 +37,43 @@
         return;
     }
 
-    auto backdropImage = surface->makeImageSnapshot();
-    // sync necessary properties from target RenderNode.
-    if (!prepareToDraw(canvas, properties, backdropImage->width(), backdropImage->height())) {
+    SkRect srcBounds = SkRect::MakeWH(properties.getWidth(), properties.getHeight());
+
+    float alphaMultiplier = 1.0f;
+    RenderNodeDrawable::setViewProperties(properties, canvas, &alphaMultiplier, true);
+    SkPaint paint;
+    paint.setAlpha(properties.layerProperties().alpha() * alphaMultiplier);
+
+    SkRect surfaceSubset;
+    canvas->getTotalMatrix().mapRect(&surfaceSubset, srcBounds);
+    if (!surfaceSubset.intersect(SkRect::MakeWH(surface->width(), surface->height()))) {
         return;
     }
 
-    auto imageSubset = mImageSubset.roundOut();
+    auto backdropImage = surface->makeImageSnapshot(surfaceSubset.roundOut());
+
+    SkIRect imageBounds = SkIRect::MakeWH(backdropImage->width(), backdropImage->height());
+    SkIPoint offset;
+    SkIRect imageSubset;
+
 #ifdef __ANDROID__
     if (canvas->recordingContext()) {
         backdropImage =
                 SkImages::MakeWithFilter(canvas->recordingContext(), backdropImage, backdropFilter,
-                                         imageSubset, imageSubset, &mOutSubset, &mOutOffset);
+                                         imageBounds, imageBounds, &imageSubset, &offset);
     } else
 #endif
     {
-        backdropImage = SkImages::MakeWithFilter(backdropImage, backdropFilter, imageSubset,
-                                                 imageSubset, &mOutSubset, &mOutOffset);
+        backdropImage = SkImages::MakeWithFilter(backdropImage, backdropFilter, imageBounds,
+                                                 imageBounds, &imageSubset, &offset);
     }
-    canvas->drawImageRect(backdropImage, SkRect::Make(mOutSubset), mDstBounds,
-                          SkSamplingOptions(SkFilterMode::kLinear), &mPaint,
-                          SkCanvas::kStrict_SrcRectConstraint);
+
+    canvas->save();
+    canvas->resetMatrix();
+    canvas->drawImageRect(backdropImage, SkRect::Make(imageSubset), surfaceSubset,
+                          SkSamplingOptions(SkFilterMode::kLinear), &paint,
+                          SkCanvas::kFast_SrcRectConstraint);
+    canvas->restore();
 }
 
 }  // namespace skiapipeline
diff --git a/libs/hwui/pipeline/skia/BackdropFilterDrawable.h b/libs/hwui/pipeline/skia/BackdropFilterDrawable.h
index 9e35837..5e216a1 100644
--- a/libs/hwui/pipeline/skia/BackdropFilterDrawable.h
+++ b/libs/hwui/pipeline/skia/BackdropFilterDrawable.h
@@ -37,23 +37,10 @@
     BackdropFilterDrawable(RenderNode* renderNode, SkCanvas* canvas)
             : mTargetRenderNode(renderNode), mBounds(canvas->getLocalClipBounds()) {}
 
-    ~BackdropFilterDrawable();
+    ~BackdropFilterDrawable() = default;
 
 private:
     RenderNode* mTargetRenderNode;
-    SkPaint mPaint;
-
-    SkRect mDstBounds;
-    SkRect mImageSubset;
-    SkIRect mOutSubset;
-    SkIPoint mOutOffset;
-
-    /**
-     * Check all necessary properties before actual drawing.
-     * Return true if ready to draw.
-     */
-    bool prepareToDraw(SkCanvas* canvas, const RenderProperties& properties, int backdropImageWidth,
-                       int backdropImageHeight);
 
 protected:
     void onDraw(SkCanvas* canvas) override;
diff --git a/libs/hwui/tests/common/scenes/BackdropBlur.cpp b/libs/hwui/tests/common/scenes/BackdropBlur.cpp
new file mode 100644
index 0000000..a1133ff
--- /dev/null
+++ b/libs/hwui/tests/common/scenes/BackdropBlur.cpp
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+
+#include <SkBlendMode.h>
+
+#include "SkImageFilter.h"
+#include "SkImageFilters.h"
+#include "TestSceneBase.h"
+#include "utils/Blur.h"
+
+class BackdropBlurAnimation : public TestScene {
+private:
+    std::unique_ptr<TestScene> listView;
+
+public:
+    explicit BackdropBlurAnimation(const TestScene::Options& opts) {
+        listView.reset(TestScene::testMap()["listview"].createScene(opts));
+    }
+
+    void createContent(int width, int height, Canvas& canvas) override {
+        sp<RenderNode> list = TestUtils::createNode(
+                0, 0, width, height,
+                [this, width, height](RenderProperties& props, Canvas& canvas) {
+                    props.setClipToBounds(false);
+                    listView->createContent(width, height, canvas);
+                });
+
+        canvas.drawRenderNode(list.get());
+
+        int x = width / 8;
+        int y = height / 4;
+        sp<RenderNode> blurNode = TestUtils::createNode(
+                x, y, width - x, height - y, [](RenderProperties& props, Canvas& canvas) {
+                    props.mutableOutline().setRoundRect(0, 0, props.getWidth(), props.getHeight(),
+                                                        dp(16), 1);
+                    props.mutableOutline().setShouldClip(true);
+                    sk_sp<SkImageFilter> blurFilter = SkImageFilters::Blur(
+                            Blur::convertRadiusToSigma(dp(8)), Blur::convertRadiusToSigma(dp(8)),
+                            SkTileMode::kClamp, nullptr, nullptr);
+                    props.mutateLayerProperties().setBackdropImageFilter(blurFilter.get());
+                    canvas.drawColor(0x33000000, SkBlendMode::kSrcOver);
+                });
+
+        canvas.drawRenderNode(blurNode.get());
+    }
+
+    void doFrame(int frameNr) override { listView->doFrame(frameNr); }
+};
+
+static TestScene::Registrar _BackdropBlur(TestScene::Info{
+        "backdropblur", "A rounded rect that does a blur-behind of a sky animation.",
+        [](const TestScene::Options& opts) -> test::TestScene* {
+            return new BackdropBlurAnimation(opts);
+        }});
diff --git a/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp b/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp
index ca54087..4b29100 100644
--- a/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp
+++ b/libs/hwui/tests/unit/RenderNodeDrawableTests.cpp
@@ -1280,7 +1280,7 @@
     canvas->drawDrawable(&backdropDrawable);
     // the drawable is still visible, ok to draw.
     EXPECT_EQ(2, canvas->mDrawCounter);
-    EXPECT_EQ(SkRect::MakeLTRB(0, 0, CANVAS_WIDTH - 30, CANVAS_HEIGHT - 30), canvas->mDstBounds);
+    EXPECT_EQ(SkRect::MakeLTRB(30, 30, CANVAS_WIDTH, CANVAS_HEIGHT), canvas->mDstBounds);
 
     canvas->translate(CANVAS_WIDTH, CANVAS_HEIGHT);
     canvas->drawDrawable(&drawable);
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 26e6e71..a21a805 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -1473,3 +1473,11 @@
        purpose: PURPOSE_BUGFIX
    }
 }
+
+flag {
+   name: "magic_portrait_wallpapers"
+   namespace: "systemui"
+   description: "Magic Portrait related changes in systemui"
+   bug: "370863642"
+}
+
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/activity/EdgeToEdgeActivitContent.kt b/packages/SystemUI/compose/core/src/com/android/compose/activity/EdgeToEdgeActivitContent.kt
deleted file mode 100644
index 97c8076..0000000
--- a/packages/SystemUI/compose/core/src/com/android/compose/activity/EdgeToEdgeActivitContent.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.compose.activity
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.material3.LocalContentColor
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.contentColorFor
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import com.android.compose.rememberSystemUiController
-import com.android.compose.theme.PlatformTheme
-
-/** Scaffolding for an edge-to-edge activity content. */
-@Composable
-fun EdgeToEdgeActivityContent(
-    modifier: Modifier = Modifier,
-    content: @Composable () -> Unit,
-) {
-    // Make the status and navigation bars transparent, ensuring that the status bar icons are dark
-    // when the theme is light and vice-versa.
-    val systemUiController = rememberSystemUiController()
-    val isDarkTheme = isSystemInDarkTheme()
-    val useDarkIcons = !isDarkTheme
-    DisposableEffect(systemUiController, useDarkIcons) {
-        systemUiController.setSystemBarsColor(
-            color = Color.Transparent,
-            darkIcons = useDarkIcons,
-        )
-        onDispose {}
-    }
-
-    PlatformTheme(isDarkTheme) {
-        val backgroundColor = MaterialTheme.colorScheme.background
-        Box(modifier.fillMaxSize().background(backgroundColor)) {
-            CompositionLocalProvider(LocalContentColor provides contentColorFor(backgroundColor)) {
-                content()
-            }
-        }
-    }
-}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
index bf8f6ce..7dc2901 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
@@ -522,13 +522,7 @@
     val currentSceneKey =
         if (isSplitAroundTheFold) SceneKeys.SplitSceneKey else SceneKeys.ContiguousSceneKey
 
-    val state = remember {
-        MutableSceneTransitionLayoutState(
-            currentSceneKey,
-            SceneTransitions,
-            enableInterruptions = false,
-        )
-    }
+    val state = remember { MutableSceneTransitionLayoutState(currentSceneKey, SceneTransitions) }
 
     // Update state whenever currentSceneKey has changed.
     LaunchedEffect(state, currentSceneKey) {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
index 2e8fc14..2657d7c 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
@@ -234,7 +234,6 @@
     canHideOverlay: (OverlayKey) -> Boolean = { true },
     canReplaceOverlay: (from: OverlayKey, to: OverlayKey) -> Boolean = { _, _ -> true },
     stateLinks: List<StateLink> = emptyList(),
-    enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED,
 ): MutableSceneTransitionLayoutState {
     return MutableSceneTransitionLayoutStateImpl(
         initialScene,
@@ -245,7 +244,6 @@
         canHideOverlay,
         canReplaceOverlay,
         stateLinks,
-        enableInterruptions,
     )
 }
 
@@ -261,9 +259,6 @@
         true
     },
     private val stateLinks: List<StateLink> = emptyList(),
-
-    // TODO(b/290930950): Remove this flag.
-    internal val enableInterruptions: Boolean = DEFAULT_INTERRUPTIONS_ENABLED,
 ) : MutableSceneTransitionLayoutState {
     private val creationThread: Thread = Thread.currentThread()
 
@@ -406,13 +401,6 @@
             transition.updateOverscrollSpecs(fromSpec = null, toSpec = null)
         }
 
-        if (!enableInterruptions) {
-            // Set the current transition.
-            check(transitionStates.size == 1)
-            transitionStates = listOf(transition)
-            return
-        }
-
         when (val currentState = transitionStates.last()) {
             is TransitionState.Idle -> {
                 // Replace [Idle] by [transition].
@@ -755,9 +743,6 @@
 
 private const val TAG = "SceneTransitionLayoutState"
 
-/** Whether support for interruptions in enabled by default. */
-internal const val DEFAULT_INTERRUPTIONS_ENABLED = true
-
 /**
  * The max number of concurrent transitions. If the number of transitions goes past this number,
  * this probably means that there is a leak and we will Log.wtf before clearing the list of
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
index d6751ae..9d3f25e 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
@@ -337,10 +337,6 @@
         }
 
         internal open fun interruptionProgress(layoutImpl: SceneTransitionLayoutImpl): Float {
-            if (!layoutImpl.state.enableInterruptions) {
-                return 0f
-            }
-
             if (replacedTransition != null) {
                 return replacedTransition.interruptionProgress(layoutImpl)
             }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index 1eed54e..a2c2729 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -213,10 +213,6 @@
                         from(SceneA, to = SceneB) { spec = tween }
                         from(SceneB, to = SceneC) { spec = tween }
                     },
-
-                    // Disable interruptions so that the current transition is directly removed
-                    // when starting a new one.
-                    enableInterruptions = false,
                 )
             }
 
@@ -243,7 +239,12 @@
                 onElement(TestElements.Bar).assertExists()
 
                 // Start transition from SceneB to SceneC
-                rule.runOnUiThread { state.setTargetScene(SceneC, coroutineScope) }
+                rule.runOnUiThread {
+                    // We snap to scene B so that the transition A => B is removed from the list of
+                    // transitions.
+                    state.snapToScene(SceneB)
+                    state.setTargetScene(SceneC, coroutineScope)
+                }
             }
 
             at(3 * frameDuration) { onElement(TestElements.Bar).assertIsNotDisplayed() }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
index 400f0b3..b00b894b 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
@@ -85,15 +85,9 @@
 
     /** The content under test. */
     @Composable
-    private fun TestContent(enableInterruptions: Boolean = true) {
+    private fun TestContent() {
         coroutineScope = rememberCoroutineScope()
-        layoutState = remember {
-            MutableSceneTransitionLayoutState(
-                SceneA,
-                EmptyTestTransitions,
-                enableInterruptions = enableInterruptions,
-            )
-        }
+        layoutState = remember { MutableSceneTransitionLayoutState(SceneA, EmptyTestTransitions) }
 
         SceneTransitionLayout(state = layoutState, modifier = Modifier.size(LayoutSize)) {
             scene(SceneA, userActions = mapOf(Back to SceneB)) {
@@ -205,7 +199,7 @@
 
     @Test
     fun testSharedElement() {
-        rule.setContent { TestContent(enableInterruptions = false) }
+        rule.setContent { TestContent() }
 
         // In scene A, the shared element SharedFoo() is at the top end of the layout and has a size
         // of 50.dp.
@@ -253,6 +247,9 @@
             .isWithin(DpOffsetSubject.DefaultTolerance)
             .of(DpOffset(25.dp, 25.dp))
 
+        // Finish the transition.
+        rule.mainClock.advanceTimeBy(TestTransitionDuration / 2)
+
         // Animate to scene C, let the animation start then go to the middle of the transition.
         currentScene = SceneC
         rule.mainClock.advanceTimeByFrame()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt
index fe5024f..59676ce 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/FakeWallpaperRepository.kt
@@ -17,10 +17,17 @@
 package com.android.systemui.wallpapers.data.repository
 
 import android.app.WallpaperInfo
+import android.view.View
 import kotlinx.coroutines.flow.MutableStateFlow
 
 /** Fake implementation of the wallpaper repository. */
 class FakeWallpaperRepository : WallpaperRepository {
     override val wallpaperInfo = MutableStateFlow<WallpaperInfo?>(null)
     override val wallpaperSupportsAmbientMode = MutableStateFlow(false)
+    override var rootView: View? = null
+    private val _notificationStackAbsoluteBottom = MutableStateFlow(0F)
+
+    override fun setNotificationStackAbsoluteBottom(bottom: Float) {
+        _notificationStackAbsoluteBottom.value = bottom
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt
index ec52055..95d1b5d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt
@@ -68,11 +68,16 @@
 
     val previewClock: Flow<ClockController>
 
+    /** top of notifications without bcsmartspace in small clock settings */
+    val notificationDefaultTop: StateFlow<Float>
+
     val clockEventController: ClockEventController
 
     val shouldForceSmallClock: Boolean
 
     fun setClockSize(size: ClockSize)
+
+    fun setNotificationDefaultTop(top: Float)
 }
 
 @SysUISingleton
@@ -108,7 +113,7 @@
             .stateIn(
                 scope = applicationScope,
                 started = SharingStarted.WhileSubscribed(),
-                initialValue = getClockSize()
+                initialValue = getClockSize(),
             )
 
     override val currentClockId: Flow<ClockId> =
@@ -138,7 +143,7 @@
             .stateIn(
                 scope = applicationScope,
                 started = SharingStarted.WhileSubscribed(),
-                initialValue = clockRegistry.createCurrentClock()
+                initialValue = clockRegistry.createCurrentClock(),
             )
 
     override val previewClock: Flow<ClockController> =
@@ -149,6 +154,14 @@
             clockRegistry.createCurrentClock()
         }
 
+    private val _notificationDefaultTop: MutableStateFlow<Float> = MutableStateFlow(0F)
+
+    override val notificationDefaultTop: StateFlow<Float> = _notificationDefaultTop.asStateFlow()
+
+    override fun setNotificationDefaultTop(top: Float) {
+        _notificationDefaultTop.value = top
+    }
+
     override val shouldForceSmallClock: Boolean
         get() =
             featureFlags.isEnabled(Flags.LOCKSCREEN_ENABLE_LANDSCAPE) &&
@@ -160,7 +173,7 @@
             secureSettings.getIntForUser(
                 Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK,
                 /* defaultValue= */ 1,
-                UserHandle.USER_CURRENT
+                UserHandle.USER_CURRENT,
             )
         )
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
index 130242f..8210174 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -269,6 +269,9 @@
      */
     val isEncryptedOrLockdown: Flow<Boolean>
 
+    /** The top of shortcut in screen, used by wallpaper to find remaining space in lockscreen */
+    val shortcutAbsoluteTop: StateFlow<Float>
+
     /**
      * Returns `true` if the keyguard is showing; `false` otherwise.
      *
@@ -339,6 +342,8 @@
      * otherwise.
      */
     fun isShowKeyguardWhenReenabled(): Boolean
+
+    fun setShortcutAbsoluteTop(top: Float)
 }
 
 /** Encapsulates application state for the keyguard. */
@@ -503,7 +508,7 @@
                 trySendWithFailureLogging(
                     statusBarStateController.dozeAmount,
                     TAG,
-                    "initial dozeAmount"
+                    "initial dozeAmount",
                 )
 
                 awaitClose { statusBarStateController.removeCallback(callback) }
@@ -521,7 +526,7 @@
             object : DozeTransitionCallback {
                 override fun onDozeTransition(
                     oldState: DozeMachine.State,
-                    newState: DozeMachine.State
+                    newState: DozeMachine.State,
                 ) {
                     trySendWithFailureLogging(
                         DozeTransitionModel(
@@ -529,7 +534,7 @@
                             to = dozeMachineStateToModel(newState),
                         ),
                         TAG,
-                        "doze transition model"
+                        "doze transition model",
                     )
                 }
             }
@@ -541,7 +546,7 @@
                 to = dozeMachineStateToModel(dozeTransitionListener.newState),
             ),
             TAG,
-            "initial doze transition model"
+            "initial doze transition model",
         )
 
         awaitClose { dozeTransitionListener.removeCallback(callback) }
@@ -579,7 +584,7 @@
                             trySendWithFailureLogging(
                                 statusBarStateIntToObject(state),
                                 TAG,
-                                "state"
+                                "state",
                             )
                         }
                     }
@@ -590,7 +595,7 @@
             .stateIn(
                 scope,
                 SharingStarted.Eagerly,
-                statusBarStateIntToObject(statusBarStateController.state)
+                statusBarStateIntToObject(statusBarStateController.state),
             )
 
     private val _biometricUnlockState: MutableStateFlow<BiometricUnlockModel> =
@@ -610,7 +615,7 @@
             trySendWithFailureLogging(
                 authController.fingerprintSensorLocation,
                 TAG,
-                "AuthController.Callback#onFingerprintLocationChanged"
+                "AuthController.Callback#onFingerprintLocationChanged",
             )
         }
 
@@ -635,6 +640,9 @@
     private val _isActiveDreamLockscreenHosted = MutableStateFlow(false)
     override val isActiveDreamLockscreenHosted = _isActiveDreamLockscreenHosted.asStateFlow()
 
+    private val _shortcutAbsoluteTop = MutableStateFlow(0F)
+    override val shortcutAbsoluteTop = _shortcutAbsoluteTop.asStateFlow()
+
     init {
         val callback =
             object : KeyguardStateController.Callback {
@@ -721,6 +729,10 @@
         }
     }
 
+    override fun setShortcutAbsoluteTop(top: Float) {
+        _shortcutAbsoluteTop.value = top
+    }
+
     private fun dozeMachineStateToModel(state: DozeMachine.State): DozeStateModel {
         return when (state) {
             DozeMachine.State.UNINITIALIZED -> DozeStateModel.UNINITIALIZED
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt
index c0049d4..5b7eedd 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt
@@ -94,7 +94,7 @@
                 .stateIn(
                     scope = applicationScope,
                     started = SharingStarted.WhileSubscribed(),
-                    initialValue = ClockSize.LARGE
+                    initialValue = ClockSize.LARGE,
                 )
         } else {
             keyguardClockRepository.clockSize
@@ -152,4 +152,8 @@
             clock.largeClock.animations.fold(foldFraction)
         }
     }
+
+    fun setNotificationStackDefaultTop(top: Float) {
+        keyguardClockRepository.setNotificationDefaultTop(top)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index e6ee112..d7f96b5 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -526,6 +526,10 @@
         repository.showDismissibleKeyguard()
     }
 
+    fun setShortcutAbsoluteTop(top: Float) {
+        repository.setShortcutAbsoluteTop(top)
+    }
+
     companion object {
         private const val TAG = "KeyguardInteractor"
     }
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 a1c963b..0b10c8a 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
@@ -44,6 +44,7 @@
 import com.android.systemui.plugins.clocks.ClockController
 import com.android.systemui.plugins.clocks.ClockFaceLayout
 import com.android.systemui.res.R
+import com.android.systemui.shade.LargeScreenHeaderHelper
 import com.android.systemui.shared.R as sharedR
 import com.android.systemui.util.ui.value
 import dagger.Lazy
@@ -70,6 +71,7 @@
     val blueprintInteractor: Lazy<KeyguardBlueprintInteractor>,
     private val rootViewModel: KeyguardRootViewModel,
     private val aodBurnInViewModel: AodBurnInViewModel,
+    private val largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>,
 ) : KeyguardSection() {
     private var disposableHandle: DisposableHandle? = null
 
@@ -172,7 +174,7 @@
         }
     }
 
-    open fun applyDefaultConstraints(constraints: ConstraintSet) {
+    fun applyDefaultConstraints(constraints: ConstraintSet) {
         val guideline =
             if (keyguardClockViewModel.clockShouldBeCentered.value) PARENT_ID
             else R.id.split_shade_guideline
@@ -211,6 +213,28 @@
 
             // Explicitly clear pivot to force recalculate pivot instead of using legacy value
             setTransformPivot(R.id.lockscreen_clock_view_large, Float.NaN, Float.NaN)
+
+            val smallClockBottom =
+                keyguardClockViewModel.getSmallClockTopMargin() +
+                    context.resources.getDimensionPixelSize(
+                        com.android.systemui.customization.R.dimen.small_clock_height
+                    )
+            val dateWeatherSmartspaceHeight = getDimen(context, DATE_WEATHER_VIEW_HEIGHT).toFloat()
+            val marginBetweenSmartspaceAndNotification =
+                context.resources.getDimensionPixelSize(
+                    R.dimen.keyguard_status_view_bottom_margin
+                ) +
+                    if (context.resources.getBoolean(R.bool.config_use_large_screen_shade_header)) {
+                        largeScreenHeaderHelperLazy.get().getLargeScreenHeaderHeight()
+                    } else {
+                        0
+                    }
+
+            clockInteractor.setNotificationStackDefaultTop(
+                smallClockBottom +
+                    dateWeatherSmartspaceHeight +
+                    marginBetweenSmartspaceAndNotification
+            )
         }
 
         constrainWeatherClockDateIconsBarrier(constraints)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt
index 6c6e14c..d3895de 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder
 import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel
@@ -52,6 +53,7 @@
     private val indicationController: KeyguardIndicationController,
     private val keyguardBlueprintInteractor: Lazy<KeyguardBlueprintInteractor>,
     private val keyguardQuickAffordanceViewBinder: KeyguardQuickAffordanceViewBinder,
+    private val keyguardInteractor: KeyguardInteractor,
 ) : BaseShortcutSection() {
 
     // Amount to increase the bottom margin by to avoid colliding with inset
@@ -117,7 +119,7 @@
                 BOTTOM,
                 PARENT_ID,
                 BOTTOM,
-                verticalOffsetMargin + safeInsetBottom
+                verticalOffsetMargin + safeInsetBottom,
             )
 
             constrainWidth(R.id.end_button, width)
@@ -128,7 +130,7 @@
                 BOTTOM,
                 PARENT_ID,
                 BOTTOM,
-                verticalOffsetMargin + safeInsetBottom
+                verticalOffsetMargin + safeInsetBottom,
             )
 
             // The constraint set visibility for start and end button are default visible, set to
@@ -136,5 +138,13 @@
             setVisibilityMode(R.id.start_button, VISIBILITY_MODE_IGNORE)
             setVisibilityMode(R.id.end_button, VISIBILITY_MODE_IGNORE)
         }
+
+        val shortcutAbsoluteTopInScreen =
+            (resources.displayMetrics.heightPixels -
+                    (verticalOffsetMargin + safeInsetBottom) -
+                    height)
+                .toFloat()
+
+        keyguardInteractor.setShortcutAbsoluteTop(shortcutAbsoluteTopInScreen)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 2c6b09c..adb3352 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -3884,6 +3884,7 @@
             }
             showingLayout.dump(pw, args);
             dumpCustomOutline(pw, args);
+            dumpClipping(pw, args);
             if (getViewState() != null) {
                 getViewState().dump(pw, args);
                 pw.println();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
index afda426..ef6cad1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
@@ -856,17 +856,23 @@
                 pw.println();
             }
             if (DUMP_VERBOSE) {
-                pw.println("mInRemovalAnimation: " + mInRemovalAnimation);
-                pw.println("mClipTopAmount: " + mClipTopAmount);
-                pw.println("mClipBottomAmount " + mClipBottomAmount);
-                pw.println("mClipToActualHeight: " + mClipToActualHeight);
-                pw.println("mExtraWidthForClipping: " + mExtraWidthForClipping);
-                pw.println("mMinimumHeightForClipping: " + mMinimumHeightForClipping);
-                pw.println("getClipBounds(): " + getClipBounds());
+                dumpClipping(pw, args);
             }
         });
     }
 
+    protected void dumpClipping(IndentingPrintWriter pw, String[] args) {
+        pw.print("Clipping: ");
+        pw.print("mInRemovalAnimation", mInRemovalAnimation);
+        pw.print("mClipTopAmount", mClipTopAmount);
+        pw.print("mClipBottomAmount", mClipBottomAmount);
+        pw.print("mClipToActualHeight", mClipToActualHeight);
+        pw.print("mExtraWidthForClipping", mExtraWidthForClipping);
+        pw.print("mMinimumHeightForClipping", mMinimumHeightForClipping);
+        pw.print("getClipBounds()", getClipBounds());
+        pw.println();
+    }
+
     /**
      * return the amount that the content is translated
      */
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 8ae6b79..d828a67 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
@@ -130,6 +130,7 @@
 import com.android.systemui.util.ColorUtilKt;
 import com.android.systemui.util.DumpUtilsKt;
 import com.android.systemui.util.ListenerSet;
+import com.android.systemui.wallpapers.domain.interactor.WallpaperInteractor;
 
 import com.google.errorprone.annotations.CompileTimeConstant;
 
@@ -628,6 +629,9 @@
     @Nullable
     private OnClickListener mManageButtonClickListener;
 
+    @Nullable
+    private WallpaperInteractor mWallpaperInteractor;
+
     public NotificationStackScrollLayout(Context context, AttributeSet attrs) {
         super(context, attrs, 0, 0);
         Resources res = getResources();
@@ -1192,6 +1196,7 @@
         if (!SceneContainerFlag.isEnabled()) {
             setMaxLayoutHeight(getHeight());
             updateContentHeight();
+            mWallpaperInteractor.setNotificationStackAbsoluteBottom(mContentHeight);
         }
         clampScrollPosition();
         requestChildrenUpdate();
@@ -1253,6 +1258,7 @@
         if (mAmbientState.getStackTop() != stackTop) {
             mAmbientState.setStackTop(stackTop);
             onTopPaddingChanged(/* animate = */ isAddOrRemoveAnimationPending());
+            mWallpaperInteractor.setNotificationStackAbsoluteBottom((int) stackTop);
         }
     }
 
@@ -5898,6 +5904,10 @@
         mController.getNotificationRoundnessManager().setAnimatedChildren(mChildrenToAddAnimated);
     }
 
+    public void setWallpaperInteractor(WallpaperInteractor wallpaperInteractor) {
+        mWallpaperInteractor = wallpaperInteractor;
+    }
+
     void addSwipedOutView(View v) {
         mSwipedOutViews.add(v);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
index 7b02d0c..00c5c40 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java
@@ -145,6 +145,7 @@
 import com.android.systemui.tuner.TunerService;
 import com.android.systemui.util.Compile;
 import com.android.systemui.util.settings.SecureSettings;
+import com.android.systemui.wallpapers.domain.interactor.WallpaperInteractor;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
@@ -226,6 +227,8 @@
     private final SensitiveNotificationProtectionController
             mSensitiveNotificationProtectionController;
 
+    private final WallpaperInteractor mWallpaperInteractor;
+
     private View mLongPressedView;
 
     private final NotificationListContainerImpl mNotificationListContainer =
@@ -756,7 +759,8 @@
             NotificationDismissibilityProvider dismissibilityProvider,
             ActivityStarter activityStarter,
             SplitShadeStateController splitShadeStateController,
-            SensitiveNotificationProtectionController sensitiveNotificationProtectionController) {
+            SensitiveNotificationProtectionController sensitiveNotificationProtectionController,
+            WallpaperInteractor wallpaperInteractor) {
         mView = view;
         mKeyguardTransitionRepo = keyguardTransitionRepo;
         mViewBinder = viewBinder;
@@ -812,6 +816,7 @@
         mDismissibilityProvider = dismissibilityProvider;
         mActivityStarter = activityStarter;
         mSensitiveNotificationProtectionController = sensitiveNotificationProtectionController;
+        mWallpaperInteractor = wallpaperInteractor;
         mView.passSplitShadeStateController(splitShadeStateController);
         if (SceneContainerFlag.isEnabled()) {
             mWakeUpCoordinator.setStackScroller(this);
@@ -948,6 +953,8 @@
             collectFlow(mView, mKeyguardTransitionRepo.getTransitions(),
                     this::onKeyguardTransitionChanged);
         }
+
+        mView.setWallpaperInteractor(mWallpaperInteractor);
     }
 
     private boolean isInVisibleLocation(NotificationEntry entry) {
diff --git a/packages/SystemUI/src/com/android/systemui/util/WallpaperController.kt b/packages/SystemUI/src/com/android/systemui/util/WallpaperController.kt
index 65a0218..0744b5a 100644
--- a/packages/SystemUI/src/com/android/systemui/util/WallpaperController.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/WallpaperController.kt
@@ -32,19 +32,24 @@
  * Note: New logic should be added to [WallpaperRepository], not this class.
  */
 @SysUISingleton
-class WallpaperController @Inject constructor(
+class WallpaperController
+@Inject
+constructor(
     private val wallpaperManager: WallpaperManager,
     private val wallpaperRepository: WallpaperRepository,
 ) {
 
     var rootView: View? = null
+        set(value) {
+            field = value
+            wallpaperRepository.rootView = value
+        }
 
     private var notificationShadeZoomOut: Float = 0f
     private var unfoldTransitionZoomOut: Float = 0f
 
     private val shouldUseDefaultUnfoldTransition: Boolean
-        get() = wallpaperRepository.wallpaperInfo.value?.shouldUseDefaultUnfoldTransition()
-            ?: true
+        get() = wallpaperRepository.wallpaperInfo.value?.shouldUseDefaultUnfoldTransition() ?: true
 
     fun setNotificationShadeZoom(zoomOut: Float) {
         notificationShadeZoomOut = zoomOut
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt
index b45b8cd..54953c9 100644
--- a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/NoopWallpaperRepository.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.wallpapers.data.repository
 
 import android.app.WallpaperInfo
+import android.view.View
 import com.android.systemui.dagger.SysUISingleton
 import javax.inject.Inject
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -33,4 +34,7 @@
 class NoopWallpaperRepository @Inject constructor() : WallpaperRepository {
     override val wallpaperInfo: StateFlow<WallpaperInfo?> = MutableStateFlow(null).asStateFlow()
     override val wallpaperSupportsAmbientMode = MutableStateFlow(false).asStateFlow()
+    override var rootView: View? = null
+
+    override fun setNotificationStackAbsoluteBottom(bottom: Float) {}
 }
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt
index 041b6f9..203e1da 100644
--- a/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/data/repository/WallpaperRepository.kt
@@ -21,10 +21,16 @@
 import android.content.Context
 import android.content.Intent
 import android.content.IntentFilter
+import android.os.Bundle
 import android.os.UserHandle
+import android.view.View
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.Flags
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.keyguard.data.repository.KeyguardClockRepository
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
 import com.android.systemui.user.data.model.SelectedUserModel
 import com.android.systemui.user.data.model.SelectionStatus
 import com.android.systemui.user.data.repository.UserRepository
@@ -32,16 +38,19 @@
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
 /** A repository storing information about the current wallpaper. */
@@ -51,6 +60,15 @@
 
     /** Emits true if the current user's current wallpaper supports ambient mode. */
     val wallpaperSupportsAmbientMode: StateFlow<Boolean>
+
+    /** Set rootView to get its windowToken afterwards */
+    var rootView: View?
+
+    /**
+     * Set bottom of notifications from notification stack, and Magic Portrait will layout base on
+     * this value
+     */
+    fun setNotificationStackAbsoluteBottom(bottom: Float)
 }
 
 @SysUISingleton
@@ -61,6 +79,8 @@
     @Background private val bgDispatcher: CoroutineDispatcher,
     broadcastDispatcher: BroadcastDispatcher,
     userRepository: UserRepository,
+    keyguardRepository: KeyguardRepository,
+    keyguardClockRepository: KeyguardClockRepository,
     private val wallpaperManager: WallpaperManager,
     context: Context,
 ) : WallpaperRepository {
@@ -69,10 +89,7 @@
 
     private val wallpaperChanged: Flow<Unit> =
         broadcastDispatcher
-            .broadcastFlow(
-                IntentFilter(Intent.ACTION_WALLPAPER_CHANGED),
-                user = UserHandle.ALL,
-            )
+            .broadcastFlow(IntentFilter(Intent.ACTION_WALLPAPER_CHANGED), user = UserHandle.ALL)
             // The `combine` defining `wallpaperSupportsAmbientMode` will not run until both of the
             // input flows emit at least once. Since this flow is an input flow, it needs to emit
             // when it starts up to ensure that the `combine` will run if the user changes before we
@@ -87,6 +104,27 @@
             // Only update the wallpaper status once the user selection has finished.
             .filter { it.selectionStatus == SelectionStatus.SELECTION_COMPLETE }
 
+    /** The bottom of notification stack respect to the top of screen. */
+    private val notificationStackAbsoluteBottom: MutableStateFlow<Float> = MutableStateFlow(0F)
+
+    /** The top of shortcut respect to the top of screen. */
+    private val shortcutAbsoluteTop: StateFlow<Float> = keyguardRepository.shortcutAbsoluteTop
+
+    /**
+     * The top of notification stack to give a default state of lockscreen remaining space for
+     * states with notifications to compare with. It's the bottom of smartspace date and weather
+     * smartspace in small clock state, plus proper bottom margin.
+     */
+    private val notificationStackDefaultTop = keyguardClockRepository.notificationDefaultTop
+    @VisibleForTesting var sendLockscreenLayoutJob: Job? = null
+    private val lockscreenRemainingSpaceWithNotification: Flow<Triple<Float, Float, Float>> =
+        combine(
+            notificationStackAbsoluteBottom,
+            notificationStackDefaultTop,
+            shortcutAbsoluteTop,
+            ::Triple,
+        )
+
     override val wallpaperInfo: StateFlow<WallpaperInfo?> =
         if (!wallpaperManager.isWallpaperSupported || !deviceSupportsAodWallpaper) {
             MutableStateFlow(null).asStateFlow()
@@ -116,9 +154,70 @@
                 initialValue = wallpaperInfo.value?.supportsAmbientMode() == true,
             )
 
+    override var rootView: View? = null
+
+    val shouldSendNotificationLayout =
+        wallpaperInfo
+            .map {
+                val shouldSendNotificationLayout = shouldSendNotificationLayout(it)
+                if (shouldSendNotificationLayout) {
+                    sendLockscreenLayoutJob =
+                        scope.launch {
+                            lockscreenRemainingSpaceWithNotification.collect {
+                                (notificationBottom, notificationDefaultTop, shortcutTop) ->
+                                wallpaperManager.sendWallpaperCommand(
+                                    /* windowToken = */ rootView?.windowToken,
+                                    /* action = */ WallpaperManager
+                                        .COMMAND_LOCKSCREEN_LAYOUT_CHANGED,
+                                    /* x = */ 0,
+                                    /* y = */ 0,
+                                    /* z = */ 0,
+                                    /* extras = */ Bundle().apply {
+                                        putFloat("screenLeft", 0F)
+                                        putFloat("smartspaceBottom", notificationDefaultTop)
+                                        putFloat("notificationBottom", notificationBottom)
+                                        putFloat(
+                                            "screenRight",
+                                            context.resources.displayMetrics.widthPixels.toFloat(),
+                                        )
+                                        putFloat("shortCutTop", shortcutTop)
+                                    },
+                                )
+                            }
+                        }
+                } else {
+                    sendLockscreenLayoutJob?.cancel()
+                }
+                shouldSendNotificationLayout
+            }
+            .stateIn(
+                scope,
+                // Always be listening for wallpaper changes.
+                if (Flags.magicPortraitWallpapers()) SharingStarted.Eagerly
+                else SharingStarted.Lazily,
+                initialValue = false,
+            )
+
+    override fun setNotificationStackAbsoluteBottom(bottom: Float) {
+        notificationStackAbsoluteBottom.value = bottom
+    }
+
     private suspend fun getWallpaper(selectedUser: SelectedUserModel): WallpaperInfo? {
         return withContext(bgDispatcher) {
             wallpaperManager.getWallpaperInfoForUser(selectedUser.userInfo.id)
         }
     }
+
+    private fun shouldSendNotificationLayout(wallpaperInfo: WallpaperInfo?): Boolean {
+        return if (wallpaperInfo != null && wallpaperInfo.component != null) {
+            wallpaperInfo.component!!.className == MAGIC_PORTRAIT_CLASSNAME
+        } else {
+            false
+        }
+    }
+
+    companion object {
+        const val MAGIC_PORTRAIT_CLASSNAME =
+            "com.google.android.apps.magicportrait.service.MagicPortraitWallpaperService"
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/wallpapers/domain/interactor/WallpaperInteractor.kt b/packages/SystemUI/src/com/android/systemui/wallpapers/domain/interactor/WallpaperInteractor.kt
new file mode 100644
index 0000000..79ebf01
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/wallpapers/domain/interactor/WallpaperInteractor.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.wallpapers.domain.interactor
+
+import com.android.systemui.wallpapers.data.repository.WallpaperRepository
+import javax.inject.Inject
+
+class WallpaperInteractor @Inject constructor(val wallpaperRepository: WallpaperRepository) {
+    fun setNotificationStackAbsoluteBottom(bottom: Float) {
+        wallpaperRepository.setNotificationStackAbsoluteBottom(bottom)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
index 0b944f0..96a0aad 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt
@@ -39,13 +39,13 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.res.R
+import com.android.systemui.shade.LargeScreenHeaderHelper
 import com.android.systemui.shade.data.repository.shadeRepository
 import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationsKeyguardInteractor
 import com.android.systemui.statusbar.policy.fakeConfigurationController
 import com.android.systemui.statusbar.ui.fakeSystemBarUtilsProxy
 import com.android.systemui.testKosmos
 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 kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -57,6 +57,7 @@
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.ArgumentMatchers.anyString
 import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.mock
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(AndroidJUnit4::class)
@@ -122,6 +123,7 @@
                     { keyguardBlueprintInteractor },
                     keyguardRootViewModel,
                     aodBurnInViewModel,
+                    largeScreenHeaderHelperLazy = { mock<LargeScreenHeaderHelper>() },
                 )
         }
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
index 7cd306e..6425da4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java
@@ -83,6 +83,7 @@
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.notification.ColorUpdateLogger;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
+import com.android.systemui.statusbar.notification.HeadsUpTouchHelper;
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
 import com.android.systemui.statusbar.notification.collection.NotifCollection;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
@@ -103,7 +104,6 @@
 import com.android.systemui.statusbar.notification.stack.NotificationSwipeHelper.NotificationCallback;
 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder;
 import com.android.systemui.statusbar.phone.HeadsUpAppearanceController;
-import com.android.systemui.statusbar.notification.HeadsUpTouchHelper;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
@@ -113,6 +113,7 @@
 import com.android.systemui.statusbar.policy.ZenModeController;
 import com.android.systemui.tuner.TunerService;
 import com.android.systemui.util.settings.SecureSettings;
+import com.android.systemui.wallpapers.domain.interactor.WallpaperInteractor;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -154,6 +155,7 @@
     @Mock private KeyguardBypassController mKeyguardBypassController;
     @Mock private PowerInteractor mPowerInteractor;
     @Mock private PrimaryBouncerInteractor mPrimaryBouncerInteractor;
+    @Mock private WallpaperInteractor mWallpaperInteractor;
     @Mock private NotificationLockscreenUserManager mNotificationLockscreenUserManager;
     @Mock private MetricsLogger mMetricsLogger;
     @Mock private ColorUpdateLogger mColorUpdateLogger;
@@ -1070,7 +1072,8 @@
                 mock(NotificationDismissibilityProvider.class),
                 mActivityStarter,
                 new ResourcesSplitShadeStateController(),
-                mSensitiveNotificationProtectionController);
+                mSensitiveNotificationProtectionController,
+                mWallpaperInteractor);
     }
 
     static class LogMatcher implements ArgumentMatcher<LogMaker> {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 8a3e551..59fc0d1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -102,6 +102,7 @@
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.statusbar.policy.AvalancheController;
 import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController;
+import com.android.systemui.wallpapers.domain.interactor.WallpaperInteractor;
 
 import kotlin.Unit;
 
@@ -146,6 +147,7 @@
     @Mock private NotificationStackScrollLayoutController mStackScrollLayoutController;
     @Mock private ScreenOffAnimationController mScreenOffAnimationController;
     @Mock private NotificationShelf mNotificationShelf;
+    @Mock private WallpaperInteractor mWallpaperInteractor;
     @Mock private NotificationStackSizeCalculator mStackSizeCalculator;
     @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     @Mock private LargeScreenShadeInterpolator mLargeScreenShadeInterpolator;
@@ -208,6 +210,7 @@
                 .thenReturn(mNotificationRoundnessManager);
         mStackScroller.setController(mStackScrollLayoutController);
         mStackScroller.setShelf(mNotificationShelf);
+        mStackScroller.setWallpaperInteractor(mWallpaperInteractor);
         when(mStackScroller.getExpandHelper()).thenReturn(mExpandHelper);
 
         doNothing().when(mGroupExpansionManager).collapseGroups();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt
index bdecf2b..b8dd334 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt
@@ -18,18 +18,21 @@
 
 import android.app.WallpaperInfo
 import android.app.WallpaperManager
+import android.content.ComponentName
 import android.content.Intent
 import android.content.pm.UserInfo
+import android.platform.test.annotations.EnableFlags
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.data.repository.FakeKeyguardClockRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.user.data.model.SelectedUserModel
 import com.android.systemui.user.data.model.SelectionStatus
 import com.android.systemui.user.data.repository.FakeUserRepository
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
+import com.android.systemui.wallpapers.data.repository.WallpaperRepositoryImpl.Companion.MAGIC_PORTRAIT_CLASSNAME
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.StandardTestDispatcher
@@ -39,6 +42,9 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
 
 @SmallTest
 @OptIn(ExperimentalCoroutinesApi::class)
@@ -48,6 +54,8 @@
     private val testDispatcher = StandardTestDispatcher()
     private val testScope = TestScope(testDispatcher)
     private val userRepository = FakeUserRepository()
+    private val keyguardClockRepository = FakeKeyguardClockRepository()
+    private val keyguardRepository = FakeKeyguardRepository()
     private val wallpaperManager: WallpaperManager = mock()
 
     private val underTest: WallpaperRepositoryImpl by lazy {
@@ -56,6 +64,8 @@
             testDispatcher,
             fakeBroadcastDispatcher,
             userRepository,
+            keyguardRepository,
+            keyguardClockRepository,
             wallpaperManager,
             context,
         )
@@ -219,7 +229,7 @@
         testScope.runTest {
             context.orCreateTestableResources.addOverride(
                 com.android.internal.R.bool.config_dozeSupportsAodWallpaper,
-                false
+                false,
             )
 
             val latest by collectLastValue(underTest.wallpaperInfo)
@@ -407,7 +417,7 @@
         testScope.runTest {
             context.orCreateTestableResources.addOverride(
                 com.android.internal.R.bool.config_dozeSupportsAodWallpaper,
-                false
+                false,
             )
 
             val latest by collectLastValue(underTest.wallpaperSupportsAmbientMode)
@@ -425,6 +435,54 @@
             assertThat(latest).isFalse()
         }
 
+    @Test
+    @EnableFlags(Flags.FLAG_MAGIC_PORTRAIT_WALLPAPERS)
+    fun shouldSendNotificationLayout_setMagicPortraitWallpaper_launchSendLayoutJob() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.shouldSendNotificationLayout)
+            val magicPortraitWallpaper =
+                mock<WallpaperInfo>().apply {
+                    whenever(this.component)
+                        .thenReturn(ComponentName(context, MAGIC_PORTRAIT_CLASSNAME))
+                }
+            whenever(wallpaperManager.getWallpaperInfoForUser(any()))
+                .thenReturn(magicPortraitWallpaper)
+            fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+                context,
+                Intent(Intent.ACTION_WALLPAPER_CHANGED),
+            )
+            assertThat(latest).isTrue()
+            assertThat(underTest.sendLockscreenLayoutJob).isNotNull()
+            assertThat(underTest.sendLockscreenLayoutJob!!.isActive).isEqualTo(true)
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_MAGIC_PORTRAIT_WALLPAPERS)
+    fun shouldSendNotificationLayout_setNotMagicPortraitWallpaper_cancelSendLayoutJob() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.shouldSendNotificationLayout)
+            val magicPortraitWallpaper = MAGIC_PORTRAIT_WP
+            whenever(wallpaperManager.getWallpaperInfoForUser(any()))
+                .thenReturn(magicPortraitWallpaper)
+            fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+                context,
+                Intent(Intent.ACTION_WALLPAPER_CHANGED),
+            )
+            assertThat(latest).isTrue()
+            assertThat(underTest.sendLockscreenLayoutJob).isNotNull()
+            assertThat(underTest.sendLockscreenLayoutJob!!.isActive).isEqualTo(true)
+
+            val nonMagicPortraitWallpaper = UNSUPPORTED_WP
+            whenever(wallpaperManager.getWallpaperInfoForUser(any()))
+                .thenReturn(nonMagicPortraitWallpaper)
+            fakeBroadcastDispatcher.sendIntentToMatchingReceiversOnly(
+                context,
+                Intent(Intent.ACTION_WALLPAPER_CHANGED),
+            )
+            assertThat(latest).isFalse()
+            assertThat(underTest.sendLockscreenLayoutJob?.isCancelled).isEqualTo(true)
+        }
+
     private companion object {
         val USER_WITH_UNSUPPORTED_WP = UserInfo(/* id= */ 3, /* name= */ "user3", /* flags= */ 0)
         val UNSUPPORTED_WP =
@@ -433,5 +491,10 @@
         val USER_WITH_SUPPORTED_WP = UserInfo(/* id= */ 4, /* name= */ "user4", /* flags= */ 0)
         val SUPPORTED_WP =
             mock<WallpaperInfo>().apply { whenever(this.supportsAmbientMode()).thenReturn(true) }
+
+        val MAGIC_PORTRAIT_WP =
+            mock<WallpaperInfo>().apply {
+                whenever(this.component).thenReturn(ComponentName("", MAGIC_PORTRAIT_CLASSNAME))
+            }
     }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/app/WallpaperManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/app/WallpaperManagerKosmos.kt
new file mode 100644
index 0000000..2850ab7
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/app/WallpaperManagerKosmos.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.app
+
+import android.app.WallpaperManager
+import android.content.applicationContext
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+@OptIn(ExperimentalCoroutinesApi::class)
+val Kosmos.wallpaperManager: WallpaperManager by Fixture {
+    WallpaperManager.getInstance(applicationContext)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardClockRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardClockRepository.kt
index 5e5f8cb..159dd34 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardClockRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardClockRepository.kt
@@ -46,16 +46,27 @@
     private val _previewClock = MutableStateFlow(Mockito.mock(ClockController::class.java))
     override val previewClock: Flow<ClockController>
         get() = _previewClock
+
+    private val _notificationDefaultTop = MutableStateFlow(0F)
+    override val notificationDefaultTop: StateFlow<Float>
+        get() = _notificationDefaultTop
+
     override val clockEventController: ClockEventController
         get() = mock()
+
     override val shouldForceSmallClock: Boolean
         get() = _shouldForceSmallClock
+
     private var _shouldForceSmallClock: Boolean = false
 
     override fun setClockSize(size: ClockSize) {
         _clockSize.value = size
     }
 
+    override fun setNotificationDefaultTop(top: Float) {
+        _notificationDefaultTop.value = top
+    }
+
     fun setSelectedClockSize(size: ClockSizeSetting) {
         _selectedClockSize.value = size
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
index 54a6c0c..e513e8d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
@@ -131,6 +131,10 @@
     private val _isEncryptedOrLockdown = MutableStateFlow(true)
     override val isEncryptedOrLockdown: Flow<Boolean> = _isEncryptedOrLockdown
 
+    private val _shortcutAbsoluteTop = MutableStateFlow(0F)
+    override val shortcutAbsoluteTop: StateFlow<Float>
+        get() = _shortcutAbsoluteTop.asStateFlow()
+
     private val _isKeyguardEnabled = MutableStateFlow(true)
     override val isKeyguardEnabled: StateFlow<Boolean> = _isKeyguardEnabled.asStateFlow()
 
@@ -241,7 +245,7 @@
 
     override fun setBiometricUnlockState(
         mode: BiometricUnlockMode,
-        source: BiometricUnlockSource?
+        source: BiometricUnlockSource?,
     ) {
         _biometricUnlockState.tryEmit(BiometricUnlockModel(mode, source))
     }
@@ -294,6 +298,10 @@
         return isShowKeyguardWhenReenabled
     }
 
+    override fun setShortcutAbsoluteTop(top: Float) {
+        _shortcutAbsoluteTop.value = top
+    }
+
     override fun setCanIgnoreAuthAndReturnToGone(canWake: Boolean) {
         _canIgnoreAuthAndReturnToGone.value = canWake
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt
index 12d7c49..49a8c18 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryKosmos.kt
@@ -29,9 +29,10 @@
 import com.android.systemui.keyguard.ui.viewmodel.keyguardRootViewModel
 import com.android.systemui.keyguard.ui.viewmodel.keyguardSmartspaceViewModel
 import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.util.mockito.mock
+import com.android.systemui.shade.LargeScreenHeaderHelper
 import java.util.Optional
 import org.mockito.Mockito.spy
+import org.mockito.kotlin.mock
 
 val Kosmos.keyguardClockSection: ClockSection by
     Kosmos.Fixture {
@@ -43,6 +44,7 @@
             blueprintInteractor = { keyguardBlueprintInteractor },
             rootViewModel = keyguardRootViewModel,
             aodBurnInViewModel = aodBurnInViewModel,
+            largeScreenHeaderHelperLazy = { mock<LargeScreenHeaderHelper>() },
         )
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorKosmos.kt
index d52883e..bdb9abb 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorKosmos.kt
@@ -27,13 +27,13 @@
 val Kosmos.keyguardClockInteractor by
     Kosmos.Fixture {
         KeyguardClockInteractor(
-            keyguardClockRepository = keyguardClockRepository,
-            applicationScope = applicationCoroutineScope,
             mediaCarouselInteractor = mediaCarouselInteractor,
             activeNotificationsInteractor = activeNotificationsInteractor,
             shadeInteractor = shadeInteractor,
             keyguardInteractor = keyguardInteractor,
             keyguardTransitionInteractor = keyguardTransitionInteractor,
             headsUpNotificationInteractor = headsUpNotificationInteractor,
+            applicationScope = applicationCoroutineScope,
+            keyguardClockRepository = keyguardClockRepository,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryKosmos.kt
new file mode 100644
index 0000000..1d8c891
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryKosmos.kt
@@ -0,0 +1,43 @@
+/*
+ * 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.wallpapers.data.repository
+
+import android.content.applicationContext
+import com.android.app.wallpaperManager
+import com.android.systemui.broadcast.broadcastDispatcher
+import com.android.systemui.keyguard.data.repository.keyguardClockRepository
+import com.android.systemui.keyguard.data.repository.keyguardRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.user.data.repository.userRepository
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+@OptIn(ExperimentalCoroutinesApi::class)
+val Kosmos.wallpaperRepository by Fixture {
+    WallpaperRepositoryImpl(
+        context = applicationContext,
+        scope = testScope,
+        bgDispatcher = testDispatcher,
+        broadcastDispatcher = broadcastDispatcher,
+        userRepository = userRepository,
+        wallpaperManager = wallpaperManager,
+        keyguardClockRepository = keyguardClockRepository,
+        keyguardRepository = keyguardRepository,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/domain/interactor/WallpaperInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/domain/interactor/WallpaperInteractorKosmos.kt
new file mode 100644
index 0000000..5278351
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/wallpapers/domain/interactor/WallpaperInteractorKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.wallpapers.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.wallpapers.data.repository.wallpaperRepository
+
+val Kosmos.wallpaperInteractor by
+    Kosmos.Fixture { WallpaperInteractor(wallpaperRepository = wallpaperRepository) }
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index 596e375..e0cf96f 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -72,9 +72,6 @@
 import static android.content.pm.PermissionInfo.PROTECTION_FLAG_APPOP;
 import static android.permission.flags.Flags.deviceAwareAppOpNewSchemaEnabled;
 
-import static com.android.internal.util.FrameworkStatsLog.APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__CHECK_OPERATION;
-import static com.android.internal.util.FrameworkStatsLog.APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__NOTE_OPERATION;
-import static com.android.internal.util.FrameworkStatsLog.APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__NOTE_PROXY_OPERATION;
 import static com.android.server.appop.AppOpsService.ModeCallback.ALL_OPS;
 
 import android.Manifest;
@@ -163,7 +160,6 @@
 import com.android.internal.pm.pkg.component.ParsedAttribution;
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.DumpUtils;
-import com.android.internal.util.FrameworkStatsLog;
 import com.android.internal.util.Preconditions;
 import com.android.internal.util.XmlUtils;
 import com.android.internal.util.function.pooled.PooledLambda;
@@ -2833,26 +2829,12 @@
 
     @Override
     public int checkOperation(int code, int uid, String packageName) {
-        if (Flags.appopAccessTrackingLoggingEnabled()) {
-            FrameworkStatsLog.write(
-                    FrameworkStatsLog.APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED,
-                    uid, code,
-                    APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__CHECK_OPERATION,
-                    false);
-        }
         return mCheckOpsDelegateDispatcher.checkOperation(code, uid, packageName, null,
                 Context.DEVICE_ID_DEFAULT, false /*raw*/);
     }
 
     @Override
     public int checkOperationForDevice(int code, int uid, String packageName, int virtualDeviceId) {
-        if (Flags.appopAccessTrackingLoggingEnabled()) {
-            FrameworkStatsLog.write(
-                    FrameworkStatsLog.APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED,
-                    uid, code,
-                    APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__CHECK_OPERATION,
-                    false);
-        }
         return mCheckOpsDelegateDispatcher.checkOperation(code, uid, packageName, null,
                 virtualDeviceId, false /*raw*/);
     }
@@ -3033,14 +3015,6 @@
     public SyncNotedAppOp noteProxyOperationWithState(int code,
             AttributionSourceState attributionSourceState, boolean shouldCollectAsyncNotedOp,
             String message, boolean shouldCollectMessage, boolean skipProxyOperation) {
-        if (Flags.appopAccessTrackingLoggingEnabled()) {
-            FrameworkStatsLog.write(
-                    FrameworkStatsLog.APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED,
-                    attributionSourceState.uid, code,
-                    APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__NOTE_PROXY_OPERATION,
-                    attributionSourceState.attributionTag != null);
-        }
-
         AttributionSource attributionSource = new AttributionSource(attributionSourceState);
         return mCheckOpsDelegateDispatcher.noteProxyOperation(code, attributionSource,
                 shouldCollectAsyncNotedOp, message, shouldCollectMessage, skipProxyOperation);
@@ -3122,14 +3096,6 @@
     public SyncNotedAppOp noteOperation(int code, int uid, String packageName,
             String attributionTag, boolean shouldCollectAsyncNotedOp, String message,
             boolean shouldCollectMessage) {
-        if (Flags.appopAccessTrackingLoggingEnabled()) {
-            FrameworkStatsLog.write(
-                    FrameworkStatsLog.APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED,
-                    uid, code,
-                    APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__NOTE_OPERATION,
-                    attributionTag != null);
-        }
-
         return mCheckOpsDelegateDispatcher.noteOperation(code, uid, packageName,
                 attributionTag, Context.DEVICE_ID_DEFAULT, shouldCollectAsyncNotedOp, message,
                 shouldCollectMessage);
diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
index eeec369..99ced7f 100644
--- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
+++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
@@ -212,6 +212,10 @@
             Flags.FLAG_BLOCK_AUTOBRIGHTNESS_CHANGES_ON_STYLUS_USAGE,
             Flags::blockAutobrightnessChangesOnStylusUsage
     );
+    private final FlagState mIsUserRefreshRateForExternalDisplayEnabled = new FlagState(
+            Flags.FLAG_ENABLE_USER_REFRESH_RATE_FOR_EXTERNAL_DISPLAY,
+            Flags::enableUserRefreshRateForExternalDisplay
+    );
 
     private final FlagState mEnableBatteryStatsForAllDisplays = new FlagState(
             Flags.FLAG_ENABLE_BATTERY_STATS_FOR_ALL_DISPLAYS,
@@ -456,6 +460,14 @@
     }
 
     /**
+     * @return {@code true} if need to use user refresh rate settings for
+     * external displays.
+     */
+    public boolean isUserRefreshRateForExternalDisplayEnabled() {
+        return mIsUserRefreshRateForExternalDisplayEnabled.isEnabled();
+    }
+
+    /**
      * dumps all flagstates
      * @param pw printWriter
      */
@@ -501,6 +513,7 @@
         pw.println(" " + mIdleScreenConfigInSubscribingLightSensor);
         pw.println(" " + mEnableBatteryStatsForAllDisplays);
         pw.println(" " + mBlockAutobrightnessChangesOnStylusUsage);
+        pw.println(" " + mIsUserRefreshRateForExternalDisplayEnabled);
     }
 
     private static class FlagState {
diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig
index 20ecd2d..2f04d9e 100644
--- a/services/core/java/com/android/server/display/feature/display_flags.aconfig
+++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig
@@ -381,3 +381,14 @@
     bug: "352411468"
     is_fixed_read_only: true
 }
+
+flag {
+    name: "enable_user_refresh_rate_for_external_display"
+    namespace: "display_manager"
+    description: "Apply refresh rate from user preferred display mode to external displays"
+    bug: "370657357"
+    is_fixed_read_only: true
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
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 18e0d6e..ffa64bf 100644
--- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
@@ -1194,6 +1194,13 @@
         @GuardedBy("mLock")
         private void updateRefreshRateSettingLocked(float minRefreshRate, float peakRefreshRate,
                 float defaultRefreshRate, int displayId) {
+            if (mDisplayObserver.isExternalDisplayLocked(displayId)) {
+                if (mLoggingEnabled) {
+                    Slog.d(TAG, "skip updateRefreshRateSettingLocked for external display "
+                            + displayId);
+                }
+                return;
+            }
             // TODO(b/156304339): The logic in here, aside from updating the refresh rate votes, is
             // used to predict if we're going to be doing frequent refresh rate switching, and if
             // so, enable the brightness observer. The logic here is more complicated and fragile
@@ -1243,6 +1250,8 @@
         }
 
         private void removeRefreshRateSetting(int displayId) {
+            mVotesStorage.updateVote(displayId, Vote.PRIORITY_USER_SETTING_PEAK_REFRESH_RATE,
+                    null);
             mVotesStorage.updateVote(displayId, Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE,
                     null);
             mVotesStorage.updateVote(displayId, Vote.PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE,
@@ -1458,11 +1467,11 @@
         public void onDisplayAdded(int displayId) {
             updateDisplayDeviceConfig(displayId);
             DisplayInfo displayInfo = getDisplayInfo(displayId);
+            registerExternalDisplay(displayInfo);
             updateDisplayModes(displayId, displayInfo);
             updateLayoutLimitedFrameRate(displayId, displayInfo);
             updateUserSettingDisplayPreferredSize(displayInfo);
             updateDisplaysPeakRefreshRateAndResolution(displayInfo);
-            addDisplaysSynchronizedPeakRefreshRate(displayInfo);
         }
 
         @Override
@@ -1477,7 +1486,7 @@
             updateLayoutLimitedFrameRate(displayId, null);
             removeUserSettingDisplayPreferredSize(displayId);
             removeDisplaysPeakRefreshRateAndResolution(displayId);
-            removeDisplaysSynchronizedPeakRefreshRate(displayId);
+            unregisterExternalDisplay(displayId);
         }
 
         @Override
@@ -1489,6 +1498,30 @@
             updateUserSettingDisplayPreferredSize(displayInfo);
         }
 
+        private void registerExternalDisplay(DisplayInfo displayInfo) {
+            if (displayInfo == null || displayInfo.type != Display.TYPE_EXTERNAL) {
+                return;
+            }
+            synchronized (mLock) {
+                mExternalDisplaysConnected.add(displayInfo.displayId);
+                if (mExternalDisplaysConnected.size() == 1) {
+                    addDisplaysSynchronizedPeakRefreshRate();
+                }
+            }
+        }
+
+        private void unregisterExternalDisplay(int displayId) {
+            synchronized (mLock) {
+                if (!isExternalDisplayLocked(displayId)) {
+                    return;
+                }
+                mExternalDisplaysConnected.remove(displayId);
+                if (mExternalDisplaysConnected.isEmpty()) {
+                    removeDisplaysSynchronizedPeakRefreshRate();
+                }
+            }
+        }
+
         boolean isExternalDisplayLocked(int displayId) {
             return mExternalDisplaysConnected.contains(displayId);
         }
@@ -1534,10 +1567,24 @@
                 return;
             }
 
-            mVotesStorage.updateVote(info.displayId,
-                    Vote.PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE,
-                    Vote.forSize(/* width */ preferredMode.getPhysicalWidth(),
-                            /* height */ preferredMode.getPhysicalHeight()));
+            if (info.type == Display.TYPE_EXTERNAL
+                    && mDisplayManagerFlags.isUserRefreshRateForExternalDisplayEnabled()
+                    && !isRefreshRateSynchronizationEnabled()) {
+                mVotesStorage.updateVote(info.displayId,
+                        Vote.PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE,
+                        Vote.forSizeAndPhysicalRefreshRatesRange(
+                                /* minWidth */ preferredMode.getPhysicalWidth(),
+                                /* minHeight */ preferredMode.getPhysicalHeight(),
+                                /* width */ preferredMode.getPhysicalWidth(),
+                                /* height */ preferredMode.getPhysicalHeight(),
+                                /* minRefreshRate */ preferredMode.getRefreshRate(),
+                                /* maxRefreshRate */ preferredMode.getRefreshRate()));
+            } else {
+                mVotesStorage.updateVote(info.displayId,
+                        Vote.PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE,
+                        Vote.forSize(/* width */ preferredMode.getPhysicalWidth(),
+                                /* height */ preferredMode.getPhysicalHeight()));
+            }
         }
 
         @Nullable
@@ -1584,17 +1631,10 @@
          * Sets 60Hz target refresh rate as the vote with
          * {@link Vote#PRIORITY_SYNCHRONIZED_REFRESH_RATE} priority.
          */
-        private void addDisplaysSynchronizedPeakRefreshRate(@Nullable final DisplayInfo info) {
-            if (info == null || info.type != Display.TYPE_EXTERNAL
-                    || !isRefreshRateSynchronizationEnabled()) {
+        private void addDisplaysSynchronizedPeakRefreshRate() {
+            if (!isRefreshRateSynchronizationEnabled()) {
                 return;
             }
-            synchronized (mLock) {
-                mExternalDisplaysConnected.add(info.displayId);
-                if (mExternalDisplaysConnected.size() != 1) {
-                    return;
-                }
-            }
             // set minRefreshRate as the max refresh rate.
             mVotesStorage.updateGlobalVote(Vote.PRIORITY_SYNCHRONIZED_REFRESH_RATE,
                     Vote.forPhysicalRefreshRates(
@@ -1610,19 +1650,10 @@
                                     + SYNCHRONIZED_REFRESH_RATE_TOLERANCE));
         }
 
-        private void removeDisplaysSynchronizedPeakRefreshRate(final int displayId) {
+        private void removeDisplaysSynchronizedPeakRefreshRate() {
             if (!isRefreshRateSynchronizationEnabled()) {
                 return;
             }
-            synchronized (mLock) {
-                if (!isExternalDisplayLocked(displayId)) {
-                    return;
-                }
-                mExternalDisplaysConnected.remove(displayId);
-                if (!mExternalDisplaysConnected.isEmpty()) {
-                    return;
-                }
-            }
             mVotesStorage.updateGlobalVote(Vote.PRIORITY_SYNCHRONIZED_REFRESH_RATE, null);
             mVotesStorage.updateGlobalVote(Vote.PRIORITY_SYNCHRONIZED_RENDER_FRAME_RATE, null);
         }
diff --git a/services/core/java/com/android/server/display/mode/Vote.java b/services/core/java/com/android/server/display/mode/Vote.java
index 459f9a6..f5abb05 100644
--- a/services/core/java/com/android/server/display/mode/Vote.java
+++ b/services/core/java/com/android/server/display/mode/Vote.java
@@ -44,7 +44,7 @@
     // It votes [minRefreshRate, Float.POSITIVE_INFINITY]
     int PRIORITY_USER_SETTING_MIN_RENDER_FRAME_RATE = 3;
 
-    // User setting preferred display resolution.
+    // User setting preferred display resolution, for external displays also includes refresh rate.
     int PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE = 4;
 
     // APP_REQUEST_RENDER_FRAME_RATE_RANGE is used to for internal apps to limit the render
diff --git a/services/core/java/com/android/server/notification/ManagedServices.java b/services/core/java/com/android/server/notification/ManagedServices.java
index cd0a2a7..03fc60c 100644
--- a/services/core/java/com/android/server/notification/ManagedServices.java
+++ b/services/core/java/com/android/server/notification/ManagedServices.java
@@ -106,8 +106,7 @@
     protected final String TAG = getClass().getSimpleName().replace('$', '.');
     protected final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
-    protected static final int ON_BINDING_DIED_REBIND_DELAY_MS = 10000;
-    protected static final int ON_BINDING_DIED_REBIND_MSG = 1234;
+    private static final int ON_BINDING_DIED_REBIND_DELAY_MS = 10000;
     protected static final String ENABLED_SERVICES_SEPARATOR = ":";
     private static final String DB_VERSION_1 = "1";
     private static final String DB_VERSION_2 = "2";
@@ -857,13 +856,7 @@
             String approvedItem = getApprovedValue(pkgOrComponent);
 
             if (approvedItem != null) {
-                final ComponentName component = ComponentName.unflattenFromString(approvedItem);
                 if (enabled) {
-                    if (component != null && !isValidService(component, userId)) {
-                        Log.e(TAG, "Skip allowing " + mConfig.caption + " " + pkgOrComponent
-                                + " (userSet: " + userSet + ") for invalid service");
-                        return;
-                    }
                     approved.add(approvedItem);
                 } else {
                     approved.remove(approvedItem);
@@ -961,7 +954,7 @@
                 || isPackageOrComponentAllowed(component.getPackageName(), userId))) {
             return false;
         }
-        return isValidService(component, userId);
+        return componentHasBindPermission(component, userId);
     }
 
     private boolean componentHasBindPermission(ComponentName component, int userId) {
@@ -1313,12 +1306,11 @@
                     if (TextUtils.equals(getPackageName(approvedPackageOrComponent), packageName)) {
                         final ComponentName component = ComponentName.unflattenFromString(
                                 approvedPackageOrComponent);
-                        if (component != null && !isValidService(component, userId)) {
+                        if (component != null && !componentHasBindPermission(component, userId)) {
                             approved.removeAt(j);
                             if (DEBUG) {
                                 Slog.v(TAG, "Removing " + approvedPackageOrComponent
-                                        + " from approved list; no bind permission or "
-                                        + "service interface filter found "
+                                        + " from approved list; no bind permission found "
                                         + mConfig.bindPermission);
                             }
                         }
@@ -1337,11 +1329,6 @@
         }
     }
 
-    protected boolean isValidService(ComponentName component, int userId) {
-        return componentHasBindPermission(component, userId) && queryPackageForServices(
-                component.getPackageName(), userId).contains(component);
-    }
-
     protected boolean isValidEntry(String packageOrComponent, int userId) {
         return hasMatchingServices(packageOrComponent, userId);
     }
@@ -1499,25 +1486,23 @@
      * Called when user switched to unbind all services from other users.
      */
     @VisibleForTesting
-    void unbindOtherUserServices(int switchedToUser) {
+    void unbindOtherUserServices(int currentUser) {
         TimingsTraceAndSlog t = new TimingsTraceAndSlog();
-        t.traceBegin("ManagedServices.unbindOtherUserServices_current" + switchedToUser);
-        unbindServicesImpl(switchedToUser, true /* allExceptUser */);
+        t.traceBegin("ManagedServices.unbindOtherUserServices_current" + currentUser);
+        unbindServicesImpl(currentUser, true /* allExceptUser */);
         t.traceEnd();
     }
 
-    void unbindUserServices(int removedUser) {
+    void unbindUserServices(int user) {
         TimingsTraceAndSlog t = new TimingsTraceAndSlog();
-        t.traceBegin("ManagedServices.unbindUserServices" + removedUser);
-        unbindServicesImpl(removedUser, false /* allExceptUser */);
+        t.traceBegin("ManagedServices.unbindUserServices" + user);
+        unbindServicesImpl(user, false /* allExceptUser */);
         t.traceEnd();
     }
 
     void unbindServicesImpl(int user, boolean allExceptUser) {
         final SparseArray<Set<ComponentName>> componentsToUnbind = new SparseArray<>();
         synchronized (mMutex) {
-            // Remove enqueued rebinds to avoid rebinding services for a switched user
-            mHandler.removeMessages(ON_BINDING_DIED_REBIND_MSG);
             final Set<ManagedServiceInfo> removableBoundServices = getRemovableConnectedServices();
             for (ManagedServiceInfo info : removableBoundServices) {
                 if ((allExceptUser && (info.userid != user))
@@ -1712,7 +1697,6 @@
                             mServicesRebinding.add(servicesBindingTag);
                             mHandler.postDelayed(() ->
                                     reregisterService(name, userid),
-                                    ON_BINDING_DIED_REBIND_MSG,
                                     ON_BINDING_DIED_REBIND_DELAY_MS);
                         } else {
                             Slog.v(TAG, getCaption() + " not rebinding in user " + userid
diff --git a/services/core/java/com/android/server/power/feature/PowerManagerFlags.java b/services/core/java/com/android/server/power/feature/PowerManagerFlags.java
index fd60e06..db57d11 100644
--- a/services/core/java/com/android/server/power/feature/PowerManagerFlags.java
+++ b/services/core/java/com/android/server/power/feature/PowerManagerFlags.java
@@ -47,6 +47,9 @@
             Flags::perDisplayWakeByTouch
     );
 
+    private final FlagState mFrameworkWakelockInfo =
+            new FlagState(Flags.FLAG_FRAMEWORK_WAKELOCK_INFO, Flags::frameworkWakelockInfo);
+
     /** Returns whether early-screen-timeout-detector is enabled on not. */
     public boolean isEarlyScreenTimeoutDetectorEnabled() {
         return mEarlyScreenTimeoutDetectorFlagState.isEnabled();
@@ -67,6 +70,13 @@
     }
 
     /**
+     * @return Whether FrameworkWakelockInfo atom logging is enabled or not.
+     */
+    public boolean isFrameworkWakelockInfoEnabled() {
+        return mFrameworkWakelockInfo.isEnabled();
+    }
+
+    /**
      * dumps all flagstates
      * @param pw printWriter
      */
@@ -75,6 +85,7 @@
         pw.println(" " + mEarlyScreenTimeoutDetectorFlagState);
         pw.println(" " + mImproveWakelockLatency);
         pw.println(" " + mPerDisplayWakeByTouch);
+        pw.println(" " + mFrameworkWakelockInfo);
     }
 
     private static class FlagState {
diff --git a/services/core/java/com/android/server/power/feature/power_flags.aconfig b/services/core/java/com/android/server/power/feature/power_flags.aconfig
index 9cf3bb6..8bb69ba 100644
--- a/services/core/java/com/android/server/power/feature/power_flags.aconfig
+++ b/services/core/java/com/android/server/power/feature/power_flags.aconfig
@@ -26,3 +26,10 @@
     bug: "343295183"
     is_fixed_read_only: true
 }
+
+flag {
+    name: "framework_wakelock_info"
+    namespace: "power"
+    description: "Feature flag to enable statsd pulling of FrameworkWakelockInfo atoms"
+    bug: "352602149"
+}
diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
index 936fadf..391071f 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -145,6 +145,7 @@
 import com.android.modules.utils.TypedXmlPullParser;
 import com.android.modules.utils.TypedXmlSerializer;
 import com.android.server.LocalServices;
+import com.android.server.power.feature.PowerManagerFlags;
 import com.android.server.power.optimization.Flags;
 import com.android.server.power.stats.SystemServerCpuThreadReader.SystemServiceCpuThreadTimes;
 import com.android.server.power.stats.format.MobileRadioPowerStatsLayout;
@@ -5142,6 +5143,10 @@
             mFrameworkStatsLogger.wakelockStateChanged(mapIsolatedUid(uid), wc, name,
                     uidStats.mProcessState, true /* acquired */,
                     getPowerManagerWakeLockLevel(type));
+            if (mPowerManagerFlags.isFrameworkWakelockInfoEnabled()) {
+                mFrameworkEvents.noteStartWakeLock(
+                        mapIsolatedUid(uid), name, getPowerManagerWakeLockLevel(type), uptimeMs);
+            }
         }
     }
 
@@ -5187,6 +5192,10 @@
             mFrameworkStatsLogger.wakelockStateChanged(mapIsolatedUid(uid), wc, name,
                     uidStats.mProcessState, false/* acquired */,
                     getPowerManagerWakeLockLevel(type));
+            if (mPowerManagerFlags.isFrameworkWakelockInfoEnabled()) {
+                mFrameworkEvents.noteStopWakeLock(
+                        mapIsolatedUid(uid), name, getPowerManagerWakeLockLevel(type), uptimeMs);
+            }
 
             if (mappedUid != uid) {
                 // Decrement the ref count for the isolated uid and delete the mapping if uneeded.
@@ -11343,6 +11352,9 @@
         return mTmpCpuTimeInFreq;
     }
 
+    WakelockStatsFrameworkEvents mFrameworkEvents = new WakelockStatsFrameworkEvents();
+    PowerManagerFlags mPowerManagerFlags = new PowerManagerFlags();
+
     public BatteryStatsImpl(@NonNull BatteryStatsConfig config, @NonNull Clock clock,
             @NonNull MonotonicClock monotonicClock, @Nullable File systemDir,
             @NonNull Handler handler, @Nullable PlatformIdleStateCallback platformIdleStateCallback,
@@ -15964,6 +15976,10 @@
                 // Already plugged in. Schedule the long plug in alarm.
                 scheduleNextResetWhilePluggedInCheck();
             }
+
+            if (mPowerManagerFlags.isFrameworkWakelockInfoEnabled()) {
+                mFrameworkEvents.initialize(context);
+            }
         }
     }
 
@@ -16791,7 +16807,8 @@
         }
         int NSORPMS = in.readInt();
         if (NSORPMS > 10000) {
-            throw new ParcelFormatException("File corrupt: too many screen-off rpm stats " + NSORPMS);
+            throw new ParcelFormatException(
+                    "File corrupt: too many screen-off rpm stats " + NSORPMS);
         }
         for (int irpm = 0; irpm < NSORPMS; irpm++) {
             if (in.readInt() != 0) {
diff --git a/services/core/java/com/android/server/power/stats/WakelockStatsFrameworkEvents.java b/services/core/java/com/android/server/power/stats/WakelockStatsFrameworkEvents.java
new file mode 100644
index 0000000..c9693bd
--- /dev/null
+++ b/services/core/java/com/android/server/power/stats/WakelockStatsFrameworkEvents.java
@@ -0,0 +1,328 @@
+/*
+ * 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.power.stats;
+
+import android.app.StatsManager;
+import android.content.Context;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.StatsEvent;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ConcurrentUtils;
+import com.android.internal.util.FrameworkStatsLog;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/** A class to initialise and log metrics pulled by statsd. */
+public class WakelockStatsFrameworkEvents {
+    // statsd has a dimensional limit on the number of different keys it can handle.
+    // Beyond that limit, statsd will drop data.
+    //
+    // When we have seem SUMMARY_THRESHOLD distinct (uid, tag, wakeLockLevel) keys,
+    // we start summarizing new keys as (uid, OVERFLOW_TAG, OVERFLOW_LEVEL) to
+    // reduce the number of keys we pass to statsd.
+    //
+    // When we reach MAX_WAKELOCK_DIMENSIONS distinct keys, we summarize all new keys
+    // as (OVERFLOW_UID, HARD_CAP_TAG, OVERFLOW_LEVEL) to hard cap the number of
+    // distinct keys we pass to statsd.
+    @VisibleForTesting public static final int SUMMARY_THRESHOLD = 500;
+    @VisibleForTesting public static final int MAX_WAKELOCK_DIMENSIONS = 1000;
+
+    @VisibleForTesting public static final int HARD_CAP_UID = -1;
+    @VisibleForTesting public static final String OVERFLOW_TAG = "*overflow*";
+    @VisibleForTesting public static final String HARD_CAP_TAG = "*overflow hard cap*";
+    @VisibleForTesting public static final int OVERFLOW_LEVEL = 1;
+
+    private static class WakeLockKey {
+        private int uid;
+        private String tag;
+        private int powerManagerWakeLockLevel;
+        private int hashCode;
+
+        WakeLockKey(int uid, String tag, int powerManagerWakeLockLevel) {
+            this.uid = uid;
+            this.tag = new String(tag);
+            this.powerManagerWakeLockLevel = powerManagerWakeLockLevel;
+
+            this.hashCode = Objects.hash(uid, tag, powerManagerWakeLockLevel);
+        }
+
+        int getUid() {
+            return uid;
+        }
+
+        String getTag() {
+            return tag;
+        }
+
+        int getPowerManagerWakeLockLevel() {
+            return powerManagerWakeLockLevel;
+        }
+
+        void setOverflow() {
+            tag = OVERFLOW_TAG;
+            powerManagerWakeLockLevel = OVERFLOW_LEVEL;
+            this.hashCode = Objects.hash(uid, tag, powerManagerWakeLockLevel);
+        }
+
+        void setHardCap() {
+            uid = HARD_CAP_UID;
+            tag = HARD_CAP_TAG;
+            powerManagerWakeLockLevel = OVERFLOW_LEVEL;
+            this.hashCode = Objects.hash(uid, tag, powerManagerWakeLockLevel);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || !(o instanceof WakeLockKey)) return false;
+
+            WakeLockKey that = (WakeLockKey) o;
+            return uid == that.uid
+                    && tag.equals(that.tag)
+                    && powerManagerWakeLockLevel == that.powerManagerWakeLockLevel;
+        }
+
+        @Override
+        public int hashCode() {
+            return this.hashCode;
+        }
+    }
+
+    private static class WakeLockStats {
+        // accumulated uptime attributed to this WakeLock since boot, where overlap
+        // (including nesting) is ignored
+        public long uptimeMillis = 0;
+
+        // count of WakeLocks that have been acquired and then released
+        public long completedCount = 0;
+    }
+
+    private final Object mLock = new Object();
+
+    @GuardedBy("mLock")
+    private final Map<WakeLockKey, WakeLockStats> mWakeLockStats = new HashMap<>();
+
+    private static class WakeLockData {
+        // uptime millis when first acquired
+        public long acquireUptimeMillis = 0;
+        public int refCount = 0;
+
+        WakeLockData(long uptimeMillis) {
+            acquireUptimeMillis = uptimeMillis;
+        }
+    }
+
+    @GuardedBy("mLock")
+    private final Map<WakeLockKey, WakeLockData> mOpenWakeLocks = new HashMap<>();
+
+    public void noteStartWakeLock(
+            int uid, String tag, int powerManagerWakeLockLevel, long eventUptimeMillis) {
+        final WakeLockKey key = new WakeLockKey(uid, tag, powerManagerWakeLockLevel);
+
+        synchronized (mLock) {
+            WakeLockData data =
+                    mOpenWakeLocks.computeIfAbsent(key, k -> new WakeLockData(eventUptimeMillis));
+            data.refCount++;
+            mOpenWakeLocks.put(key, data);
+        }
+    }
+
+    @VisibleForTesting
+    public boolean inOverflow() {
+        synchronized (mLock) {
+            return inOverflowLocked();
+        }
+    }
+
+    @GuardedBy("mLock")
+    private boolean inOverflowLocked() {
+        return mWakeLockStats.size() >= SUMMARY_THRESHOLD;
+    }
+
+    @VisibleForTesting
+    public boolean inHardCap() {
+        synchronized (mLock) {
+            return inHardCapLocked();
+        }
+    }
+
+    @GuardedBy("mLock")
+    private boolean inHardCapLocked() {
+        return mWakeLockStats.size() >= MAX_WAKELOCK_DIMENSIONS;
+    }
+
+    public void noteStopWakeLock(
+            int uid, String tag, int powerManagerWakeLockLevel, long eventUptimeMillis) {
+        WakeLockKey key = new WakeLockKey(uid, tag, powerManagerWakeLockLevel);
+
+        synchronized (mLock) {
+            WakeLockData data = mOpenWakeLocks.get(key);
+            if (data == null) {
+                Log.e(TAG, "WakeLock not found when stopping: " + uid + " " + tag);
+                return;
+            }
+
+            if (data.refCount == 1) {
+                mOpenWakeLocks.remove(key);
+                long wakeLockDur = eventUptimeMillis - data.acquireUptimeMillis;
+
+                // Rewrite key if in an overflow state.
+                if (inOverflowLocked() && !mWakeLockStats.containsKey(key)) {
+                    key.setOverflow();
+                    if (inHardCapLocked() && !mWakeLockStats.containsKey(key)) {
+                        key.setHardCap();
+                    }
+                }
+
+                WakeLockStats stats = mWakeLockStats.computeIfAbsent(key, k -> new WakeLockStats());
+                stats.uptimeMillis += wakeLockDur;
+                stats.completedCount++;
+                mWakeLockStats.put(key, stats);
+            } else {
+                data.refCount--;
+                mOpenWakeLocks.put(key, data);
+            }
+        }
+    }
+
+    public List<StatsEvent> pullFrameworkWakelockInfoAtoms() {
+        return pullFrameworkWakelockInfoAtoms(SystemClock.uptimeMillis());
+    }
+
+    public List<StatsEvent> pullFrameworkWakelockInfoAtoms(long nowMillis) {
+        List<StatsEvent> result = new ArrayList<>();
+        HashSet<WakeLockKey> keys = new HashSet<>();
+
+        // Used to collect open WakeLocks when in an overflow state.
+        HashMap<WakeLockKey, WakeLockStats> openOverflowStats = new HashMap<>();
+
+        synchronized (mLock) {
+            keys.addAll(mWakeLockStats.keySet());
+
+            // If we are in an overflow state, an open wakelock may have a new key
+            // that needs to be summarized.
+            if (inOverflowLocked()) {
+                for (WakeLockKey key : mOpenWakeLocks.keySet()) {
+                    if (!mWakeLockStats.containsKey(key)) {
+                        WakeLockData data = mOpenWakeLocks.get(key);
+
+                        key.setOverflow();
+                        if (inHardCapLocked() && !mWakeLockStats.containsKey(key)) {
+                            key.setHardCap();
+                        }
+                        keys.add(key);
+
+                        WakeLockStats stats =
+                                openOverflowStats.computeIfAbsent(key, k -> new WakeLockStats());
+                        stats.uptimeMillis += nowMillis - data.acquireUptimeMillis;
+                        openOverflowStats.put(key, stats);
+                    }
+                }
+            } else {
+                keys.addAll(mOpenWakeLocks.keySet());
+            }
+
+            for (WakeLockKey key : keys) {
+                long openWakeLockUptime = 0;
+                WakeLockData data = mOpenWakeLocks.get(key);
+                if (data != null) {
+                    openWakeLockUptime = nowMillis - data.acquireUptimeMillis;
+                }
+
+                WakeLockStats stats = mWakeLockStats.computeIfAbsent(key, k -> new WakeLockStats());
+                WakeLockStats extraTime =
+                        openOverflowStats.computeIfAbsent(key, k -> new WakeLockStats());
+
+                stats.uptimeMillis += openWakeLockUptime + extraTime.uptimeMillis;
+
+                StatsEvent event =
+                        StatsEvent.newBuilder()
+                                .setAtomId(FrameworkStatsLog.FRAMEWORK_WAKELOCK_INFO)
+                                .writeInt(key.getUid())
+                                .writeString(key.getTag())
+                                .writeInt(key.getPowerManagerWakeLockLevel())
+                                .writeLong(stats.uptimeMillis)
+                                .writeLong(stats.completedCount)
+                                .build();
+                result.add(event);
+            }
+        }
+
+        return result;
+    }
+
+    private static final String TAG = "BatteryStatsPulledMetrics";
+
+    private final StatsPullCallbackHandler mStatsPullCallbackHandler =
+            new StatsPullCallbackHandler();
+
+    private boolean mIsInitialized = false;
+
+    public void initialize(Context context) {
+        if (mIsInitialized) {
+            return;
+        }
+
+        final StatsManager statsManager = context.getSystemService(StatsManager.class);
+        if (statsManager == null) {
+            Log.e(
+                    TAG,
+                    "Error retrieving StatsManager. Cannot initialize BatteryStatsPulledMetrics.");
+        } else {
+            Log.d(TAG, "Registering callback with StatsManager");
+
+            // DIRECT_EXECUTOR means that callback will run on binder thread.
+            statsManager.setPullAtomCallback(
+                    FrameworkStatsLog.FRAMEWORK_WAKELOCK_INFO,
+                    null /* metadata */,
+                    ConcurrentUtils.DIRECT_EXECUTOR,
+                    mStatsPullCallbackHandler);
+            mIsInitialized = true;
+        }
+    }
+
+    private class StatsPullCallbackHandler implements StatsManager.StatsPullAtomCallback {
+        @Override
+        public int onPullAtom(int atomTag, List<StatsEvent> data) {
+            // handle the tags appropriately.
+            List<StatsEvent> events = pullEvents(atomTag);
+            if (events == null) {
+                return StatsManager.PULL_SKIP;
+            }
+
+            data.addAll(events);
+            return StatsManager.PULL_SUCCESS;
+        }
+
+        private List<StatsEvent> pullEvents(int atomTag) {
+            switch (atomTag) {
+                case FrameworkStatsLog.FRAMEWORK_WAKELOCK_INFO:
+                    return pullFrameworkWakelockInfoAtoms();
+                default:
+                    return null;
+            }
+        }
+    }
+}
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 d91f154..58f0ab4 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
@@ -1872,6 +1872,60 @@
     }
 
     @Test
+    public void testPeakRefreshRate_notAppliedToExternalDisplays() {
+        when(mDisplayManagerFlags.isBackUpSmoothDisplayAndForcePeakRefreshRateEnabled())
+                .thenReturn(true);
+        mInjector.mDisplayInfo.type = Display.TYPE_EXTERNAL;
+        DisplayModeDirector director =
+                new DisplayModeDirector(mContext, mHandler, mInjector,
+                        mDisplayManagerFlags, mDisplayDeviceConfigProvider);
+        director.getBrightnessObserver().setDefaultDisplayState(Display.STATE_ON);
+        director.getDisplayObserver().onDisplayAdded(DISPLAY_ID);
+        director.getDisplayObserver().onDisplayAdded(DISPLAY_ID_2);
+
+        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= */ 130),
+        };
+        Display.Mode[] modes2 = new Display.Mode[] {
+                new Display.Mode(/* modeId= */ 1, /* width= */ 1280, /* height= */ 720,
+                        /* refreshRate= */ 60),
+                new Display.Mode(/* modeId= */ 2, /* width= */ 1280, /* height= */ 720,
+                        /* refreshRate= */ 140),
+        };
+        SparseArray<Display.Mode[]> supportedModesByDisplay = new SparseArray<>();
+        supportedModesByDisplay.put(DISPLAY_ID, modes1);
+        supportedModesByDisplay.put(DISPLAY_ID_2, modes2);
+
+        Sensor lightSensor = createLightSensor();
+        SensorManager sensorManager = createMockSensorManager(lightSensor);
+        director.start(sensorManager);
+        director.injectSupportedModesByDisplay(supportedModesByDisplay);
+
+        // Disable Smooth Display
+        setPeakRefreshRate(RefreshRateSettingsUtils.DEFAULT_REFRESH_RATE);
+
+        Vote vote1 = director.getVote(DISPLAY_ID,
+                Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE);
+        Vote vote2 = director.getVote(DISPLAY_ID_2,
+                Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE);
+        assertThat(vote1).isNull();
+        assertThat(vote2).isNull();
+
+        // Enable Smooth Display
+        setPeakRefreshRate(Float.POSITIVE_INFINITY);
+
+        vote1 = director.getVote(DISPLAY_ID,
+                Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE);
+        vote2 = director.getVote(DISPLAY_ID_2,
+                Vote.PRIORITY_USER_SETTING_PEAK_RENDER_FRAME_RATE);
+        assertThat(vote1).isNull();
+        assertThat(vote2).isNull();
+    }
+
+    @Test
     public void testPeakRefreshRate_DisplayChanged() {
         when(mDisplayManagerFlags.isBackUpSmoothDisplayAndForcePeakRefreshRateEnabled())
                 .thenReturn(true);
@@ -1968,8 +2022,9 @@
     @Test
     @Parameters({
         "true, true, 60",
-        "false, true, 50",
-        "true, false, 50"
+        "false, true, 60",
+        "true, false, 50",
+        "false, false, 50"
     })
     public void testExternalDisplayMaxRefreshRate(boolean isRefreshRateSynchronizationEnabled,
             boolean isExternalDisplay, float expectedMaxRenderFrameRate) {
@@ -3810,6 +3865,7 @@
                 SensorManagerInternal sensorManagerInternal) {
             mDeviceConfig = new FakeDeviceConfig();
             mDisplayInfo = new DisplayInfo();
+            mDisplayInfo.type = Display.TYPE_INTERNAL;
             mDisplayInfo.defaultModeId = MODE_ID;
             mDisplayInfo.supportedModes = new Display.Mode[] {new Display.Mode(MODE_ID,
                     800, 600, /* refreshRate= */ 60)};
@@ -3856,6 +3912,7 @@
         @Override
         public boolean getDisplayInfo(int displayId, DisplayInfo displayInfo) {
             displayInfo.copyFrom(mDisplayInfo);
+            displayInfo.displayId = displayId;
             return mDisplayInfoValid;
         }
 
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayObserverTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayObserverTest.java
index 5e240cf..e3f150e 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayObserverTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayObserverTest.java
@@ -189,6 +189,7 @@
     @Test
     public void testExternalDisplay_voteUserPreferredMode() {
         when(mDisplayManagerFlags.isUserPreferredModeVoteEnabled()).thenReturn(true);
+        when(mDisplayManagerFlags.isUserRefreshRateForExternalDisplayEnabled()).thenReturn(false);
         var preferredMode = mExternalDisplayModes[5];
         mExternalDisplayUserPreferredModeId = preferredMode.getModeId();
         var expectedVote = Vote.forSize(
@@ -229,6 +230,108 @@
                 .isEqualTo(null);
     }
 
+    /** Vote for user preferred mode */
+    @Test
+    public void testDefaultDisplay_voteUserPreferredMode() {
+        when(mDisplayManagerFlags.isUserPreferredModeVoteEnabled()).thenReturn(true);
+        when(mDisplayManagerFlags.isUserRefreshRateForExternalDisplayEnabled()).thenReturn(true);
+        var preferredMode = mInternalDisplayModes[5];
+        mInternalDisplayUserPreferredModeId = preferredMode.getModeId();
+        var expectedVote = Vote.forSize(
+                        preferredMode.getPhysicalWidth(),
+                        preferredMode.getPhysicalHeight());
+        init();
+        assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(null);
+        mObserver.onDisplayAdded(EXTERNAL_DISPLAY);
+        mObserver.onDisplayAdded(DEFAULT_DISPLAY);
+        assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(null);
+        assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(expectedVote);
+
+        mInternalDisplayUserPreferredModeId = INVALID_MODE_ID;
+        mObserver.onDisplayChanged(EXTERNAL_DISPLAY);
+        mObserver.onDisplayChanged(DEFAULT_DISPLAY);
+        assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(null);
+        assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(null);
+
+        preferredMode = mInternalDisplayModes[4];
+        mInternalDisplayUserPreferredModeId = preferredMode.getModeId();
+        expectedVote = Vote.forSize(
+                        preferredMode.getPhysicalWidth(),
+                        preferredMode.getPhysicalHeight());
+        mObserver.onDisplayChanged(EXTERNAL_DISPLAY);
+        mObserver.onDisplayChanged(DEFAULT_DISPLAY);
+        assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(expectedVote);
+        assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(null);
+
+        // Testing that the vote is removed.
+        mObserver.onDisplayRemoved(EXTERNAL_DISPLAY);
+        assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(expectedVote);
+        assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(null);
+    }
+
+    /** Vote for user preferred mode with refresh rate, synchronization vote must be disabled */
+    @Test
+    public void testExternalDisplay_voteUserPreferredMode_withRefreshRate() {
+        when(mDisplayManagerFlags.isUserPreferredModeVoteEnabled()).thenReturn(true);
+        when(mDisplayManagerFlags.isDisplaysRefreshRatesSynchronizationEnabled()).thenReturn(false);
+        when(mDisplayManagerFlags.isUserRefreshRateForExternalDisplayEnabled()).thenReturn(true);
+        var preferredMode = mExternalDisplayModes[5];
+        mExternalDisplayUserPreferredModeId = preferredMode.getModeId();
+        var expectedVote = Vote.forSizeAndPhysicalRefreshRatesRange(
+                        preferredMode.getPhysicalWidth(),
+                        preferredMode.getPhysicalHeight(),
+                        preferredMode.getPhysicalWidth(),
+                        preferredMode.getPhysicalHeight(),
+                        preferredMode.getRefreshRate(),
+                        preferredMode.getRefreshRate());
+        init();
+        assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(null);
+        mObserver.onDisplayAdded(EXTERNAL_DISPLAY);
+        assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(null);
+        assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(expectedVote);
+
+        mExternalDisplayUserPreferredModeId = INVALID_MODE_ID;
+        mObserver.onDisplayChanged(EXTERNAL_DISPLAY);
+        assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(null);
+        assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(null);
+
+        preferredMode = mExternalDisplayModes[4];
+        mExternalDisplayUserPreferredModeId = preferredMode.getModeId();
+        expectedVote = Vote.forSizeAndPhysicalRefreshRatesRange(
+                        preferredMode.getPhysicalWidth(),
+                        preferredMode.getPhysicalHeight(),
+                        preferredMode.getPhysicalWidth(),
+                        preferredMode.getPhysicalHeight(),
+                        preferredMode.getRefreshRate(),
+                        preferredMode.getRefreshRate());
+        mObserver.onDisplayChanged(EXTERNAL_DISPLAY);
+        assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(null);
+        assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(expectedVote);
+
+        // Testing that the vote is removed.
+        mObserver.onDisplayRemoved(EXTERNAL_DISPLAY);
+        assertThat(getVote(DEFAULT_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(null);
+        assertThat(getVote(EXTERNAL_DISPLAY, PRIORITY_USER_SETTING_DISPLAY_PREFERRED_SIZE))
+                .isEqualTo(null);
+    }
+
     /** External display: Do not apply limit to user preferred mode */
     @Test
     public void testExternalDisplay_doNotApplyLimitToUserPreferredMode() {
diff --git a/services/tests/powerstatstests/Android.bp b/services/tests/powerstatstests/Android.bp
index d6ca10a..fab2031 100644
--- a/services/tests/powerstatstests/Android.bp
+++ b/services/tests/powerstatstests/Android.bp
@@ -27,6 +27,9 @@
         "servicestests-utils",
         "platform-test-annotations",
         "flag-junit",
+        "statsdprotolite",
+        "StatsdTestUtils",
+        "platformprotoslite",
     ],
 
     libs: [
@@ -68,6 +71,9 @@
         "androidx.test.uiautomator_uiautomator",
         "modules-utils-binary-xml",
         "flag-junit",
+        "statsdprotolite",
+        "StatsdTestUtils",
+        "platformprotoslite",
     ],
     srcs: [
         "src/com/android/server/power/stats/*.java",
diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockStatsFrameworkEventsTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockStatsFrameworkEventsTest.java
new file mode 100644
index 0000000..cb644db
--- /dev/null
+++ b/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockStatsFrameworkEventsTest.java
@@ -0,0 +1,433 @@
+/*
+ * 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.power.stats;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.os.WakeLockLevelEnum;
+import android.util.StatsEvent;
+import android.util.StatsEventTestUtils;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.os.AtomsProto;
+import com.android.os.framework.FrameworkExtensionAtoms;
+import com.android.os.framework.FrameworkExtensionAtoms.FrameworkWakelockInfo;
+
+import com.google.protobuf.CodedInputStream;
+import com.google.protobuf.CodedOutputStream;
+import com.google.protobuf.ExtensionRegistryLite;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class WakelockStatsFrameworkEventsTest {
+    private WakelockStatsFrameworkEvents mEvents;
+    private ExtensionRegistryLite mRegistry;
+
+    @Before
+    public void setup() {
+        mEvents = new WakelockStatsFrameworkEvents();
+        mRegistry = ExtensionRegistryLite.newInstance();
+        FrameworkExtensionAtoms.registerAllExtensions(mRegistry);
+    }
+
+    private static final int UID_1 = 1;
+    private static final int UID_2 = 2;
+
+    private static final String TAG_1 = "TAG1";
+    private static final String TAG_2 = "TAG2";
+
+    private static final WakeLockLevelEnum WAKELOCK_TYPE_1 = WakeLockLevelEnum.PARTIAL_WAKE_LOCK;
+    private static final WakeLockLevelEnum WAKELOCK_TYPE_2 = WakeLockLevelEnum.DOZE_WAKE_LOCK;
+
+    private static final long TS_1 = 1000;
+    private static final long TS_2 = 2000;
+    private static final long TS_3 = 3000;
+    private static final long TS_4 = 4000;
+    private static final long TS_5 = 5000;
+
+    // Assumes that mEvents is empty.
+    private void makeMetricsAlmostOverflow() throws Exception {
+        for (int i = 0; i < mEvents.SUMMARY_THRESHOLD - 1; i++) {
+            String tag = "forceOverflow" + i;
+            mEvents.noteStartWakeLock(UID_1, tag, WAKELOCK_TYPE_1.getNumber(), TS_1);
+            mEvents.noteStopWakeLock(UID_1, tag, WAKELOCK_TYPE_1.getNumber(), TS_2);
+        }
+
+        assertFalse("not overflow", mEvents.inOverflow());
+        ArrayList<FrameworkWakelockInfo> info = pullResults(TS_4);
+        FrameworkWakelockInfo notOverflowInfo =
+                info.stream()
+                        .filter(i -> i.getAttributionTag().equals(mEvents.OVERFLOW_TAG))
+                        .findFirst()
+                        .orElse(null);
+
+        assertEquals("not overflow", notOverflowInfo, null);
+
+        // Add one more to hit an overflow state.
+        String lastTag = "forceOverflowLast";
+        mEvents.noteStartWakeLock(UID_1, lastTag, WAKELOCK_TYPE_2.getNumber(), TS_1);
+        mEvents.noteStopWakeLock(UID_1, lastTag, WAKELOCK_TYPE_2.getNumber(), TS_2);
+
+        assertTrue("overflow", mEvents.inOverflow());
+        info = pullResults(TS_4);
+
+        FrameworkWakelockInfo tag1Info =
+                info.stream()
+                        .filter(i -> i.getAttributionTag().equals(lastTag))
+                        .findFirst()
+                        .orElse(null);
+
+        assertTrue("lastTag found", tag1Info != null);
+        assertEquals("uid", UID_1, tag1Info.getAttributionUid());
+        assertEquals("tag", lastTag, tag1Info.getAttributionTag());
+        assertEquals("type", WAKELOCK_TYPE_2, tag1Info.getType());
+        assertEquals("duration", TS_2 - TS_1, tag1Info.getUptimeMillis());
+        assertEquals("count", 1, tag1Info.getCompletedCount());
+    }
+
+    // Assumes that mEvents is empty.
+    private void makeMetricsAlmostHardCap() throws Exception {
+        for (int i = 0; i < mEvents.MAX_WAKELOCK_DIMENSIONS - 1; i++) {
+            mEvents.noteStartWakeLock(i /* uid */, TAG_1, WAKELOCK_TYPE_1.getNumber(), TS_1);
+            mEvents.noteStopWakeLock(i /* uid */, TAG_1, WAKELOCK_TYPE_1.getNumber(), TS_2);
+        }
+
+        assertFalse("not hard capped", mEvents.inHardCap());
+        ArrayList<FrameworkWakelockInfo> info = pullResults(TS_4);
+        FrameworkWakelockInfo notOverflowInfo =
+                info.stream()
+                        .filter(i -> i.getAttributionTag().equals(mEvents.HARD_CAP_TAG))
+                        .findFirst()
+                        .orElse(null);
+
+        assertEquals("not overflow", notOverflowInfo, null);
+
+        // Add one more to hit an hardcap state.
+        int hardCapUid = mEvents.MAX_WAKELOCK_DIMENSIONS;
+        mEvents.noteStartWakeLock(hardCapUid, TAG_2, WAKELOCK_TYPE_2.getNumber(), TS_1);
+        mEvents.noteStopWakeLock(hardCapUid, TAG_2, WAKELOCK_TYPE_2.getNumber(), TS_2);
+
+        assertTrue("hard capped", mEvents.inHardCap());
+        info = pullResults(TS_4);
+
+        FrameworkWakelockInfo tag2Info =
+                info.stream()
+                        .filter(i -> i.getAttributionUid() == hardCapUid)
+                        .findFirst()
+                        .orElse(null);
+
+        assertTrue("hardCapUid found", tag2Info != null);
+        assertEquals("uid", hardCapUid, tag2Info.getAttributionUid());
+        assertEquals("tag", mEvents.OVERFLOW_TAG, tag2Info.getAttributionTag());
+        assertEquals(
+                "type", WakeLockLevelEnum.forNumber(mEvents.OVERFLOW_LEVEL), tag2Info.getType());
+        assertEquals("duration", TS_2 - TS_1, tag2Info.getUptimeMillis());
+        assertEquals("count", 1, tag2Info.getCompletedCount());
+    }
+
+    private ArrayList<FrameworkWakelockInfo> pullResults(long timestamp) throws Exception {
+        ArrayList<FrameworkWakelockInfo> result = new ArrayList<>();
+        List<StatsEvent> events = mEvents.pullFrameworkWakelockInfoAtoms(timestamp);
+
+        for (StatsEvent e : events) {
+            // The returned atom does not have external extensions registered.
+            // So we serialize and then deserialize with extensions registered.
+            AtomsProto.Atom atom = StatsEventTestUtils.convertToAtom(e);
+
+            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+            CodedOutputStream codedos = CodedOutputStream.newInstance(outputStream);
+            atom.writeTo(codedos);
+            codedos.flush();
+
+            ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
+            CodedInputStream codedis = CodedInputStream.newInstance(inputStream);
+            AtomsProto.Atom atomWithExtensions = AtomsProto.Atom.parseFrom(codedis, mRegistry);
+
+            assertTrue(
+                    atomWithExtensions.hasExtension(FrameworkExtensionAtoms.frameworkWakelockInfo));
+            FrameworkWakelockInfo info =
+                    atomWithExtensions.getExtension(FrameworkExtensionAtoms.frameworkWakelockInfo);
+            result.add(info);
+        }
+
+        return result;
+    }
+
+    @Test
+    public void singleWakelock() throws Exception {
+        mEvents.noteStartWakeLock(UID_1, TAG_1, WAKELOCK_TYPE_1.getNumber(), TS_1);
+        mEvents.noteStopWakeLock(UID_1, TAG_1, WAKELOCK_TYPE_1.getNumber(), TS_2);
+
+        ArrayList<FrameworkWakelockInfo> info = pullResults(TS_3);
+
+        assertEquals("size", 1, info.size());
+        assertEquals("uid", UID_1, info.get(0).getAttributionUid());
+        assertEquals("tag", TAG_1, info.get(0).getAttributionTag());
+        assertEquals("type", WAKELOCK_TYPE_1, info.get(0).getType());
+        assertEquals("duration", TS_2 - TS_1, info.get(0).getUptimeMillis());
+        assertEquals("count", 1, info.get(0).getCompletedCount());
+    }
+
+    @Test
+    public void wakelockOpen() throws Exception {
+        mEvents.noteStartWakeLock(UID_1, TAG_1, WAKELOCK_TYPE_1.getNumber(), TS_1);
+
+        ArrayList<FrameworkWakelockInfo> info = pullResults(TS_3);
+
+        assertEquals("size", 1, info.size());
+        assertEquals("uid", UID_1, info.get(0).getAttributionUid());
+        assertEquals("tag", TAG_1, info.get(0).getAttributionTag());
+        assertEquals("type", WAKELOCK_TYPE_1, info.get(0).getType());
+        assertEquals("duration", TS_3 - TS_1, info.get(0).getUptimeMillis());
+        assertEquals("count", 0, info.get(0).getCompletedCount());
+    }
+
+    @Test
+    public void wakelockOpenOverlap() throws Exception {
+        mEvents.noteStartWakeLock(UID_1, TAG_1, WAKELOCK_TYPE_1.getNumber(), TS_1);
+        mEvents.noteStartWakeLock(UID_1, TAG_1, WAKELOCK_TYPE_1.getNumber(), TS_2);
+        mEvents.noteStopWakeLock(UID_1, TAG_1, WAKELOCK_TYPE_1.getNumber(), TS_3);
+
+        ArrayList<FrameworkWakelockInfo> info = pullResults(TS_4);
+
+        assertEquals("size", 1, info.size());
+        assertEquals("uid", UID_1, info.get(0).getAttributionUid());
+        assertEquals("tag", TAG_1, info.get(0).getAttributionTag());
+        assertEquals("type", WAKELOCK_TYPE_1, info.get(0).getType());
+        assertEquals("duration", TS_4 - TS_1, info.get(0).getUptimeMillis());
+        assertEquals("count", 0, info.get(0).getCompletedCount());
+    }
+
+    @Test
+    public void testOverflow() throws Exception {
+        makeMetricsAlmostOverflow();
+
+        // This one gets tagged as an overflow.
+        mEvents.noteStartWakeLock(UID_1, TAG_2, WAKELOCK_TYPE_2.getNumber(), TS_1);
+        mEvents.noteStopWakeLock(UID_1, TAG_2, WAKELOCK_TYPE_2.getNumber(), TS_2);
+
+        ArrayList<FrameworkWakelockInfo> info = pullResults(TS_4);
+        FrameworkWakelockInfo overflowInfo =
+                info.stream()
+                        .filter(i -> i.getAttributionTag().equals(mEvents.OVERFLOW_TAG))
+                        .findFirst()
+                        .orElse(null);
+
+        assertEquals("uid", UID_1, overflowInfo.getAttributionUid());
+        assertEquals(
+                "type",
+                WakeLockLevelEnum.forNumber(mEvents.OVERFLOW_LEVEL),
+                overflowInfo.getType());
+        assertEquals("duration", TS_2 - TS_1, overflowInfo.getUptimeMillis());
+        assertEquals("count", 1, overflowInfo.getCompletedCount());
+    }
+
+    @Test
+    public void testOverflowOpen() throws Exception {
+        makeMetricsAlmostOverflow();
+
+        // This is the open wakelock that overflows.
+        mEvents.noteStartWakeLock(UID_1, TAG_2, WAKELOCK_TYPE_2.getNumber(), TS_1);
+
+        ArrayList<FrameworkWakelockInfo> info = pullResults(TS_4);
+        FrameworkWakelockInfo overflowInfo =
+                info.stream()
+                        .filter(i -> i.getAttributionTag().equals(mEvents.OVERFLOW_TAG))
+                        .findFirst()
+                        .orElse(null);
+
+        assertEquals("uid", UID_1, overflowInfo.getAttributionUid());
+        assertEquals(
+                "type",
+                WakeLockLevelEnum.forNumber(mEvents.OVERFLOW_LEVEL),
+                overflowInfo.getType());
+        assertEquals("duration", (TS_4 - TS_1), overflowInfo.getUptimeMillis());
+        assertEquals("count", 0, overflowInfo.getCompletedCount());
+    }
+
+    @Test
+    public void testHardCap() throws Exception {
+        makeMetricsAlmostHardCap();
+
+        // This one gets tagged as a hard cap.
+        mEvents.noteStartWakeLock(UID_1, TAG_2, WAKELOCK_TYPE_2.getNumber(), TS_1);
+        mEvents.noteStopWakeLock(UID_1, TAG_2, WAKELOCK_TYPE_2.getNumber(), TS_2);
+
+        ArrayList<FrameworkWakelockInfo> info = pullResults(TS_4);
+        FrameworkWakelockInfo hardCapInfo =
+                info.stream()
+                        .filter(i -> i.getAttributionTag().equals(mEvents.HARD_CAP_TAG))
+                        .findFirst()
+                        .orElse(null);
+
+        assertEquals("uid", mEvents.HARD_CAP_UID, hardCapInfo.getAttributionUid());
+        assertEquals(
+                "type",
+                WakeLockLevelEnum.forNumber(mEvents.OVERFLOW_LEVEL),
+                hardCapInfo.getType());
+        assertEquals("duration", TS_2 - TS_1, hardCapInfo.getUptimeMillis());
+        assertEquals("count", 1, hardCapInfo.getCompletedCount());
+    }
+
+    @Test
+    public void testHardCapOpen() throws Exception {
+        makeMetricsAlmostHardCap();
+
+        // This is the open wakelock that overflows.
+        mEvents.noteStartWakeLock(UID_1, TAG_2, WAKELOCK_TYPE_2.getNumber(), TS_1);
+
+        ArrayList<FrameworkWakelockInfo> info = pullResults(TS_4);
+        FrameworkWakelockInfo hardCapInfo =
+                info.stream()
+                        .filter(i -> i.getAttributionTag().equals(mEvents.HARD_CAP_TAG))
+                        .findFirst()
+                        .orElse(null);
+
+        assertEquals("uid", mEvents.HARD_CAP_UID, hardCapInfo.getAttributionUid());
+        assertEquals(
+                "type",
+                WakeLockLevelEnum.forNumber(mEvents.OVERFLOW_LEVEL),
+                hardCapInfo.getType());
+        assertEquals("duration", (TS_4 - TS_1), hardCapInfo.getUptimeMillis());
+        assertEquals("count", 0, hardCapInfo.getCompletedCount());
+    }
+
+    @Test
+    public void overlappingWakelocks() throws Exception {
+        mEvents.noteStartWakeLock(UID_1, TAG_1, WAKELOCK_TYPE_1.getNumber(), TS_1);
+        mEvents.noteStartWakeLock(UID_1, TAG_1, WAKELOCK_TYPE_1.getNumber(), TS_2);
+        mEvents.noteStopWakeLock(UID_1, TAG_1, WAKELOCK_TYPE_1.getNumber(), TS_3);
+        mEvents.noteStopWakeLock(UID_1, TAG_1, WAKELOCK_TYPE_1.getNumber(), TS_4);
+
+        ArrayList<FrameworkWakelockInfo> info = pullResults(TS_5);
+
+        assertEquals("size", 1, info.size());
+        assertEquals("uid", UID_1, info.get(0).getAttributionUid());
+        assertEquals("tag", TAG_1, info.get(0).getAttributionTag());
+        assertEquals("type", WAKELOCK_TYPE_1, info.get(0).getType());
+        assertEquals("duration", TS_4 - TS_1, info.get(0).getUptimeMillis());
+        assertEquals("count", 1, info.get(0).getCompletedCount());
+    }
+
+    @Test
+    public void diffUid() throws Exception {
+        mEvents.noteStartWakeLock(UID_1, TAG_1, WAKELOCK_TYPE_1.getNumber(), TS_1);
+        mEvents.noteStartWakeLock(UID_2, TAG_1, WAKELOCK_TYPE_1.getNumber(), TS_2);
+        mEvents.noteStopWakeLock(UID_2, TAG_1, WAKELOCK_TYPE_1.getNumber(), TS_3);
+        mEvents.noteStopWakeLock(UID_1, TAG_1, WAKELOCK_TYPE_1.getNumber(), TS_4);
+
+        ArrayList<FrameworkWakelockInfo> info = pullResults(TS_5);
+        assertEquals("size", 2, info.size());
+
+        FrameworkWakelockInfo uid1Info =
+                info.stream().filter(i -> i.getAttributionUid() == UID_1).findFirst().orElse(null);
+
+        assertTrue("UID_1 found", uid1Info != null);
+        assertEquals("uid", UID_1, uid1Info.getAttributionUid());
+        assertEquals("tag", TAG_1, uid1Info.getAttributionTag());
+        assertEquals("type", WAKELOCK_TYPE_1, uid1Info.getType());
+        assertEquals("duration", TS_4 - TS_1, uid1Info.getUptimeMillis());
+        assertEquals("count", 1, uid1Info.getCompletedCount());
+
+        FrameworkWakelockInfo uid2Info =
+                info.stream().filter(i -> i.getAttributionUid() == UID_2).findFirst().orElse(null);
+        assertTrue("UID_2 found", uid2Info != null);
+        assertEquals("uid", UID_2, uid2Info.getAttributionUid());
+        assertEquals("tag", TAG_1, uid2Info.getAttributionTag());
+        assertEquals("type", WAKELOCK_TYPE_1, uid2Info.getType());
+        assertEquals("duration", TS_3 - TS_2, uid2Info.getUptimeMillis());
+        assertEquals("count", 1, uid2Info.getCompletedCount());
+    }
+
+    @Test
+    public void diffTag() throws Exception {
+        mEvents.noteStartWakeLock(UID_1, TAG_1, WAKELOCK_TYPE_1.getNumber(), TS_1);
+        mEvents.noteStartWakeLock(UID_1, TAG_2, WAKELOCK_TYPE_1.getNumber(), TS_2);
+        mEvents.noteStopWakeLock(UID_1, TAG_2, WAKELOCK_TYPE_1.getNumber(), TS_3);
+        mEvents.noteStopWakeLock(UID_1, TAG_1, WAKELOCK_TYPE_1.getNumber(), TS_4);
+
+        ArrayList<FrameworkWakelockInfo> info = pullResults(TS_5);
+        assertEquals("size", 2, info.size());
+
+        FrameworkWakelockInfo uid1Info =
+                info.stream()
+                        .filter(i -> i.getAttributionTag().equals(TAG_1))
+                        .findFirst()
+                        .orElse(null);
+
+        assertTrue("TAG_1 found", uid1Info != null);
+        assertEquals("uid", UID_1, uid1Info.getAttributionUid());
+        assertEquals("tag", TAG_1, uid1Info.getAttributionTag());
+        assertEquals("type", WAKELOCK_TYPE_1, uid1Info.getType());
+        assertEquals("duration", TS_4 - TS_1, uid1Info.getUptimeMillis());
+        assertEquals("count", 1, uid1Info.getCompletedCount());
+
+        FrameworkWakelockInfo uid2Info =
+                info.stream()
+                        .filter(i -> i.getAttributionTag().equals(TAG_2))
+                        .findFirst()
+                        .orElse(null);
+        assertTrue("TAG_2 found", uid2Info != null);
+        assertEquals("uid", UID_1, uid2Info.getAttributionUid());
+        assertEquals("tag", TAG_2, uid2Info.getAttributionTag());
+        assertEquals("type", WAKELOCK_TYPE_1, uid2Info.getType());
+        assertEquals("duration", TS_3 - TS_2, uid2Info.getUptimeMillis());
+        assertEquals("count", 1, uid2Info.getCompletedCount());
+    }
+
+    @Test
+    public void diffType() throws Exception {
+        mEvents.noteStartWakeLock(UID_1, TAG_1, WAKELOCK_TYPE_1.getNumber(), TS_1);
+        mEvents.noteStartWakeLock(UID_1, TAG_1, WAKELOCK_TYPE_2.getNumber(), TS_2);
+        mEvents.noteStopWakeLock(UID_1, TAG_1, WAKELOCK_TYPE_2.getNumber(), TS_3);
+        mEvents.noteStopWakeLock(UID_1, TAG_1, WAKELOCK_TYPE_1.getNumber(), TS_4);
+
+        ArrayList<FrameworkWakelockInfo> info = pullResults(TS_5);
+        assertEquals("size", 2, info.size());
+
+        FrameworkWakelockInfo uid1Info =
+                info.stream().filter(i -> i.getType() == WAKELOCK_TYPE_1).findFirst().orElse(null);
+
+        assertTrue("WAKELOCK_TYPE_1 found", uid1Info != null);
+        assertEquals("uid", UID_1, uid1Info.getAttributionUid());
+        assertEquals("tag", TAG_1, uid1Info.getAttributionTag());
+        assertEquals("type", WAKELOCK_TYPE_1, uid1Info.getType());
+        assertEquals("duration", TS_4 - TS_1, uid1Info.getUptimeMillis());
+        assertEquals("count", 1, uid1Info.getCompletedCount());
+
+        FrameworkWakelockInfo uid2Info =
+                info.stream().filter(i -> i.getType() == WAKELOCK_TYPE_2).findFirst().orElse(null);
+        assertTrue("WAKELOCK_TYPE_2 found", uid2Info != null);
+        assertEquals("uid", UID_1, uid2Info.getAttributionUid());
+        assertEquals("tag", TAG_1, uid2Info.getAttributionTag());
+        assertEquals("type", WAKELOCK_TYPE_2, uid2Info.getType());
+        assertEquals("duration", TS_3 - TS_2, uid2Info.getUptimeMillis());
+        assertEquals("count", 1, uid2Info.getCompletedCount());
+    }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
index 3bbc6b2..48bc9d7 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java
@@ -63,13 +63,11 @@
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.IInterface;
-import android.os.Looper;
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.platform.test.annotations.EnableFlags;
 import android.provider.Settings;
-import android.testing.TestableLooper;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -84,7 +82,6 @@
 
 import com.google.android.collect.Lists;
 
-import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
@@ -106,7 +103,6 @@
 import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 
-
 public class ManagedServicesTest extends UiServiceTestCase {
 
     @Mock
@@ -119,7 +115,6 @@
     private ManagedServices.UserProfiles mUserProfiles;
     @Mock private DevicePolicyManager mDpm;
     Object mLock = new Object();
-    private TestableLooper mTestableLooper;
 
     UserInfo mZero = new UserInfo(0, "zero", 0);
     UserInfo mTen = new UserInfo(10, "ten", 0);
@@ -147,7 +142,6 @@
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        mTestableLooper = new TestableLooper(Looper.getMainLooper());
 
         mContext.setMockPackageManager(mPm);
         mContext.addMockSystemService(Context.USER_SERVICE, mUm);
@@ -205,11 +199,6 @@
                 mIpm, APPROVAL_BY_COMPONENT);
     }
 
-    @After
-    public void tearDown() throws Exception {
-        mTestableLooper.destroy();
-    }
-
     @Test
     public void testBackupAndRestore_migration() throws Exception {
         for (int approvalLevel : new int[] {APPROVAL_BY_COMPONENT, APPROVAL_BY_PACKAGE}) {
@@ -899,7 +888,7 @@
             return true;
         });
 
-        mockServiceInfoWithMetaData(List.of(cn), service, pm, new ArrayMap<>());
+        mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>());
         service.addApprovedList("a", 0, true);
 
         service.reregisterService(cn, 0);
@@ -930,7 +919,7 @@
             return true;
         });
 
-        mockServiceInfoWithMetaData(List.of(cn), service, pm, new ArrayMap<>());
+        mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>());
         service.addApprovedList("a", 0, false);
 
         service.reregisterService(cn, 0);
@@ -961,7 +950,7 @@
             return true;
         });
 
-        mockServiceInfoWithMetaData(List.of(cn), service, pm, new ArrayMap<>());
+        mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>());
         service.addApprovedList("a/a", 0, true);
 
         service.reregisterService(cn, 0);
@@ -992,7 +981,7 @@
             return true;
         });
 
-        mockServiceInfoWithMetaData(List.of(cn), service, pm, new ArrayMap<>());
+        mockServiceInfoWithMetaData(List.of(cn), service, new ArrayMap<>());
         service.addApprovedList("a/a", 0, false);
 
         service.reregisterService(cn, 0);
@@ -1064,77 +1053,6 @@
     }
 
     @Test
-    public void registerService_bindingDied_rebindIsClearedOnUserSwitch() throws Exception {
-        Context context = mock(Context.class);
-        PackageManager pm = mock(PackageManager.class);
-        ApplicationInfo ai = new ApplicationInfo();
-        ai.targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT;
-
-        when(context.getPackageName()).thenReturn(mPkg);
-        when(context.getUserId()).thenReturn(mUser.getIdentifier());
-        when(context.getPackageManager()).thenReturn(pm);
-        when(pm.getApplicationInfo(anyString(), anyInt())).thenReturn(ai);
-
-        ManagedServices service = new TestManagedServices(context, mLock, mUserProfiles, mIpm,
-                APPROVAL_BY_PACKAGE);
-        service = spy(service);
-        ComponentName cn = ComponentName.unflattenFromString("a/a");
-
-        // Trigger onBindingDied for component when registering
-        //  => will schedule a rebind in 10 seconds
-        when(context.bindServiceAsUser(any(), any(), anyInt(), any())).thenAnswer(invocation -> {
-            Object[] args = invocation.getArguments();
-            ServiceConnection sc = (ServiceConnection) args[1];
-            sc.onBindingDied(cn);
-            return true;
-        });
-        service.registerService(cn, 0);
-        assertThat(service.isBound(cn, 0)).isFalse();
-
-        // Switch to user 10
-        service.onUserSwitched(10);
-
-        // Check that the scheduled rebind for user 0 was cleared
-        mTestableLooper.moveTimeForward(ManagedServices.ON_BINDING_DIED_REBIND_DELAY_MS);
-        mTestableLooper.processAllMessages();
-        verify(service, never()).reregisterService(any(), anyInt());
-    }
-
-    @Test
-    public void registerService_bindingDied_rebindIsExecutedAfterTimeout() throws Exception {
-        Context context = mock(Context.class);
-        PackageManager pm = mock(PackageManager.class);
-        ApplicationInfo ai = new ApplicationInfo();
-        ai.targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT;
-
-        when(context.getPackageName()).thenReturn(mPkg);
-        when(context.getUserId()).thenReturn(mUser.getIdentifier());
-        when(context.getPackageManager()).thenReturn(pm);
-        when(pm.getApplicationInfo(anyString(), anyInt())).thenReturn(ai);
-
-        ManagedServices service = new TestManagedServices(context, mLock, mUserProfiles, mIpm,
-                APPROVAL_BY_PACKAGE);
-        service = spy(service);
-        ComponentName cn = ComponentName.unflattenFromString("a/a");
-
-        // Trigger onBindingDied for component when registering
-        //  => will schedule a rebind in 10 seconds
-        when(context.bindServiceAsUser(any(), any(), anyInt(), any())).thenAnswer(invocation -> {
-            Object[] args = invocation.getArguments();
-            ServiceConnection sc = (ServiceConnection) args[1];
-            sc.onBindingDied(cn);
-            return true;
-        });
-        service.registerService(cn, 0);
-        assertThat(service.isBound(cn, 0)).isFalse();
-
-        // Check that the scheduled rebind is run
-        mTestableLooper.moveTimeForward(ManagedServices.ON_BINDING_DIED_REBIND_DELAY_MS);
-        mTestableLooper.processAllMessages();
-        verify(service, times(1)).reregisterService(eq(cn), eq(0));
-    }
-
-    @Test
     public void testPackageUninstall_packageNoLongerInApprovedList() throws Exception {
         for (int approvalLevel : new int[] {APPROVAL_BY_COMPONENT, APPROVAL_BY_PACKAGE}) {
             ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles,
@@ -1293,64 +1211,6 @@
     }
 
     @Test
-    public void testUpgradeAppNoIntentFilterNoRebind() throws Exception {
-        Context context = spy(getContext());
-        doReturn(true).when(context).bindServiceAsUser(any(), any(), anyInt(), any());
-
-        ManagedServices service = new TestManagedServices(context, mLock, mUserProfiles,
-                mIpm, APPROVAL_BY_COMPONENT);
-
-        List<String> packages = new ArrayList<>();
-        packages.add("package");
-        addExpectedServices(service, packages, 0);
-
-        final ComponentName unapprovedComponent = ComponentName.unflattenFromString("package/C1");
-        final ComponentName approvedComponent = ComponentName.unflattenFromString("package/C2");
-
-        // Both components are approved initially
-        mExpectedPrimaryComponentNames.clear();
-        mExpectedPrimaryPackages.clear();
-        mExpectedPrimaryComponentNames.put(0, "package/C1:package/C2");
-        mExpectedSecondaryComponentNames.clear();
-        mExpectedSecondaryPackages.clear();
-
-        loadXml(service);
-
-        //Component package/C1 loses serviceInterface intent filter
-        ManagedServices.Config config = service.getConfig();
-        when(mPm.queryIntentServicesAsUser(any(), anyInt(), anyInt()))
-                .thenAnswer(new Answer<List<ResolveInfo>>() {
-                    @Override
-                    public List<ResolveInfo> answer(InvocationOnMock invocationOnMock)
-                            throws Throwable {
-                        Object[] args = invocationOnMock.getArguments();
-                        Intent invocationIntent = (Intent) args[0];
-                        if (invocationIntent != null) {
-                            if (invocationIntent.getAction().equals(config.serviceInterface)
-                                    && packages.contains(invocationIntent.getPackage())) {
-                                List<ResolveInfo> dummyServices = new ArrayList<>();
-                                ResolveInfo resolveInfo = new ResolveInfo();
-                                ServiceInfo serviceInfo = new ServiceInfo();
-                                serviceInfo.packageName = invocationIntent.getPackage();
-                                serviceInfo.name = approvedComponent.getClassName();
-                                serviceInfo.permission = service.getConfig().bindPermission;
-                                resolveInfo.serviceInfo = serviceInfo;
-                                dummyServices.add(resolveInfo);
-                                return dummyServices;
-                            }
-                        }
-                        return new ArrayList<>();
-                    }
-                });
-
-        // Trigger package update
-        service.onPackagesChanged(false, new String[]{"package"}, new int[]{0});
-
-        assertFalse(service.isComponentEnabledForCurrentProfiles(unapprovedComponent));
-        assertTrue(service.isComponentEnabledForCurrentProfiles(approvedComponent));
-    }
-
-    @Test
     public void testSetPackageOrComponentEnabled() throws Exception {
         for (int approvalLevel : new int[] {APPROVAL_BY_COMPONENT, APPROVAL_BY_PACKAGE}) {
             ManagedServices service = new TestManagedServices(getContext(), mLock, mUserProfiles,
@@ -1363,21 +1223,6 @@
                             "user10package1/K", "user10.3/Component", "user10package2/L",
                             "user10.4/Component"}));
 
-            // mock permissions for services
-            PackageManager pm = mock(PackageManager.class);
-            when(getContext().getPackageManager()).thenReturn(pm);
-            List<ComponentName> enabledComponents = List.of(
-                    ComponentName.unflattenFromString("package/Comp"),
-                    ComponentName.unflattenFromString("package/C2"),
-                    ComponentName.unflattenFromString("again/M4"),
-                    ComponentName.unflattenFromString("user10package/B"),
-                    ComponentName.unflattenFromString("user10/Component"),
-                    ComponentName.unflattenFromString("user10package1/K"),
-                    ComponentName.unflattenFromString("user10.3/Component"),
-                    ComponentName.unflattenFromString("user10package2/L"),
-                    ComponentName.unflattenFromString("user10.4/Component"));
-            mockServiceInfoWithMetaData(enabledComponents, service, pm, new ArrayMap<>());
-
             for (int userId : expectedEnabled.keySet()) {
                 ArrayList<String> expectedForUser = expectedEnabled.get(userId);
                 for (int i = 0; i < expectedForUser.size(); i++) {
@@ -2099,7 +1944,7 @@
         metaDataAutobindAllow.putBoolean(META_DATA_DEFAULT_AUTOBIND, true);
         metaDatas.put(cn_allowed, metaDataAutobindAllow);
 
-        mockServiceInfoWithMetaData(componentNames, service, pm, metaDatas);
+        mockServiceInfoWithMetaData(componentNames, service, metaDatas);
 
         service.addApprovedList(cn_allowed.flattenToString(), 0, true);
         service.addApprovedList(cn_disallowed.flattenToString(), 0, true);
@@ -2144,7 +1989,7 @@
         metaDataAutobindDisallow.putBoolean(META_DATA_DEFAULT_AUTOBIND, false);
         metaDatas.put(cn_disallowed, metaDataAutobindDisallow);
 
-        mockServiceInfoWithMetaData(componentNames, service, pm, metaDatas);
+        mockServiceInfoWithMetaData(componentNames, service, metaDatas);
 
         service.addApprovedList(cn_disallowed.flattenToString(), 0, true);
 
@@ -2183,7 +2028,7 @@
         metaDataAutobindDisallow.putBoolean(META_DATA_DEFAULT_AUTOBIND, false);
         metaDatas.put(cn_disallowed, metaDataAutobindDisallow);
 
-        mockServiceInfoWithMetaData(componentNames, service, pm, metaDatas);
+        mockServiceInfoWithMetaData(componentNames, service, metaDatas);
 
         service.addApprovedList(cn_disallowed.flattenToString(), 0, true);
 
@@ -2254,8 +2099,8 @@
     }
 
     private void mockServiceInfoWithMetaData(List<ComponentName> componentNames,
-            ManagedServices service, PackageManager packageManager,
-            ArrayMap<ComponentName, Bundle> metaDatas) throws RemoteException {
+            ManagedServices service, ArrayMap<ComponentName, Bundle> metaDatas)
+            throws RemoteException {
         when(mIpm.getServiceInfo(any(), anyLong(), anyInt())).thenAnswer(
                 (Answer<ServiceInfo>) invocation -> {
                     ComponentName invocationCn = invocation.getArgument(0);
@@ -2270,39 +2115,6 @@
                     return null;
                 }
         );
-
-        // add components to queryIntentServicesAsUser response
-        final List<String> packages = new ArrayList<>();
-        for (ComponentName cn: componentNames) {
-            packages.add(cn.getPackageName());
-        }
-        ManagedServices.Config config = service.getConfig();
-        when(packageManager.queryIntentServicesAsUser(any(), anyInt(), anyInt())).
-                thenAnswer(new Answer<List<ResolveInfo>>() {
-                @Override
-                public List<ResolveInfo> answer(InvocationOnMock invocationOnMock)
-                    throws Throwable {
-                    Object[] args = invocationOnMock.getArguments();
-                    Intent invocationIntent = (Intent) args[0];
-                    if (invocationIntent != null) {
-                        if (invocationIntent.getAction().equals(config.serviceInterface)
-                            && packages.contains(invocationIntent.getPackage())) {
-                            List<ResolveInfo> dummyServices = new ArrayList<>();
-                            for (ComponentName cn: componentNames) {
-                                ResolveInfo resolveInfo = new ResolveInfo();
-                                ServiceInfo serviceInfo = new ServiceInfo();
-                                serviceInfo.packageName = invocationIntent.getPackage();
-                                serviceInfo.name = cn.getClassName();
-                                serviceInfo.permission = service.getConfig().bindPermission;
-                                resolveInfo.serviceInfo = serviceInfo;
-                                dummyServices.add(resolveInfo);
-                            }
-                            return dummyServices;
-                        }
-                    }
-                    return new ArrayList<>();
-                }
-            });
     }
 
     private void resetComponentsAndPackages() {
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java
index 7e4ae67..797b95b5 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAssistantsTest.java
@@ -25,7 +25,6 @@
 import static junit.framework.Assert.assertTrue;
 
 import static org.junit.Assert.assertNull;
-
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Matchers.any;
@@ -194,8 +193,6 @@
     public void testWriteXml_userTurnedOffNAS() throws Exception {
         int userId = ActivityManager.getCurrentUser();
 
-        doReturn(true).when(mAssistants).isValidService(eq(mCn), eq(userId));
-
         mAssistants.loadDefaultsFromConfig(true);
 
         mAssistants.setPackageOrComponentEnabled(mCn.flattenToString(), userId, true,
@@ -401,10 +398,6 @@
     public void testSetPackageOrComponentEnabled_onlyOnePackage() throws Exception {
         ComponentName component1 = ComponentName.unflattenFromString("package/Component1");
         ComponentName component2 = ComponentName.unflattenFromString("package/Component2");
-
-        doReturn(true).when(mAssistants).isValidService(eq(component1), eq(mZero.id));
-        doReturn(true).when(mAssistants).isValidService(eq(component2), eq(mZero.id));
-
         mAssistants.setPackageOrComponentEnabled(component1.flattenToString(), mZero.id, true,
                 true, true);
         verify(mNm, never()).setNotificationAssistantAccessGrantedForUserInternal(
@@ -550,7 +543,6 @@
     public void testSetAdjustmentTypeSupportedState() throws Exception {
         int userId = ActivityManager.getCurrentUser();
 
-        doReturn(true).when(mAssistants).isValidService(eq(mCn), eq(userId));
         mAssistants.loadDefaultsFromConfig(true);
         mAssistants.setPackageOrComponentEnabled(mCn.flattenToString(), userId, true,
                 true, true);
@@ -574,7 +566,6 @@
     public void testSetAdjustmentTypeSupportedState_readWriteXml_entries() throws Exception {
         int userId = ActivityManager.getCurrentUser();
 
-        doReturn(true).when(mAssistants).isValidService(eq(mCn), eq(userId));
         mAssistants.loadDefaultsFromConfig(true);
         mAssistants.setPackageOrComponentEnabled(mCn.flattenToString(), userId, true,
                 true, true);
@@ -598,7 +589,6 @@
     public void testSetAdjustmentTypeSupportedState_readWriteXml_empty() throws Exception {
         int userId = ActivityManager.getCurrentUser();
 
-        doReturn(true).when(mAssistants).isValidService(eq(mCn), eq(userId));
         mAssistants.loadDefaultsFromConfig(true);
         mAssistants.setPackageOrComponentEnabled(mCn.flattenToString(), userId, true,
                 true, true);
diff --git a/tests/graphics/SilkFX/res/layout/view_blur_behind.xml b/tests/graphics/SilkFX/res/layout/view_blur_behind.xml
new file mode 100644
index 0000000..83b1fa4
--- /dev/null
+++ b/tests/graphics/SilkFX/res/layout/view_blur_behind.xml
@@ -0,0 +1,148 @@
+<?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.
+  -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical">
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:padding="8dp"
+            android:textSize="24dp"
+            android:text="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" />
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:padding="8dp"
+            android:textSize="24dp"
+            android:text="wowwowwowwowwowwowwowwowwowwowwowwowwowwowwow" />
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:padding="8dp"
+            android:textSize="24dp"
+            android:text="I'm a little teapot" />
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:padding="8dp"
+            android:textSize="24dp"
+            android:text="Something. Something." />
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:padding="8dp"
+            android:textSize="24dp"
+            android:text="/\\/\\/\\/\\/\\/\\/\\/\\/\\/" />
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:padding="8dp"
+            android:textSize="24dp"
+            android:text="^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^" />
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:padding="8dp"
+            android:textSize="24dp"
+            android:text="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" />
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:padding="8dp"
+            android:textSize="24dp"
+            android:text="wowwowwowwowwowwowwowwowwowwowwowwowwowwowwow" />
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:padding="8dp"
+            android:textSize="24dp"
+            android:text="I'm a little teapot" />
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:padding="8dp"
+            android:textSize="24dp"
+            android:text="Something. Something." />
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:padding="8dp"
+            android:textSize="24dp"
+            android:text="/\\/\\/\\/\\/\\/\\/\\/\\/\\/" />
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:padding="8dp"
+            android:textSize="24dp"
+            android:text="^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^.^" />
+
+    </LinearLayout>
+
+    <ScrollView
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical">
+
+            <View
+                android:layout_width="match_parent"
+                android:layout_height="300dp" />
+
+            <com.android.test.silkfx.materials.BlurBehindContainer
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:background="#33AAAAAA"
+                android:padding="32dp">
+
+                <TextView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:textSize="48dp"
+                    android:text="Blur!" />
+
+            </com.android.test.silkfx.materials.BlurBehindContainer>
+
+            <View
+                android:layout_width="match_parent"
+                android:layout_height="1024dp" />
+
+        </LinearLayout>
+
+    </ScrollView>
+
+</FrameLayout>
\ No newline at end of file
diff --git a/tests/graphics/SilkFX/src/com/android/test/silkfx/Main.kt b/tests/graphics/SilkFX/src/com/android/test/silkfx/Main.kt
index 59a6078..6b6d3b8 100644
--- a/tests/graphics/SilkFX/src/com/android/test/silkfx/Main.kt
+++ b/tests/graphics/SilkFX/src/com/android/test/silkfx/Main.kt
@@ -61,7 +61,8 @@
         )),
         DemoGroup("Materials", listOf(
                 Demo("Glass", GlassActivity::class),
-                Demo("Background Blur", BackgroundBlurActivity::class)
+                Demo("Background Blur", BackgroundBlurActivity::class),
+                Demo("View blur behind", R.layout.view_blur_behind, commonControls = false)
         ))
 )
 
diff --git a/tests/graphics/SilkFX/src/com/android/test/silkfx/materials/BlurBehindContainer.kt b/tests/graphics/SilkFX/src/com/android/test/silkfx/materials/BlurBehindContainer.kt
new file mode 100644
index 0000000..ce6348e
--- /dev/null
+++ b/tests/graphics/SilkFX/src/com/android/test/silkfx/materials/BlurBehindContainer.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.test.silkfx.materials
+
+import android.content.Context
+import android.graphics.RenderEffect
+import android.graphics.Shader
+import android.util.AttributeSet
+import android.widget.FrameLayout
+
+class BlurBehindContainer(context: Context, attributeSet: AttributeSet) : FrameLayout(context, attributeSet) {
+    override fun onFinishInflate() {
+        super.onFinishInflate()
+        setBackdropRenderEffect(
+            RenderEffect.createBlurEffect(16.0f, 16.0f, Shader.TileMode.CLAMP))
+    }
+}
\ No newline at end of file