Merge changes Ie1d21324,Ic64d4c25,Ifa02960c,I16549d12 into main

* changes:
  [flexiglass] Disable NPVC and NPV when flexiglass is on
  Remove reference to NPVC
  Move shouldHideStatusBarIconsWhenExpanded to PanelExpansionInteractor
  Hydrate ShadeExpansionStateManager
diff --git a/Ravenwood.bp b/Ravenwood.bp
index f43c37b..6237e3e 100644
--- a/Ravenwood.bp
+++ b/Ravenwood.bp
@@ -77,6 +77,19 @@
     ],
 }
 
+// Extract the stats file.
+genrule {
+    name: "framework-minus-apex.ravenwood.stats",
+    defaults: ["ravenwood-internal-only-visibility-genrule"],
+    cmd: "cp $(in) $(out)",
+    srcs: [
+        ":framework-minus-apex.ravenwood-base{hoststubgen_framework-minus-apex_stats.csv}",
+    ],
+    out: [
+        "hoststubgen_framework-minus-apex_stats.csv",
+    ],
+}
+
 java_library {
     name: "services.core-for-hoststubgen",
     installable: false, // host only jar.
@@ -135,6 +148,19 @@
     ],
 }
 
+// Extract the stats file.
+genrule {
+    name: "services.core.ravenwood.stats",
+    defaults: ["ravenwood-internal-only-visibility-genrule"],
+    cmd: "cp $(in) $(out)",
+    srcs: [
+        ":services.core.ravenwood-base{hoststubgen_services.core_stats.csv}",
+    ],
+    out: [
+        "hoststubgen_services.core_stats.csv",
+    ],
+}
+
 java_library {
     name: "services.core.ravenwood-jarjar",
     installable: false,
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index d0a1b02..154b2d7 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -290,6 +290,8 @@
 
     // List of alarms per uid deferred due to user applied background restrictions on the source app
     SparseArray<ArrayList<Alarm>> mPendingBackgroundAlarms = new SparseArray<>();
+
+    private boolean mStartUserBeforeScheduledAlarms;
     private long mNextWakeup;
     private long mNextNonWakeup;
     private long mNextWakeUpSetAt;
@@ -1382,6 +1384,7 @@
     @GuardedBy("mLock")
     AlarmStore mAlarmStore;
 
+    UserWakeupStore mUserWakeupStore;
     // set to non-null if in idle mode; while in this mode, any alarms we don't want
     // to run during this time are rescehduled to go off after this alarm.
     Alarm mPendingIdleUntil = null;
@@ -1882,6 +1885,7 @@
         mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
 
         mUseFrozenStateToDropListenerAlarms = Flags.useFrozenStateToDropListenerAlarms();
+        mStartUserBeforeScheduledAlarms = Flags.startUserBeforeScheduledAlarms();
         if (mUseFrozenStateToDropListenerAlarms) {
             final ActivityManager.UidFrozenStateChangedCallback callback = (uids, frozenStates) -> {
                 final int size = frozenStates.length;
@@ -2000,6 +2004,10 @@
                 Slog.w(TAG, "Failed to open alarm driver. Falling back to a handler.");
             }
         }
+        if (mStartUserBeforeScheduledAlarms) {
+            mUserWakeupStore = new UserWakeupStore();
+            mUserWakeupStore.init();
+        }
         publishLocalService(AlarmManagerInternal.class, new LocalService());
         publishBinderService(Context.ALARM_SERVICE, mService);
     }
@@ -2041,6 +2049,9 @@
     public void onUserStarting(TargetUser user) {
         super.onUserStarting(user);
         final int userId = user.getUserIdentifier();
+        if (mStartUserBeforeScheduledAlarms) {
+            mUserWakeupStore.onUserStarting(userId);
+        }
         mHandler.post(() -> {
             for (final int appId : mExactAlarmCandidates) {
                 final int uid = UserHandle.getUid(userId, appId);
@@ -3150,6 +3161,9 @@
             pw.increaseIndent();
             pw.print(Flags.FLAG_USE_FROZEN_STATE_TO_DROP_LISTENER_ALARMS,
                     mUseFrozenStateToDropListenerAlarms);
+            pw.println();
+            pw.print(Flags.FLAG_START_USER_BEFORE_SCHEDULED_ALARMS,
+                    mStartUserBeforeScheduledAlarms);
             pw.decreaseIndent();
             pw.println();
             pw.println();
@@ -3398,6 +3412,12 @@
             pw.println("]");
             pw.println();
 
+            if (mStartUserBeforeScheduledAlarms) {
+                pw.println("Scheduled user wakeups:");
+                mUserWakeupStore.dump(pw, nowELAPSED);
+                pw.println();
+            }
+
             pw.println("App Alarm history:");
             mAppWakeupHistory.dump(pw, nowELAPSED);
 
@@ -3945,10 +3965,19 @@
                         formatNextAlarm(getContext(), alarmClock, userId));
             }
             mNextAlarmClockForUser.put(userId, alarmClock);
+            if (mStartUserBeforeScheduledAlarms) {
+                mUserWakeupStore.addUserWakeup(userId, convertToElapsed(
+                        mNextAlarmClockForUser.get(userId).getTriggerTime(), RTC));
+            }
         } else {
             if (DEBUG_ALARM_CLOCK) {
                 Log.v(TAG, "Next AlarmClockInfoForUser(" + userId + "): None");
             }
+            if (mStartUserBeforeScheduledAlarms) {
+                if (mActivityManagerInternal.isUserRunning(userId, 0)) {
+                    mUserWakeupStore.removeUserWakeup(userId);
+                }
+            }
             mNextAlarmClockForUser.remove(userId);
         }
 
@@ -4003,13 +4032,20 @@
                 DateFormat.format(pattern, info.getTriggerTime()).toString();
     }
 
+    @GuardedBy("mLock")
     void rescheduleKernelAlarmsLocked() {
         // Schedule the next upcoming wakeup alarm.  If there is a deliverable batch
         // prior to that which contains no wakeups, we schedule that as well.
         final long nowElapsed = mInjector.getElapsedRealtimeMillis();
         long nextNonWakeup = 0;
         if (mAlarmStore.size() > 0) {
-            final long firstWakeup = mAlarmStore.getNextWakeupDeliveryTime();
+            long firstWakeup = mAlarmStore.getNextWakeupDeliveryTime();
+            if (mStartUserBeforeScheduledAlarms) {
+                final long firstUserWakeup = mUserWakeupStore.getNextWakeupTime();
+                if (firstUserWakeup >= 0 && firstUserWakeup < firstWakeup) {
+                    firstWakeup = firstUserWakeup;
+                }
+            }
             final long first = mAlarmStore.getNextDeliveryTime();
             if (firstWakeup != 0) {
                 mNextWakeup = firstWakeup;
@@ -4716,6 +4752,16 @@
                                             + ", elapsed=" + nowELAPSED);
                         }
 
+                        if (mStartUserBeforeScheduledAlarms) {
+                            final int[] userIds =
+                                    mUserWakeupStore.getUserIdsToWakeup(nowELAPSED);
+                            for (int i = 0; i < userIds.length; i++) {
+                                if (!mActivityManagerInternal.startUserInBackground(
+                                        userIds[i])) {
+                                    mUserWakeupStore.removeUserWakeup(userIds[i]);
+                                }
+                            }
+                        }
                         mLastTrigger = nowELAPSED;
                         final int wakeUps = triggerAlarmsLocked(triggerList, nowELAPSED);
                         if (wakeUps == 0 && checkAllowNonWakeupDelayLocked(nowELAPSED)) {
@@ -5164,6 +5210,10 @@
             IntentFilter sdFilter = new IntentFilter();
             sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
             sdFilter.addAction(Intent.ACTION_USER_STOPPED);
+            if (mStartUserBeforeScheduledAlarms) {
+                sdFilter.addAction(Intent.ACTION_LOCKED_BOOT_COMPLETED);
+                sdFilter.addAction(Intent.ACTION_USER_REMOVED);
+            }
             sdFilter.addAction(Intent.ACTION_UID_REMOVED);
             getContext().registerReceiverForAllUsers(this, sdFilter,
                     /* broadcastPermission */ null, /* scheduler */ null);
@@ -5194,6 +5244,22 @@
                             mTemporaryQuotaReserve.removeForUser(userHandle);
                         }
                         return;
+                    case Intent.ACTION_LOCKED_BOOT_COMPLETED:
+                        final int handle = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
+                        if (handle >= 0) {
+                            if (mStartUserBeforeScheduledAlarms) {
+                                mUserWakeupStore.onUserStarted(handle);
+                            }
+                        }
+                        return;
+                    case Intent.ACTION_USER_REMOVED:
+                        final int user = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
+                        if (user >= 0) {
+                            if (mStartUserBeforeScheduledAlarms) {
+                                mUserWakeupStore.onUserRemoved(user);
+                            }
+                        }
+                        return;
                     case Intent.ACTION_UID_REMOVED:
                         mLastPriorityAlarmDispatch.delete(uid);
                         mRemovalHistory.delete(uid);
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java b/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java
new file mode 100644
index 0000000..a0d9133
--- /dev/null
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/UserWakeupStore.java
@@ -0,0 +1,381 @@
+/*
+ * 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.alarm;
+
+
+import android.annotation.Nullable;
+import android.os.Environment;
+import android.os.SystemClock;
+import android.util.AtomicFile;
+import android.util.IndentingPrintWriter;
+import android.util.Pair;
+import android.util.Slog;
+import android.util.SparseLongArray;
+import android.util.TimeUtils;
+import android.util.Xml;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.BackgroundThread;
+import com.android.internal.util.FastXmlSerializer;
+import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * User wakeup store keeps the list of user ids with the times that user needs to be started in
+ * sorted list in order for alarms to execute even if user gets stopped.
+ * The list of user ids with at least one alarms scheduled is also persisted to the XML file to
+ * start them after the device reboot.
+ */
+public class UserWakeupStore {
+    private static final boolean DEBUG = false;
+
+    static final String USER_WAKEUP_TAG = UserWakeupStore.class.getSimpleName();
+    private static final String TAG_USERS = "users";
+    private static final String TAG_USER = "user";
+    private static final String ATTR_USER_ID = "user_id";
+    private static final String ATTR_VERSION = "version";
+
+    public static final int XML_VERSION_CURRENT = 1;
+    @VisibleForTesting
+    static final String ROOT_DIR_NAME = "alarms";
+    @VisibleForTesting
+    static final String USERS_FILE_NAME = "usersWithAlarmClocks.xml";
+
+    /**
+     * Time offset of user start before the original alarm time in milliseconds.
+     * Also used to schedule user start after reboot to avoid starting them simultaneously.
+     */
+    @VisibleForTesting
+    static final long BUFFER_TIME_MS = TimeUnit.SECONDS.toMillis(30);
+    /**
+     * Maximum time deviation limit to introduce a 5-second time window for user starts.
+     */
+    @VisibleForTesting
+    static final long USER_START_TIME_DEVIATION_LIMIT_MS = TimeUnit.SECONDS.toMillis(5);
+    /**
+     * Delay between two consecutive user starts scheduled during user wakeup store initialization.
+     */
+    @VisibleForTesting
+    static final long INITIAL_USER_START_SCHEDULING_DELAY_MS = TimeUnit.SECONDS.toMillis(5);
+
+    private final Object mUserWakeupLock = new Object();
+
+    /**
+     * A list of wakeups for users with scheduled alarms.
+     */
+    @GuardedBy("mUserWakeupLock")
+    private final SparseLongArray mUserStarts = new SparseLongArray();
+    /**
+     * A list of users that are in a phase after they have been started but before alarms were
+     * initialized.
+     */
+    @GuardedBy("mUserWakeupLock")
+    private final SparseLongArray mStartingUsers = new SparseLongArray();
+    private Executor mBackgroundExecutor;
+    private static final File USER_WAKEUP_DIR = new File(Environment.getDataSystemDirectory(),
+            ROOT_DIR_NAME);
+    private static final Random sRandom = new Random(500);
+
+    /**
+     * Initialize mUserWakeups with persisted values.
+     */
+    public void init() {
+        mBackgroundExecutor = BackgroundThread.getExecutor();
+        mBackgroundExecutor.execute(this::readUserIdList);
+    }
+
+    /**
+     * Add user wakeup for the alarm.
+     * @param userId Id of the user that scheduled alarm.
+     * @param alarmTime time when alarm is expected to trigger.
+     */
+    public void addUserWakeup(int userId, long alarmTime) {
+        synchronized (mUserWakeupLock) {
+            // This should not be needed, but if an app in the user is scheduling an alarm clock, we
+            // consider the user start complete. There is a dedicated removal when user is started.
+            mStartingUsers.delete(userId);
+            mUserStarts.put(userId, alarmTime - BUFFER_TIME_MS + getUserWakeupOffset());
+        }
+        updateUserListFile();
+    }
+
+    /**
+     * Remove wakeup scheduled for the user with given userId if present.
+     */
+    public void removeUserWakeup(int userId) {
+        synchronized (mUserWakeupLock) {
+            mUserStarts.delete(userId);
+        }
+        updateUserListFile();
+    }
+
+    /**
+     * Get ids of users that need to be started now.
+     * @param nowElapsed current time.
+     * @return user ids to be started, or empty if no user needs to be started.
+     */
+    public int[] getUserIdsToWakeup(long nowElapsed) {
+        synchronized (mUserWakeupLock) {
+            final int[] userIds = new int[mUserStarts.size()];
+            int index = 0;
+            for (int i = mUserStarts.size() - 1; i >= 0; i--) {
+                if (mUserStarts.valueAt(i) <= nowElapsed) {
+                    userIds[index++] = mUserStarts.keyAt(i);
+                }
+            }
+            return Arrays.copyOfRange(userIds, 0, index);
+        }
+    }
+
+    /**
+     * Persist user ids that have alarms scheduled so that they can be started after device reboot.
+     */
+    private void updateUserListFile() {
+        mBackgroundExecutor.execute(() -> {
+            try {
+                writeUserIdList();
+                if (DEBUG) {
+                    synchronized (mUserWakeupLock) {
+                        Slog.i(USER_WAKEUP_TAG, "Printing out user wakeups " + mUserStarts.size());
+                        for (int i = 0; i < mUserStarts.size(); i++) {
+                            Slog.i(USER_WAKEUP_TAG, "User id: " + mUserStarts.keyAt(i) + "  time: "
+                                    + mUserStarts.valueAt(i));
+                        }
+                    }
+                }
+            } catch (Exception e) {
+                Slog.e(USER_WAKEUP_TAG, "Failed to write " + e.getLocalizedMessage());
+            }
+        });
+    }
+
+    /**
+     * Return scheduled start time for user or -1 if user does not have alarm set.
+     */
+    @VisibleForTesting
+    long getWakeupTimeForUserForTest(int userId) {
+        synchronized (mUserWakeupLock) {
+            return mUserStarts.get(userId, -1);
+        }
+    }
+
+    /**
+     * Move user from wakeup list to starting user list.
+     */
+    public void onUserStarting(int userId) {
+        synchronized (mUserWakeupLock) {
+            mStartingUsers.put(userId, getWakeupTimeForUserForTest(userId));
+            mUserStarts.delete(userId);
+        }
+    }
+
+    /**
+     * Remove userId from starting user list once start is complete.
+     */
+    public void onUserStarted(int userId) {
+        synchronized (mUserWakeupLock) {
+            mStartingUsers.delete(userId);
+        }
+        updateUserListFile();
+    }
+
+    /**
+     * Remove userId from the store when the user is removed.
+     */
+    public void onUserRemoved(int userId) {
+        synchronized (mUserWakeupLock) {
+            mUserStarts.delete(userId);
+            mStartingUsers.delete(userId);
+        }
+        updateUserListFile();
+    }
+
+    /**
+     * Get the soonest wakeup time in the store.
+     */
+    public long getNextWakeupTime() {
+        long nextWakeupTime = -1;
+        synchronized (mUserWakeupLock) {
+            for (int i = 0; i < mUserStarts.size(); i++) {
+                if (mUserStarts.valueAt(i) < nextWakeupTime || nextWakeupTime == -1) {
+                    nextWakeupTime = mUserStarts.valueAt(i);
+                }
+            }
+        }
+        return nextWakeupTime;
+    }
+
+    private static long getUserWakeupOffset() {
+        return sRandom.nextLong(USER_START_TIME_DEVIATION_LIMIT_MS * 2)
+                - USER_START_TIME_DEVIATION_LIMIT_MS;
+    }
+
+    /**
+     * Write a list of ids for users who have alarm scheduled.
+     * Sample XML file:
+     *
+     * <?xml version='1.0' encoding='utf-8' standalone='yes' ?>
+     * <users version="1">
+     * <user user_id="12" />
+     * <user user_id="10" />
+     * </users>
+     * ~
+     */
+    private void writeUserIdList() {
+        final AtomicFile file = getUserWakeupFile();
+        if (file == null) {
+            return;
+        }
+        try (FileOutputStream fos = file.startWrite(SystemClock.uptimeMillis())) {
+            final XmlSerializer out = new FastXmlSerializer();
+            out.setOutput(fos, StandardCharsets.UTF_8.name());
+            out.startDocument(null, true);
+            out.startTag(null, TAG_USERS);
+            XmlUtils.writeIntAttribute(out, ATTR_VERSION, XML_VERSION_CURRENT);
+            final List<Pair<Integer, Long>> listOfUsers = new ArrayList<>();
+            synchronized (mUserWakeupLock) {
+                for (int i = 0; i < mUserStarts.size(); i++) {
+                    listOfUsers.add(new Pair<>(mUserStarts.keyAt(i), mUserStarts.valueAt(i)));
+                }
+                for (int i = 0; i < mStartingUsers.size(); i++) {
+                    listOfUsers.add(new Pair<>(mStartingUsers.keyAt(i), mStartingUsers.valueAt(i)));
+                }
+            }
+            Collections.sort(listOfUsers, Comparator.comparingLong(pair -> pair.second));
+            for (int i = 0; i < listOfUsers.size(); i++) {
+                out.startTag(null, TAG_USER);
+                XmlUtils.writeIntAttribute(out, ATTR_USER_ID, listOfUsers.get(i).first);
+                out.endTag(null, TAG_USER);
+            }
+            out.endTag(null, TAG_USERS);
+            out.endDocument();
+            file.finishWrite(fos);
+        } catch (IOException e) {
+            Slog.wtf(USER_WAKEUP_TAG, "Error writing user wakeup data", e);
+            file.delete();
+        }
+    }
+
+    private void readUserIdList() {
+        final AtomicFile userWakeupFile = getUserWakeupFile();
+        if (userWakeupFile == null) {
+            return;
+        } else if (!userWakeupFile.exists()) {
+            Slog.w(USER_WAKEUP_TAG, "User wakeup file not available: "
+                    + userWakeupFile.getBaseFile());
+            return;
+        }
+        synchronized (mUserWakeupLock) {
+            mUserStarts.clear();
+            mStartingUsers.clear();
+        }
+        try (FileInputStream fis = userWakeupFile.openRead()) {
+            final TypedXmlPullParser parser = Xml.resolvePullParser(fis);
+            int type;
+            while ((type = parser.next()) != XmlPullParser.START_TAG
+                    && type != XmlPullParser.END_DOCUMENT) {
+                // Skip
+            }
+            if (type != XmlPullParser.START_TAG) {
+                Slog.e(USER_WAKEUP_TAG, "Unable to read user list. No start tag found in "
+                        + userWakeupFile.getBaseFile());
+                return;
+            }
+            int version = -1;
+            if (parser.getName().equals(TAG_USERS)) {
+                version = parser.getAttributeInt(null, ATTR_VERSION, version);
+            }
+
+            long counter = 0;
+            final long currentTime = SystemClock.elapsedRealtime();
+            // Time delay between now and first user wakeup is scheduled.
+            final long scheduleOffset = currentTime + BUFFER_TIME_MS + getUserWakeupOffset();
+            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
+                if (type == XmlPullParser.START_TAG) {
+                    if (parser.getName().equals(TAG_USER)) {
+                        final int id = parser.getAttributeInt(null, ATTR_USER_ID);
+                        synchronized (mUserWakeupLock) {
+                            mUserStarts.put(id, scheduleOffset + (counter++
+                                    * INITIAL_USER_START_SCHEDULING_DELAY_MS));
+                        }
+                    }
+                }
+            }
+        } catch (IOException | XmlPullParserException e) {
+            Slog.wtf(USER_WAKEUP_TAG, "Error reading user wakeup data", e);
+        }
+    }
+
+    @Nullable
+    private AtomicFile getUserWakeupFile() {
+        if (!USER_WAKEUP_DIR.exists() && !USER_WAKEUP_DIR.mkdir()) {
+            Slog.wtf(USER_WAKEUP_TAG, "Failed to mkdir() user list file: " + USER_WAKEUP_DIR);
+            return null;
+        }
+        final File userFile = new File(USER_WAKEUP_DIR, USERS_FILE_NAME);
+        return new AtomicFile(userFile);
+    }
+
+    void dump(IndentingPrintWriter pw, long nowELAPSED) {
+        synchronized (mUserWakeupLock) {
+            pw.increaseIndent();
+            pw.print("User wakeup store file path: ");
+            final AtomicFile file = getUserWakeupFile();
+            if (file == null) {
+                pw.println("null");
+            } else {
+                pw.println(file.getBaseFile().getAbsolutePath());
+            }
+            pw.println(mUserStarts.size() + " user wakeups scheduled: ");
+            for (int i = 0; i < mUserStarts.size(); i++) {
+                pw.print("UserId: ");
+                pw.print(mUserStarts.keyAt(i));
+                pw.print(", userStartTime: ");
+                TimeUtils.formatDuration(mUserStarts.valueAt(i), nowELAPSED, pw);
+                pw.println();
+            }
+            pw.println(mStartingUsers.size() + " starting users: ");
+            for (int i = 0; i < mStartingUsers.size(); i++) {
+                pw.print("UserId: ");
+                pw.print(mStartingUsers.keyAt(i));
+                pw.print(", userStartTime: ");
+                TimeUtils.formatDuration(mStartingUsers.valueAt(i), nowELAPSED, pw);
+                pw.println();
+            }
+            pw.decreaseIndent();
+        }
+    }
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
index b897c1a..88a3c6f 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -304,6 +304,8 @@
     private final ConnectivityController mConnectivityController;
     /** Need directly for sending uid state changes */
     private final DeviceIdleJobsController mDeviceIdleJobsController;
+    /** Need direct access to this for testing. */
+    private final FlexibilityController mFlexibilityController;
     /** Needed to get next estimated launch time. */
     private final PrefetchController mPrefetchController;
     /** Needed to get remaining quota time. */
@@ -2701,17 +2703,16 @@
         mControllers = new ArrayList<StateController>();
         mPrefetchController = new PrefetchController(this);
         mControllers.add(mPrefetchController);
-        final FlexibilityController flexibilityController =
-                new FlexibilityController(this, mPrefetchController);
-        mControllers.add(flexibilityController);
+        mFlexibilityController = new FlexibilityController(this, mPrefetchController);
+        mControllers.add(mFlexibilityController);
         mConnectivityController =
-                new ConnectivityController(this, flexibilityController);
+                new ConnectivityController(this, mFlexibilityController);
         mControllers.add(mConnectivityController);
         mControllers.add(new TimeController(this));
-        final IdleController idleController = new IdleController(this, flexibilityController);
+        final IdleController idleController = new IdleController(this, mFlexibilityController);
         mControllers.add(idleController);
         final BatteryController batteryController =
-                new BatteryController(this, flexibilityController);
+                new BatteryController(this, mFlexibilityController);
         mControllers.add(batteryController);
         mStorageController = new StorageController(this);
         mControllers.add(mStorageController);
@@ -5561,6 +5562,15 @@
         return 0;
     }
 
+    // Shell command infrastructure: set flex policy
+    void setFlexPolicy(boolean override, int appliedConstraints) {
+        if (DEBUG) {
+            Slog.v(TAG, "setFlexPolicy(): " + override + "/" + appliedConstraints);
+        }
+
+        mFlexibilityController.setLocalPolicyForTesting(override, appliedConstraints);
+    }
+
     void setMonitorBattery(boolean enabled) {
         synchronized (mLock) {
             mBatteryStateTracker.setMonitorBatteryLocked(enabled);
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java
index af7b27e..ac240cc 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java
@@ -27,6 +27,7 @@
 import android.os.UserHandle;
 
 import com.android.modules.utils.BasicShellCommandHandler;
+import com.android.server.job.controllers.JobStatus;
 
 import java.io.PrintWriter;
 
@@ -59,6 +60,10 @@
                     return cancelJob(pw);
                 case "monitor-battery":
                     return monitorBattery(pw);
+                case "disable-flex-policy":
+                    return disableFlexPolicy(pw);
+                case "enable-flex-policy":
+                    return enableFlexPolicy(pw);
                 case "get-aconfig-flag-state":
                     return getAconfigFlagState(pw);
                 case "get-battery-seq":
@@ -91,6 +96,8 @@
                     return resetExecutionQuota(pw);
                 case "reset-schedule-quota":
                     return resetScheduleQuota(pw);
+                case "reset-flex-policy":
+                    return resetFlexPolicy(pw);
                 case "stop":
                     return stop(pw);
                 case "trigger-dock-state":
@@ -346,6 +353,65 @@
         return 0;
     }
 
+    private int disableFlexPolicy(PrintWriter pw) throws Exception {
+        checkPermission("disable flex policy");
+
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            mInternal.setFlexPolicy(true, 0);
+            pw.println("Set flex policy to 0");
+            return 0;
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
+    private int enableFlexPolicy(PrintWriter pw) throws Exception {
+        checkPermission("enable flex policy");
+
+        int enabled = 0;
+
+        String opt;
+        while ((opt = getNextOption()) != null) {
+            switch (opt) {
+                case "-o":
+                case "--option":
+                    final String constraint = getNextArgRequired();
+                    switch (constraint) {
+                        case "battery-not-low":
+                            enabled |= JobStatus.CONSTRAINT_BATTERY_NOT_LOW;
+                            break;
+                        case "charging":
+                            enabled |= JobStatus.CONSTRAINT_CHARGING;
+                            break;
+                        case "connectivity":
+                            enabled |= JobStatus.CONSTRAINT_CONNECTIVITY;
+                            break;
+                        case "idle":
+                            enabled |= JobStatus.CONSTRAINT_IDLE;
+                            break;
+                        default:
+                            pw.println("Unsupported option: " + constraint);
+                            return -1;
+                    }
+                    break;
+
+                default:
+                    pw.println("Error: unknown option '" + opt + "'");
+                    return -1;
+            }
+        }
+
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            mInternal.setFlexPolicy(true, enabled);
+            pw.println("Set flex policy to " + enabled);
+            return 0;
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
     private int getAconfigFlagState(PrintWriter pw) throws Exception {
         checkPermission("get aconfig flag state", Manifest.permission.DUMP);
 
@@ -581,6 +647,19 @@
         return 0;
     }
 
+    private int resetFlexPolicy(PrintWriter pw) throws Exception {
+        checkPermission("reset flex policy");
+
+        final long ident = Binder.clearCallingIdentity();
+        try {
+            mInternal.setFlexPolicy(false, 0);
+            pw.println("Reset flex policy to its default state");
+            return 0;
+        } finally {
+            Binder.restoreCallingIdentity(ident);
+        }
+    }
+
     private int resetExecutionQuota(PrintWriter pw) throws Exception {
         checkPermission("reset execution quota");
 
@@ -773,6 +852,15 @@
         pw.println("  monitor-battery [on|off]");
         pw.println("    Control monitoring of all battery changes.  Off by default.  Turning");
         pw.println("    on makes get-battery-seq useful.");
+        pw.println("  enable-flex-policy --option <option>");
+        pw.println("    Enable flex policy with the specified options. Supported options are");
+        pw.println("    battery-not-low, charging, connectivity, idle.");
+        pw.println("    Multiple enable options can be specified (e.g.");
+        pw.println("    enable-flex-policy --option battery-not-low --option charging");
+        pw.println("  disable-flex-policy");
+        pw.println("    Turn off flex policy so that it does not affect job execution.");
+        pw.println("  reset-flex-policy");
+        pw.println("    Resets the flex policy to its default state.");
         pw.println("  get-aconfig-flag-state FULL_FLAG_NAME");
         pw.println("    Return the state of the specified aconfig flag, if known. The flag name");
         pw.println("         must be fully qualified.");
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
index ee9400f..852b00b 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java
@@ -328,6 +328,9 @@
     @GuardedBy("mLock")
     private final ArraySet<String> mPackagesToCheck = new ArraySet<>();
 
+    @GuardedBy("mLock")
+    private boolean mLocalOverride;
+
     public FlexibilityController(
             JobSchedulerService service, PrefetchController prefetchController) {
         super(service);
@@ -1923,6 +1926,27 @@
         }
     }
 
+    /**
+     * If {@code override} is true, uses {@code appliedConstraints} for flex policy evaluation,
+     * overriding anything else that was set. If {@code override} is false, any previous calls
+     * will be discarded and the policy will be reset to the normal default policy.
+     */
+    public void setLocalPolicyForTesting(boolean override, int appliedConstraints) {
+        synchronized (mLock) {
+            final boolean recheckJobs = mLocalOverride != override
+                    || mAppliedConstraints != appliedConstraints;
+            mLocalOverride = override;
+            if (mLocalOverride) {
+                mAppliedConstraints = appliedConstraints;
+            } else {
+                mAppliedConstraints = mFcConfig.APPLIED_CONSTRAINTS;
+            }
+            if (recheckJobs) {
+                mHandler.obtainMessage(MSG_CHECK_ALL_JOBS).sendToTarget();
+            }
+        }
+    }
+
     @Override
     @GuardedBy("mLock")
     public void dumpConstants(IndentingPrintWriter pw) {
@@ -1932,6 +1956,12 @@
     @Override
     @GuardedBy("mLock")
     public void dumpControllerStateLocked(IndentingPrintWriter pw, Predicate<JobStatus> predicate) {
+        if (mLocalOverride) {
+            pw.println("Local override active");
+        }
+        pw.print("Applied Flexible Constraints:");
+        JobStatus.dumpConstraints(pw, mAppliedConstraints);
+        pw.println();
         pw.print("Satisfied Flexible Constraints:");
         JobStatus.dumpConstraints(pw, mSatisfiedFlexibleConstraints);
         pw.println();
diff --git a/core/api/current.txt b/core/api/current.txt
index b8c2d90..1cfc025 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -9242,140 +9242,140 @@
 
 package android.app.slice {
 
-  public final class Slice implements android.os.Parcelable {
-    ctor protected Slice(android.os.Parcel);
-    method public int describeContents();
-    method public java.util.List<java.lang.String> getHints();
-    method public java.util.List<android.app.slice.SliceItem> getItems();
-    method @Nullable public android.app.slice.SliceSpec getSpec();
-    method public android.net.Uri getUri();
-    method public boolean isCallerNeeded();
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.app.slice.Slice> CREATOR;
-    field public static final String EXTRA_RANGE_VALUE = "android.app.slice.extra.RANGE_VALUE";
-    field public static final String EXTRA_TOGGLE_STATE = "android.app.slice.extra.TOGGLE_STATE";
-    field public static final String HINT_ACTIONS = "actions";
-    field public static final String HINT_ERROR = "error";
-    field public static final String HINT_HORIZONTAL = "horizontal";
-    field public static final String HINT_KEYWORDS = "keywords";
-    field public static final String HINT_LARGE = "large";
-    field public static final String HINT_LAST_UPDATED = "last_updated";
-    field public static final String HINT_LIST = "list";
-    field public static final String HINT_LIST_ITEM = "list_item";
-    field public static final String HINT_NO_TINT = "no_tint";
-    field public static final String HINT_PARTIAL = "partial";
-    field public static final String HINT_PERMISSION_REQUEST = "permission_request";
-    field public static final String HINT_SEE_MORE = "see_more";
-    field public static final String HINT_SELECTED = "selected";
-    field public static final String HINT_SHORTCUT = "shortcut";
-    field public static final String HINT_SUMMARY = "summary";
-    field public static final String HINT_TITLE = "title";
-    field public static final String HINT_TTL = "ttl";
-    field public static final String SUBTYPE_COLOR = "color";
-    field public static final String SUBTYPE_CONTENT_DESCRIPTION = "content_description";
-    field public static final String SUBTYPE_LAYOUT_DIRECTION = "layout_direction";
-    field public static final String SUBTYPE_MAX = "max";
-    field public static final String SUBTYPE_MESSAGE = "message";
-    field public static final String SUBTYPE_MILLIS = "millis";
-    field public static final String SUBTYPE_PRIORITY = "priority";
-    field public static final String SUBTYPE_RANGE = "range";
-    field public static final String SUBTYPE_SOURCE = "source";
-    field public static final String SUBTYPE_TOGGLE = "toggle";
-    field public static final String SUBTYPE_VALUE = "value";
+  @Deprecated public final class Slice implements android.os.Parcelable {
+    ctor @Deprecated protected Slice(android.os.Parcel);
+    method @Deprecated public int describeContents();
+    method @Deprecated public java.util.List<java.lang.String> getHints();
+    method @Deprecated public java.util.List<android.app.slice.SliceItem> getItems();
+    method @Deprecated @Nullable public android.app.slice.SliceSpec getSpec();
+    method @Deprecated public android.net.Uri getUri();
+    method @Deprecated public boolean isCallerNeeded();
+    method @Deprecated public void writeToParcel(android.os.Parcel, int);
+    field @Deprecated @NonNull public static final android.os.Parcelable.Creator<android.app.slice.Slice> CREATOR;
+    field @Deprecated public static final String EXTRA_RANGE_VALUE = "android.app.slice.extra.RANGE_VALUE";
+    field @Deprecated public static final String EXTRA_TOGGLE_STATE = "android.app.slice.extra.TOGGLE_STATE";
+    field @Deprecated public static final String HINT_ACTIONS = "actions";
+    field @Deprecated public static final String HINT_ERROR = "error";
+    field @Deprecated public static final String HINT_HORIZONTAL = "horizontal";
+    field @Deprecated public static final String HINT_KEYWORDS = "keywords";
+    field @Deprecated public static final String HINT_LARGE = "large";
+    field @Deprecated public static final String HINT_LAST_UPDATED = "last_updated";
+    field @Deprecated public static final String HINT_LIST = "list";
+    field @Deprecated public static final String HINT_LIST_ITEM = "list_item";
+    field @Deprecated public static final String HINT_NO_TINT = "no_tint";
+    field @Deprecated public static final String HINT_PARTIAL = "partial";
+    field @Deprecated public static final String HINT_PERMISSION_REQUEST = "permission_request";
+    field @Deprecated public static final String HINT_SEE_MORE = "see_more";
+    field @Deprecated public static final String HINT_SELECTED = "selected";
+    field @Deprecated public static final String HINT_SHORTCUT = "shortcut";
+    field @Deprecated public static final String HINT_SUMMARY = "summary";
+    field @Deprecated public static final String HINT_TITLE = "title";
+    field @Deprecated public static final String HINT_TTL = "ttl";
+    field @Deprecated public static final String SUBTYPE_COLOR = "color";
+    field @Deprecated public static final String SUBTYPE_CONTENT_DESCRIPTION = "content_description";
+    field @Deprecated public static final String SUBTYPE_LAYOUT_DIRECTION = "layout_direction";
+    field @Deprecated public static final String SUBTYPE_MAX = "max";
+    field @Deprecated public static final String SUBTYPE_MESSAGE = "message";
+    field @Deprecated public static final String SUBTYPE_MILLIS = "millis";
+    field @Deprecated public static final String SUBTYPE_PRIORITY = "priority";
+    field @Deprecated public static final String SUBTYPE_RANGE = "range";
+    field @Deprecated public static final String SUBTYPE_SOURCE = "source";
+    field @Deprecated public static final String SUBTYPE_TOGGLE = "toggle";
+    field @Deprecated public static final String SUBTYPE_VALUE = "value";
   }
 
-  public static class Slice.Builder {
-    ctor public Slice.Builder(@NonNull android.net.Uri, android.app.slice.SliceSpec);
-    ctor public Slice.Builder(@NonNull android.app.slice.Slice.Builder);
-    method public android.app.slice.Slice.Builder addAction(@NonNull android.app.PendingIntent, @NonNull android.app.slice.Slice, @Nullable String);
-    method public android.app.slice.Slice.Builder addBundle(android.os.Bundle, @Nullable String, java.util.List<java.lang.String>);
-    method public android.app.slice.Slice.Builder addHints(java.util.List<java.lang.String>);
-    method public android.app.slice.Slice.Builder addIcon(android.graphics.drawable.Icon, @Nullable String, java.util.List<java.lang.String>);
-    method public android.app.slice.Slice.Builder addInt(int, @Nullable String, java.util.List<java.lang.String>);
-    method public android.app.slice.Slice.Builder addLong(long, @Nullable String, java.util.List<java.lang.String>);
-    method public android.app.slice.Slice.Builder addRemoteInput(android.app.RemoteInput, @Nullable String, java.util.List<java.lang.String>);
-    method public android.app.slice.Slice.Builder addSubSlice(@NonNull android.app.slice.Slice, @Nullable String);
-    method public android.app.slice.Slice.Builder addText(CharSequence, @Nullable String, java.util.List<java.lang.String>);
-    method public android.app.slice.Slice build();
-    method public android.app.slice.Slice.Builder setCallerNeeded(boolean);
+  @Deprecated public static class Slice.Builder {
+    ctor @Deprecated public Slice.Builder(@NonNull android.net.Uri, android.app.slice.SliceSpec);
+    ctor @Deprecated public Slice.Builder(@NonNull android.app.slice.Slice.Builder);
+    method @Deprecated public android.app.slice.Slice.Builder addAction(@NonNull android.app.PendingIntent, @NonNull android.app.slice.Slice, @Nullable String);
+    method @Deprecated public android.app.slice.Slice.Builder addBundle(android.os.Bundle, @Nullable String, java.util.List<java.lang.String>);
+    method @Deprecated public android.app.slice.Slice.Builder addHints(java.util.List<java.lang.String>);
+    method @Deprecated public android.app.slice.Slice.Builder addIcon(android.graphics.drawable.Icon, @Nullable String, java.util.List<java.lang.String>);
+    method @Deprecated public android.app.slice.Slice.Builder addInt(int, @Nullable String, java.util.List<java.lang.String>);
+    method @Deprecated public android.app.slice.Slice.Builder addLong(long, @Nullable String, java.util.List<java.lang.String>);
+    method @Deprecated public android.app.slice.Slice.Builder addRemoteInput(android.app.RemoteInput, @Nullable String, java.util.List<java.lang.String>);
+    method @Deprecated public android.app.slice.Slice.Builder addSubSlice(@NonNull android.app.slice.Slice, @Nullable String);
+    method @Deprecated public android.app.slice.Slice.Builder addText(CharSequence, @Nullable String, java.util.List<java.lang.String>);
+    method @Deprecated public android.app.slice.Slice build();
+    method @Deprecated public android.app.slice.Slice.Builder setCallerNeeded(boolean);
   }
 
-  public final class SliceItem implements android.os.Parcelable {
-    method public int describeContents();
-    method public android.app.PendingIntent getAction();
-    method public android.os.Bundle getBundle();
-    method public String getFormat();
-    method @NonNull public java.util.List<java.lang.String> getHints();
-    method public android.graphics.drawable.Icon getIcon();
-    method public int getInt();
-    method public long getLong();
-    method public android.app.RemoteInput getRemoteInput();
-    method public android.app.slice.Slice getSlice();
-    method public String getSubType();
-    method public CharSequence getText();
-    method public boolean hasHint(String);
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.app.slice.SliceItem> CREATOR;
-    field public static final String FORMAT_ACTION = "action";
-    field public static final String FORMAT_BUNDLE = "bundle";
-    field public static final String FORMAT_IMAGE = "image";
-    field public static final String FORMAT_INT = "int";
-    field public static final String FORMAT_LONG = "long";
-    field public static final String FORMAT_REMOTE_INPUT = "input";
-    field public static final String FORMAT_SLICE = "slice";
-    field public static final String FORMAT_TEXT = "text";
+  @Deprecated public final class SliceItem implements android.os.Parcelable {
+    method @Deprecated public int describeContents();
+    method @Deprecated public android.app.PendingIntent getAction();
+    method @Deprecated public android.os.Bundle getBundle();
+    method @Deprecated public String getFormat();
+    method @Deprecated @NonNull public java.util.List<java.lang.String> getHints();
+    method @Deprecated public android.graphics.drawable.Icon getIcon();
+    method @Deprecated public int getInt();
+    method @Deprecated public long getLong();
+    method @Deprecated public android.app.RemoteInput getRemoteInput();
+    method @Deprecated public android.app.slice.Slice getSlice();
+    method @Deprecated public String getSubType();
+    method @Deprecated public CharSequence getText();
+    method @Deprecated public boolean hasHint(String);
+    method @Deprecated public void writeToParcel(android.os.Parcel, int);
+    field @Deprecated @NonNull public static final android.os.Parcelable.Creator<android.app.slice.SliceItem> CREATOR;
+    field @Deprecated public static final String FORMAT_ACTION = "action";
+    field @Deprecated public static final String FORMAT_BUNDLE = "bundle";
+    field @Deprecated public static final String FORMAT_IMAGE = "image";
+    field @Deprecated public static final String FORMAT_INT = "int";
+    field @Deprecated public static final String FORMAT_LONG = "long";
+    field @Deprecated public static final String FORMAT_REMOTE_INPUT = "input";
+    field @Deprecated public static final String FORMAT_SLICE = "slice";
+    field @Deprecated public static final String FORMAT_TEXT = "text";
   }
 
-  public class SliceManager {
-    method @Nullable public android.app.slice.Slice bindSlice(@NonNull android.net.Uri, @NonNull java.util.Set<android.app.slice.SliceSpec>);
-    method @Nullable public android.app.slice.Slice bindSlice(@NonNull android.content.Intent, @NonNull java.util.Set<android.app.slice.SliceSpec>);
-    method public int checkSlicePermission(@NonNull android.net.Uri, int, int);
-    method @NonNull public java.util.List<android.net.Uri> getPinnedSlices();
-    method @NonNull public java.util.Set<android.app.slice.SliceSpec> getPinnedSpecs(android.net.Uri);
-    method @NonNull @WorkerThread public java.util.Collection<android.net.Uri> getSliceDescendants(@NonNull android.net.Uri);
-    method public void grantSlicePermission(@NonNull String, @NonNull android.net.Uri);
-    method @Nullable public android.net.Uri mapIntentToUri(@NonNull android.content.Intent);
-    method public void pinSlice(@NonNull android.net.Uri, @NonNull java.util.Set<android.app.slice.SliceSpec>);
-    method public void revokeSlicePermission(@NonNull String, @NonNull android.net.Uri);
-    method public void unpinSlice(@NonNull android.net.Uri);
-    field public static final String CATEGORY_SLICE = "android.app.slice.category.SLICE";
-    field public static final String SLICE_METADATA_KEY = "android.metadata.SLICE_URI";
+  @Deprecated public class SliceManager {
+    method @Deprecated @Nullable public android.app.slice.Slice bindSlice(@NonNull android.net.Uri, @NonNull java.util.Set<android.app.slice.SliceSpec>);
+    method @Deprecated @Nullable public android.app.slice.Slice bindSlice(@NonNull android.content.Intent, @NonNull java.util.Set<android.app.slice.SliceSpec>);
+    method @Deprecated public int checkSlicePermission(@NonNull android.net.Uri, int, int);
+    method @Deprecated @NonNull public java.util.List<android.net.Uri> getPinnedSlices();
+    method @Deprecated @NonNull public java.util.Set<android.app.slice.SliceSpec> getPinnedSpecs(android.net.Uri);
+    method @Deprecated @NonNull @WorkerThread public java.util.Collection<android.net.Uri> getSliceDescendants(@NonNull android.net.Uri);
+    method @Deprecated public void grantSlicePermission(@NonNull String, @NonNull android.net.Uri);
+    method @Deprecated @Nullable public android.net.Uri mapIntentToUri(@NonNull android.content.Intent);
+    method @Deprecated public void pinSlice(@NonNull android.net.Uri, @NonNull java.util.Set<android.app.slice.SliceSpec>);
+    method @Deprecated public void revokeSlicePermission(@NonNull String, @NonNull android.net.Uri);
+    method @Deprecated public void unpinSlice(@NonNull android.net.Uri);
+    field @Deprecated public static final String CATEGORY_SLICE = "android.app.slice.category.SLICE";
+    field @Deprecated public static final String SLICE_METADATA_KEY = "android.metadata.SLICE_URI";
   }
 
-  public class SliceMetrics {
-    ctor public SliceMetrics(@NonNull android.content.Context, @NonNull android.net.Uri);
-    method public void logHidden();
-    method public void logTouch(int, @NonNull android.net.Uri);
-    method public void logVisible();
+  @Deprecated public class SliceMetrics {
+    ctor @Deprecated public SliceMetrics(@NonNull android.content.Context, @NonNull android.net.Uri);
+    method @Deprecated public void logHidden();
+    method @Deprecated public void logTouch(int, @NonNull android.net.Uri);
+    method @Deprecated public void logVisible();
   }
 
-  public abstract class SliceProvider extends android.content.ContentProvider {
-    ctor public SliceProvider(@NonNull java.lang.String...);
-    ctor public SliceProvider();
-    method public final int delete(android.net.Uri, String, String[]);
-    method public final String getType(android.net.Uri);
-    method public final android.net.Uri insert(android.net.Uri, android.content.ContentValues);
-    method public android.app.slice.Slice onBindSlice(android.net.Uri, java.util.Set<android.app.slice.SliceSpec>);
-    method @NonNull public android.app.PendingIntent onCreatePermissionRequest(android.net.Uri);
-    method @NonNull public java.util.Collection<android.net.Uri> onGetSliceDescendants(@NonNull android.net.Uri);
-    method @NonNull public android.net.Uri onMapIntentToUri(android.content.Intent);
-    method public void onSlicePinned(android.net.Uri);
-    method public void onSliceUnpinned(android.net.Uri);
-    method public final android.database.Cursor query(android.net.Uri, String[], String, String[], String);
-    method public final android.database.Cursor query(android.net.Uri, String[], String, String[], String, android.os.CancellationSignal);
-    method public final android.database.Cursor query(android.net.Uri, String[], android.os.Bundle, android.os.CancellationSignal);
-    method public final int update(android.net.Uri, android.content.ContentValues, String, String[]);
-    field public static final String SLICE_TYPE = "vnd.android.slice";
+  @Deprecated public abstract class SliceProvider extends android.content.ContentProvider {
+    ctor @Deprecated public SliceProvider(@NonNull java.lang.String...);
+    ctor @Deprecated public SliceProvider();
+    method @Deprecated public final int delete(android.net.Uri, String, String[]);
+    method @Deprecated public final String getType(android.net.Uri);
+    method @Deprecated public final android.net.Uri insert(android.net.Uri, android.content.ContentValues);
+    method @Deprecated public android.app.slice.Slice onBindSlice(android.net.Uri, java.util.Set<android.app.slice.SliceSpec>);
+    method @Deprecated @NonNull public android.app.PendingIntent onCreatePermissionRequest(android.net.Uri);
+    method @Deprecated @NonNull public java.util.Collection<android.net.Uri> onGetSliceDescendants(@NonNull android.net.Uri);
+    method @Deprecated @NonNull public android.net.Uri onMapIntentToUri(android.content.Intent);
+    method @Deprecated public void onSlicePinned(android.net.Uri);
+    method @Deprecated public void onSliceUnpinned(android.net.Uri);
+    method @Deprecated public final android.database.Cursor query(android.net.Uri, String[], String, String[], String);
+    method @Deprecated public final android.database.Cursor query(android.net.Uri, String[], String, String[], String, android.os.CancellationSignal);
+    method @Deprecated public final android.database.Cursor query(android.net.Uri, String[], android.os.Bundle, android.os.CancellationSignal);
+    method @Deprecated public final int update(android.net.Uri, android.content.ContentValues, String, String[]);
+    field @Deprecated public static final String SLICE_TYPE = "vnd.android.slice";
   }
 
-  public final class SliceSpec implements android.os.Parcelable {
-    ctor public SliceSpec(@NonNull String, int);
-    method public boolean canRender(@NonNull android.app.slice.SliceSpec);
-    method public int describeContents();
-    method public int getRevision();
-    method public String getType();
-    method public void writeToParcel(android.os.Parcel, int);
-    field @NonNull public static final android.os.Parcelable.Creator<android.app.slice.SliceSpec> CREATOR;
+  @Deprecated public final class SliceSpec implements android.os.Parcelable {
+    ctor @Deprecated public SliceSpec(@NonNull String, int);
+    method @Deprecated public boolean canRender(@NonNull android.app.slice.SliceSpec);
+    method @Deprecated public int describeContents();
+    method @Deprecated public int getRevision();
+    method @Deprecated public String getType();
+    method @Deprecated public void writeToParcel(android.os.Parcel, int);
+    field @Deprecated @NonNull public static final android.os.Parcelable.Creator<android.app.slice.SliceSpec> CREATOR;
   }
 
 }
@@ -17982,7 +17982,6 @@
     method @NonNull public android.graphics.fonts.FontFamily.Builder addFont(@NonNull android.graphics.fonts.Font);
     method @NonNull public android.graphics.fonts.FontFamily build();
     method @FlaggedApi("com.android.text.flags.new_fonts_fallback_xml") @Nullable public android.graphics.fonts.FontFamily buildVariableFamily();
-    method @FlaggedApi("com.android.text.flags.new_fonts_fallback_xml") public boolean canBuildVariableFamily();
   }
 
   public final class FontStyle {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 5547028..501203e 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -2186,7 +2186,7 @@
     field @NonNull public static final android.os.Parcelable.Creator<android.app.contextualsearch.CallbackToken> CREATOR;
   }
 
-  @FlaggedApi("android.app.contextualsearch.flags.enable_service") public class ContextualSearchManager {
+  @FlaggedApi("android.app.contextualsearch.flags.enable_service") public final class ContextualSearchManager {
     method @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXTUAL_SEARCH) public void startContextualSearch(int);
     field public static final String ACTION_LAUNCH_CONTEXTUAL_SEARCH = "android.app.contextualsearch.action.LAUNCH_CONTEXTUAL_SEARCH";
     field public static final int ENTRYPOINT_LONG_PRESS_HOME = 2; // 0x2
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index a73aa71..ca5d3eb 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -3977,6 +3977,7 @@
 
   public final class InputMethodManager {
     method @RequiresPermission(android.Manifest.permission.TEST_INPUT_METHOD) public void addVirtualStylusIdForTestSession();
+    method @RequiresPermission(android.Manifest.permission.TEST_INPUT_METHOD) public void finishTrackingPendingImeVisibilityRequests();
     method public int getDisplayId();
     method @FlaggedApi("android.view.inputmethod.imm_userhandle_hostsidetests") @NonNull @RequiresPermission(value=android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional=true) public java.util.List<android.view.inputmethod.InputMethodInfo> getEnabledInputMethodListAsUser(@NonNull android.os.UserHandle);
     method @FlaggedApi("android.view.inputmethod.imm_userhandle_hostsidetests") @NonNull @RequiresPermission(value=android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional=true) public java.util.List<android.view.inputmethod.InputMethodSubtype> getEnabledInputMethodSubtypeListAsUser(@NonNull String, boolean, @NonNull android.os.UserHandle);
diff --git a/core/java/android/accessibilityservice/BrailleDisplayController.java b/core/java/android/accessibilityservice/BrailleDisplayController.java
index 5282aa3..7334676 100644
--- a/core/java/android/accessibilityservice/BrailleDisplayController.java
+++ b/core/java/android/accessibilityservice/BrailleDisplayController.java
@@ -305,4 +305,6 @@
     @FlaggedApi(Flags.FLAG_BRAILLE_DISPLAY_HID)
     @TestApi
     String TEST_BRAILLE_DISPLAY_UNIQUE_ID = "UNIQUE_ID";
+    /** @hide */
+    String TEST_BRAILLE_DISPLAY_NAME = "NAME";
 }
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index 5843e51..e66f7fe 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -148,6 +148,13 @@
     public abstract void onUserRemoved(@UserIdInt int userId);
 
     /**
+     * Start user, if it is not already running, but don't bring it to foreground.
+     * @param userId ID of the user to start
+     * @return true if the user has been successfully started
+     */
+    public abstract boolean startUserInBackground(int userId);
+
+    /**
      * Kill foreground apps from the specified user.
      */
     public abstract void killForegroundAppsForUser(@UserIdInt int userId);
diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl
index 5e6b54b..84bc6ce 100644
--- a/core/java/android/app/IActivityManager.aidl
+++ b/core/java/android/app/IActivityManager.aidl
@@ -962,4 +962,12 @@
      */
     oneway void frozenBinderTransactionDetected(int debugPid, int code, int flags, int err);
     int getBindingUidProcessState(int uid, in String callingPackage);
+
+    /**
+     * Return the timestampe (in the elapsed timebase) when the UID became idle from active
+     * last time (regardless of if the UID is still idle, or became active again).
+     * This is useful when trying to detect whether an UID has ever became idle since a certain
+     * time in the past.
+     */
+    long getUidLastIdleElapsedTime(int uid, in String callingPackage);
 }
diff --git a/core/java/android/app/OWNERS b/core/java/android/app/OWNERS
index 41b97d0..97c2e43 100644
--- a/core/java/android/app/OWNERS
+++ b/core/java/android/app/OWNERS
@@ -42,6 +42,7 @@
 # ActivityThread
 per-file ActivityThread.java = file:/services/core/java/com/android/server/am/OWNERS
 per-file ActivityThread.java = file:/services/core/java/com/android/server/wm/OWNERS
+per-file ActivityThread.java = file:RESOURCES_OWNERS
 
 # Alarm
 per-file *Alarm* = file:/apex/jobscheduler/OWNERS
diff --git a/core/java/android/app/RESOURCES_OWNERS b/core/java/android/app/RESOURCES_OWNERS
index 5582803..fe37c0c 100644
--- a/core/java/android/app/RESOURCES_OWNERS
+++ b/core/java/android/app/RESOURCES_OWNERS
@@ -1,3 +1,5 @@
-rtmitchell@google.com
-toddke@google.com
+zyy@google.com
+jakmcbane@google.com
+branliu@google.com
+markpun@google.com
 patb@google.com
diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java
index b1e7b62..370aff8 100644
--- a/core/java/android/app/ResourcesManager.java
+++ b/core/java/android/app/ResourcesManager.java
@@ -120,6 +120,17 @@
     private final ReferenceQueue<Resources> mResourcesReferencesQueue = new ReferenceQueue<>();
 
     /**
+     * A list of Resources references for all Resources instances created through Resources public
+     * constructor, only system Resources created by the private constructor are excluded.
+     * This addition is necessary due to certain Application Resources created by constructor
+     * directly which are not managed by ResourcesManager, hence we require a comprehensive
+     * collection of all Resources references to help with asset paths appending tasks when shared
+     * libraries are registered.
+     */
+    private final ArrayList<WeakReference<Resources>> mAllResourceReferences = new ArrayList<>();
+    private final ReferenceQueue<Resources> mAllResourceReferencesQueue = new ReferenceQueue<>();
+
+    /**
      * The localeConfig of the app.
      */
     private LocaleConfig mLocaleConfig = new LocaleConfig(LocaleList.getEmptyLocaleList());
@@ -1568,7 +1579,7 @@
                 }
             }
 
-            redirectResourcesToNewImplLocked(updatedResourceKeys);
+            redirectAllResourcesToNewImplLocked(updatedResourceKeys);
         }
     }
 
@@ -1707,6 +1718,43 @@
         }
     }
 
+    // Another redirect function which will loop through all Resources and reload ResourcesImpl
+    // if it needs a shared library asset paths update.
+    private void redirectAllResourcesToNewImplLocked(
+            @NonNull final ArrayMap<ResourcesImpl, ResourcesKey> updatedResourceKeys) {
+        cleanupReferences(mAllResourceReferences, mAllResourceReferencesQueue);
+
+        // Update any references to ResourcesImpl that require reloading.
+        final int resourcesCount = mAllResourceReferences.size();
+        for (int i = 0; i < resourcesCount; i++) {
+            final WeakReference<Resources> ref = mAllResourceReferences.get(i);
+            final Resources r = ref != null ? ref.get() : null;
+            if (r != null) {
+                final ResourcesKey key = updatedResourceKeys.get(r.getImpl());
+                if (key != null) {
+                    final ResourcesImpl impl = findOrCreateResourcesImplForKeyLocked(key);
+                    if (impl == null) {
+                        throw new Resources.NotFoundException("failed to redirect ResourcesImpl");
+                    }
+                    r.setImpl(impl);
+                } else {
+                    // ResourcesKey is null which means the ResourcesImpl could belong to a
+                    // Resources created by application through Resources constructor and was not
+                    // managed by ResourcesManager, so the ResourcesImpl needs to be recreated to
+                    // have shared library asset paths appended if there are any.
+                    if (r.getImpl() != null) {
+                        final ResourcesImpl oldImpl = r.getImpl();
+                        // ResourcesImpl constructor will help to append shared library asset paths.
+                        final ResourcesImpl newImpl = new ResourcesImpl(oldImpl.getAssets(),
+                                oldImpl.getMetrics(), oldImpl.getConfiguration(),
+                                oldImpl.getDisplayAdjustments());
+                        r.setImpl(newImpl);
+                    }
+                }
+            }
+        }
+    }
+
     /**
      * Returns the LocaleConfig current set
      */
@@ -1827,4 +1875,17 @@
     public @NonNull ArrayMap<String, SharedLibraryAssets> getSharedLibAssetsMap() {
         return new ArrayMap<>(mSharedLibAssetsMap);
     }
+
+    /**
+     * Add all resources references to the list which is designed to help to append shared library
+     * asset paths. This is invoked in Resources constructor to include all Resources instances.
+     */
+    public void registerAllResourcesReference(@NonNull Resources resources) {
+        if (android.content.res.Flags.registerResourcePaths()) {
+            synchronized (mLock) {
+                mAllResourceReferences.add(
+                        new WeakReference<>(resources, mAllResourceReferencesQueue));
+            }
+        }
+    }
 }
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index be0aaff..25697c5 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -197,8 +197,8 @@
   description: "Fix provisioning for single-user headless DO"
   bug: "289515470"
   metadata {
-      purpose: PURPOSE_BUGFIX
-    }
+    purpose: PURPOSE_BUGFIX
+  }
 }
 
 flag {
@@ -214,3 +214,13 @@
   description: "Guards a new flow for recursive required enterprise app list merging"
   bug: "319084618"
 }
+
+flag {
+  name: "headless_device_owner_delegate_security_logging_bug_fix"
+  namespace: "enterprise"
+  description: "Fix delegate security logging for single user headless DO."
+  bug: "289515470"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
diff --git a/core/java/android/app/contextualsearch/CallbackToken.java b/core/java/android/app/contextualsearch/CallbackToken.java
index 0bbd1e5..378193f 100644
--- a/core/java/android/app/contextualsearch/CallbackToken.java
+++ b/core/java/android/app/contextualsearch/CallbackToken.java
@@ -51,6 +51,7 @@
     private static final String TAG = CallbackToken.class.getSimpleName();
     private final IBinder mToken;
 
+    private final Object mLock = new Object();
     private boolean mTokenUsed = false;
 
     public CallbackToken() {
@@ -75,10 +76,14 @@
     public void getContextualSearchState(@NonNull @CallbackExecutor Executor executor,
             @NonNull OutcomeReceiver<ContextualSearchState, Throwable> callback) {
         if (DEBUG) Log.d(TAG, "getContextualSearchState for token:" + mToken);
-        if (mTokenUsed) {
-            callback.onError(new IllegalAccessException("Token already used."));
+        boolean tokenUsed;
+        synchronized (mLock) {
+            tokenUsed = markUsedLocked();
         }
-        mTokenUsed = true;
+        if (tokenUsed) {
+            callback.onError(new IllegalAccessException("Token already used."));
+            return;
+        }
         try {
             // Get the service from the system server.
             IBinder b = ServiceManager.getService(Context.CONTEXTUAL_SEARCH_SERVICE);
@@ -96,6 +101,12 @@
         }
     }
 
+    private boolean markUsedLocked() {
+        boolean oldValue = mTokenUsed;
+        mTokenUsed = true;
+        return oldValue;
+    }
+
     /**
      * Return the token necessary for validating the caller of {@link #getContextualSearchState}.
      *
diff --git a/core/java/android/app/contextualsearch/ContextualSearchManager.java b/core/java/android/app/contextualsearch/ContextualSearchManager.java
index a894a0e..c080a6b 100644
--- a/core/java/android/app/contextualsearch/ContextualSearchManager.java
+++ b/core/java/android/app/contextualsearch/ContextualSearchManager.java
@@ -43,7 +43,7 @@
  */
 @SystemApi
 @FlaggedApi(Flags.FLAG_ENABLE_SERVICE)
-public class ContextualSearchManager {
+public final class ContextualSearchManager {
 
     /**
      * Key to get the entrypoint from the extras of the activity launched by contextual search.
diff --git a/core/java/android/app/slice/Slice.java b/core/java/android/app/slice/Slice.java
index 475ee7a..5514868 100644
--- a/core/java/android/app/slice/Slice.java
+++ b/core/java/android/app/slice/Slice.java
@@ -41,7 +41,12 @@
  *
  * <p>They are constructed using {@link Builder} in a tree structure
  * that provides the OS some information about how the content should be displayed.
+ * @deprecated Slice framework has been deprecated, it will not receive any updates from
+ *          {@link android.os.Build.VANILLA_ICE_CREAM} and forward. If you are looking for a
+ *          framework that sends displayable data from one app to another, consider using
+ *          {@link android.app.appsearch.AppSearchManager}.
  */
+@Deprecated
 public final class Slice implements Parcelable {
 
     /**
@@ -338,7 +343,12 @@
 
     /**
      * A Builder used to construct {@link Slice}s
+     * @deprecated Slice framework has been deprecated, it will not receive any updates from
+     *          {@link android.os.Build.VANILLA_ICE_CREAM} and forward. If you are looking for a
+     *          framework that sends displayable data from one app to another, consider using
+     *          {@link android.app.appsearch.AppSearchManager}.
      */
+    @Deprecated
     public static class Builder {
 
         private final Uri mUri;
diff --git a/core/java/android/app/slice/SliceItem.java b/core/java/android/app/slice/SliceItem.java
index 2d6f4a6..27c726d 100644
--- a/core/java/android/app/slice/SliceItem.java
+++ b/core/java/android/app/slice/SliceItem.java
@@ -53,7 +53,12 @@
  * The hints that a {@link SliceItem} are a set of strings which annotate
  * the content. The hints that are guaranteed to be understood by the system
  * are defined on {@link Slice}.
+ * @deprecated Slice framework has been deprecated, it will not receive any updates from
+ *          {@link android.os.Build.VANILLA_ICE_CREAM} and forward. If you are looking for a
+ *          framework that sends displayable data from one app to another, consider using
+ *          {@link android.app.appsearch.AppSearchManager}.
  */
+@Deprecated
 public final class SliceItem implements Parcelable {
 
     private static final String TAG = "SliceItem";
diff --git a/core/java/android/app/slice/SliceManager.java b/core/java/android/app/slice/SliceManager.java
index 2e179d0..4fc2a0c 100644
--- a/core/java/android/app/slice/SliceManager.java
+++ b/core/java/android/app/slice/SliceManager.java
@@ -59,7 +59,12 @@
  * Class to handle interactions with {@link Slice}s.
  * <p>
  * The SliceManager manages permissions and pinned state for slices.
+ * @deprecated Slice framework has been deprecated, it will not receive any updates from
+ *          {@link android.os.Build.VANILLA_ICE_CREAM} and forward. If you are looking for a
+ *          framework that sends displayable data from one app to another, consider using
+ *          {@link android.app.appsearch.AppSearchManager}.
  */
+@Deprecated
 @SystemService(Context.SLICE_SERVICE)
 public class SliceManager {
 
diff --git a/core/java/android/app/slice/SliceMetrics.java b/core/java/android/app/slice/SliceMetrics.java
index 746beaf..abfe3a2f 100644
--- a/core/java/android/app/slice/SliceMetrics.java
+++ b/core/java/android/app/slice/SliceMetrics.java
@@ -31,7 +31,12 @@
  * not need to reference this class.
  *
  * @see androidx.slice.widget.SliceView
+ * @deprecated Slice framework has been deprecated, it will not receive any updates from
+ *          {@link android.os.Build.VANILLA_ICE_CREAM} and forward. If you are looking for a
+ *          framework that sends displayable data from one app to another, consider using
+ *          {@link android.app.appsearch.AppSearchManager}.
  */
+@Deprecated
 public class SliceMetrics {
 
     private static final String TAG = "SliceMetrics";
diff --git a/core/java/android/app/slice/SliceProvider.java b/core/java/android/app/slice/SliceProvider.java
index 42c3aa6..4374550 100644
--- a/core/java/android/app/slice/SliceProvider.java
+++ b/core/java/android/app/slice/SliceProvider.java
@@ -91,7 +91,12 @@
  * </pre>
  *
  * @see Slice
+ * @deprecated Slice framework has been deprecated, it will not receive any updates from
+ *          {@link android.os.Build.VANILLA_ICE_CREAM} and forward. If you are looking for a
+ *          framework that sends displayable data from one app to another, consider using
+ *          {@link android.app.appsearch.AppSearchManager}.
  */
+@Deprecated
 public abstract class SliceProvider extends ContentProvider {
     /**
      * This is the Android platform's MIME type for a URI
diff --git a/core/java/android/app/slice/SliceSpec.java b/core/java/android/app/slice/SliceSpec.java
index a332349..078f552 100644
--- a/core/java/android/app/slice/SliceSpec.java
+++ b/core/java/android/app/slice/SliceSpec.java
@@ -38,7 +38,12 @@
  *
  * @see Slice
  * @see SliceProvider#onBindSlice(Uri, Set)
+ * @deprecated Slice framework has been deprecated, it will not receive any updates from
+ *          {@link android.os.Build.VANILLA_ICE_CREAM} and forward. If you are looking for a
+ *          framework that sends displayable data from one app to another, consider using
+ *          {@link android.app.appsearch.AppSearchManager}.
  */
+@Deprecated
 public final class SliceSpec implements Parcelable {
 
     private final String mType;
diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java
index 2c26389..a08d659 100644
--- a/core/java/android/companion/CompanionDeviceManager.java
+++ b/core/java/android/companion/CompanionDeviceManager.java
@@ -26,6 +26,7 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresFeature;
 import android.annotation.RequiresPermission;
 import android.annotation.SuppressLint;
 import android.annotation.SystemApi;
@@ -80,25 +81,27 @@
 import java.util.function.Consumer;
 
 /**
- * System level service for managing companion devices
+ * Public interfaces for managing companion devices.
  *
- * See <a href="{@docRoot}guide/topics/connectivity/companion-device-pairing">this guide</a>
- * for a usage example.
+ * <p>The interfaces in this class allow companion apps to
+ * {@link #associate(AssociationRequest, Executor, Callback)} discover and request device profiles}
+ * for companion devices, {@link #startObservingDevicePresence(String) listen to device presence
+ * events}, {@link #startSystemDataTransfer(int, Executor, OutcomeReceiver) transfer system level
+ * data} via {@link #attachSystemDataTransport(int, InputStream, OutputStream) the reported
+ * channel} and more.</p>
  *
- * <p>To obtain an instance call {@link Context#getSystemService}({@link
- * Context#COMPANION_DEVICE_SERVICE}) Then, call {@link #associate(AssociationRequest,
- * Callback, Handler)} to initiate the flow of associating current package with a
- * device selected by user.</p>
- *
- * @see CompanionDeviceManager#associate
- * @see AssociationRequest
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about managing companion devices, read the <a href=
+ * "{@docRoot}guide/topics/connectivity/companion-device-pairing">Companion Device Pairing</a>
+ * developer guide.
+ * </div>
  */
 @SuppressLint("LongLogTag")
 @SystemService(Context.COMPANION_DEVICE_SERVICE)
+@RequiresFeature(PackageManager.FEATURE_COMPANION_DEVICE_SETUP)
 public final class CompanionDeviceManager {
-
-    private static final boolean DEBUG = false;
-    private static final String LOG_TAG = "CDM_CompanionDeviceManager";
+    private static final String TAG = "CDM_CompanionDeviceManager";
 
     /** @hide */
     @IntDef(prefix = {"RESULT_"}, value = {
@@ -374,7 +377,7 @@
     }
 
     private final ICompanionDeviceManager mService;
-    private Context mContext;
+    private final Context mContext;
 
     @GuardedBy("mListeners")
     private final ArrayList<OnAssociationsChangedListenerProxy> mListeners = new ArrayList<>();
@@ -432,7 +435,11 @@
             @NonNull AssociationRequest request,
             @NonNull Callback callback,
             @Nullable Handler handler) {
-        if (!checkFeaturePresent()) return;
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
+            return;
+        }
+
         Objects.requireNonNull(request, "Request cannot be null");
         Objects.requireNonNull(callback, "Callback cannot be null");
         handler = Handler.mainIfNull(handler);
@@ -492,7 +499,11 @@
             @NonNull AssociationRequest request,
             @NonNull Executor executor,
             @NonNull Callback callback) {
-        if (!checkFeaturePresent()) return;
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
+            return;
+        }
+
         Objects.requireNonNull(request, "Request cannot be null");
         Objects.requireNonNull(executor, "Executor cannot be null");
         Objects.requireNonNull(callback, "Callback cannot be null");
@@ -521,7 +532,10 @@
     @UserHandleAware
     @Nullable
     public IntentSender buildAssociationCancellationIntent() {
-        if (!checkFeaturePresent()) return null;
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
+            return null;
+        }
 
         try {
             PendingIntent pendingIntent = mService.buildAssociationCancellationIntent(
@@ -543,7 +557,8 @@
      * @param flags system data types to be enabled.
      */
     public void enableSystemDataSyncForTypes(int associationId, @DataSyncTypes int flags) {
-        if (!checkFeaturePresent()) {
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
             return;
         }
 
@@ -565,7 +580,8 @@
      * @param flags system data types to be disabled.
      */
     public void disableSystemDataSyncForTypes(int associationId, @DataSyncTypes int flags) {
-        if (!checkFeaturePresent()) {
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
             return;
         }
 
@@ -580,6 +596,11 @@
      * @hide
      */
     public void enablePermissionsSync(int associationId) {
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
+            return;
+        }
+
         try {
             mService.enablePermissionsSync(associationId);
         } catch (RemoteException e) {
@@ -591,6 +612,11 @@
      * @hide
      */
     public void disablePermissionsSync(int associationId) {
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
+            return;
+        }
+
         try {
             mService.disablePermissionsSync(associationId);
         } catch (RemoteException e) {
@@ -602,6 +628,11 @@
      * @hide
      */
     public PermissionSyncRequest getPermissionSyncRequest(int associationId) {
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
+            return null;
+        }
+
         try {
             return mService.getPermissionSyncRequest(associationId);
         } catch (RemoteException e) {
@@ -636,7 +667,10 @@
     @UserHandleAware
     @NonNull
     public List<AssociationInfo> getMyAssociations() {
-        if (!checkFeaturePresent()) return Collections.emptyList();
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
+            return Collections.emptyList();
+        }
 
         try {
             return mService.getAssociations(mContext.getOpPackageName(), mContext.getUserId());
@@ -665,7 +699,10 @@
     @UserHandleAware
     @Deprecated
     public void disassociate(@NonNull String deviceMacAddress) {
-        if (!checkFeaturePresent()) return;
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
+            return;
+        }
 
         try {
             mService.legacyDisassociate(deviceMacAddress, mContext.getOpPackageName(),
@@ -690,7 +727,10 @@
      */
     @UserHandleAware
     public void disassociate(int associationId) {
-        if (!checkFeaturePresent()) return;
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
+            return;
+        }
 
         try {
             mService.disassociate(associationId);
@@ -716,9 +756,11 @@
      */
     @UserHandleAware
     public void requestNotificationAccess(ComponentName component) {
-        if (!checkFeaturePresent()) {
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
             return;
         }
+
         try {
             PendingIntent pendingIntent = mService.requestNotificationAccess(
                     component, mContext.getUserId());
@@ -755,9 +797,11 @@
      */
     @Deprecated
     public boolean hasNotificationAccess(ComponentName component) {
-        if (!checkFeaturePresent()) {
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
             return false;
         }
+
         try {
             return mService.hasNotificationAccess(component);
         } catch (RemoteException e) {
@@ -793,7 +837,11 @@
             @NonNull String packageName,
             @NonNull MacAddress macAddress,
             @NonNull UserHandle user) {
-        if (!checkFeaturePresent()) return false;
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
+            return false;
+        }
+
         Objects.requireNonNull(packageName, "package name cannot be null");
         Objects.requireNonNull(macAddress, "mac address cannot be null");
         Objects.requireNonNull(user, "user cannot be null");
@@ -816,7 +864,8 @@
     @SystemApi
     @UserHandleAware
     @RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES)
-    public @NonNull List<AssociationInfo> getAllAssociations() {
+    @NonNull
+    public List<AssociationInfo> getAllAssociations() {
         return getAllAssociations(mContext.getUserId());
     }
 
@@ -826,8 +875,13 @@
      * @hide
      */
     @RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES)
-    public @NonNull List<AssociationInfo> getAllAssociations(@UserIdInt int userId) {
-        if (!checkFeaturePresent()) return Collections.emptyList();
+    @NonNull
+    public List<AssociationInfo> getAllAssociations(@UserIdInt int userId) {
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
+            return Collections.emptyList();
+        }
+
         try {
             return mService.getAllAssociationsForUser(userId);
         } catch (RemoteException e) {
@@ -875,7 +929,11 @@
     public void addOnAssociationsChangedListener(
             @NonNull Executor executor, @NonNull OnAssociationsChangedListener listener,
             @UserIdInt int userId) {
-        if (!checkFeaturePresent()) return;
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
+            return;
+        }
+
         synchronized (mListeners) {
             final OnAssociationsChangedListenerProxy proxy = new OnAssociationsChangedListenerProxy(
                     executor, listener);
@@ -899,7 +957,11 @@
     @RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES)
     public void removeOnAssociationsChangedListener(
             @NonNull OnAssociationsChangedListener listener) {
-        if (!checkFeaturePresent()) return;
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
+            return;
+        }
+
         synchronized (mListeners) {
             final Iterator<OnAssociationsChangedListenerProxy> iterator = mListeners.iterator();
             while (iterator.hasNext()) {
@@ -931,6 +993,11 @@
     public void addOnTransportsChangedListener(
             @NonNull @CallbackExecutor Executor executor,
             @NonNull Consumer<List<AssociationInfo>> listener) {
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
+            return;
+        }
+
         final OnTransportsChangedListenerProxy proxy = new OnTransportsChangedListenerProxy(
                 executor, listener);
         try {
@@ -950,6 +1017,11 @@
     @RequiresPermission(android.Manifest.permission.USE_COMPANION_TRANSPORTS)
     public void removeOnTransportsChangedListener(
             @NonNull Consumer<List<AssociationInfo>> listener) {
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
+            return;
+        }
+
         final OnTransportsChangedListenerProxy proxy = new OnTransportsChangedListenerProxy(
                 null, listener);
         try {
@@ -969,6 +1041,11 @@
      */
     @RequiresPermission(android.Manifest.permission.USE_COMPANION_TRANSPORTS)
     public void sendMessage(int messageType, @NonNull byte[] data, @NonNull int[] associationIds) {
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
+            return;
+        }
+
         try {
             mService.sendMessage(messageType, data, associationIds);
         } catch (RemoteException e) {
@@ -989,6 +1066,11 @@
     public void addOnMessageReceivedListener(
             @NonNull @CallbackExecutor Executor executor, int messageType,
             @NonNull BiConsumer<Integer, byte[]> listener) {
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
+            return;
+        }
+
         final OnMessageReceivedListenerProxy proxy = new OnMessageReceivedListenerProxy(
                 executor, listener);
         try {
@@ -1006,6 +1088,11 @@
     @RequiresPermission(android.Manifest.permission.USE_COMPANION_TRANSPORTS)
     public void removeOnMessageReceivedListener(int messageType,
             @NonNull BiConsumer<Integer, byte[]> listener) {
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
+            return;
+        }
+
         final OnMessageReceivedListenerProxy proxy = new OnMessageReceivedListenerProxy(
                 null, listener);
         try {
@@ -1031,9 +1118,11 @@
     @RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES)
     public boolean canPairWithoutPrompt(@NonNull String packageName,
             @NonNull String deviceMacAddress, @NonNull UserHandle user) {
-        if (!checkFeaturePresent()) {
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
             return false;
         }
+
         Objects.requireNonNull(packageName, "package name cannot be null");
         Objects.requireNonNull(deviceMacAddress, "device mac address cannot be null");
         Objects.requireNonNull(user, "user handle cannot be null");
@@ -1081,9 +1170,11 @@
     @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE)
     public void startObservingDevicePresence(@NonNull String deviceAddress)
             throws DeviceNotAssociatedException {
-        if (!checkFeaturePresent()) {
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
             return;
         }
+
         Objects.requireNonNull(deviceAddress, "address cannot be null");
         try {
             mService.legacyStartObservingDevicePresence(deviceAddress,
@@ -1123,9 +1214,11 @@
     @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE)
     public void stopObservingDevicePresence(@NonNull String deviceAddress)
             throws DeviceNotAssociatedException {
-        if (!checkFeaturePresent()) {
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
             return;
         }
+
         Objects.requireNonNull(deviceAddress, "address cannot be null");
         try {
             mService.legacyStopObservingDevicePresence(deviceAddress,
@@ -1171,6 +1264,11 @@
     @FlaggedApi(Flags.FLAG_DEVICE_PRESENCE)
     @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE)
     public void startObservingDevicePresence(@NonNull ObservingDevicePresenceRequest request) {
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
+            return;
+        }
+
         Objects.requireNonNull(request, "request cannot be null");
 
         try {
@@ -1192,6 +1290,11 @@
     @FlaggedApi(Flags.FLAG_DEVICE_PRESENCE)
     @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE)
     public void stopObservingDevicePresence(@NonNull ObservingDevicePresenceRequest request) {
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
+            return;
+        }
+
         Objects.requireNonNull(request, "request cannot be null");
 
         try {
@@ -1222,7 +1325,7 @@
     @RequiresPermission(android.Manifest.permission.DELIVER_COMPANION_MESSAGES)
     public void dispatchMessage(int messageId, int associationId, @NonNull byte[] message)
             throws DeviceNotAssociatedException {
-        Log.w(LOG_TAG, "dispatchMessage replaced by attachSystemDataTransport");
+        Log.w(TAG, "dispatchMessage replaced by attachSystemDataTransport");
     }
 
     /**
@@ -1244,6 +1347,11 @@
     @RequiresPermission(android.Manifest.permission.DELIVER_COMPANION_MESSAGES)
     public void attachSystemDataTransport(int associationId, @NonNull InputStream in,
             @NonNull OutputStream out) throws DeviceNotAssociatedException {
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
+            return;
+        }
+
         synchronized (mTransports) {
             if (mTransports.contains(associationId)) {
                 detachSystemDataTransport(associationId);
@@ -1272,6 +1380,11 @@
     @RequiresPermission(android.Manifest.permission.DELIVER_COMPANION_MESSAGES)
     public void detachSystemDataTransport(int associationId)
             throws DeviceNotAssociatedException {
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
+            return;
+        }
+
         synchronized (mTransports) {
             final Transport transport = mTransports.get(associationId);
             if (transport != null) {
@@ -1296,9 +1409,11 @@
             @NonNull String packageName,
             @NonNull MacAddress macAddress,
             @NonNull byte[] certificate) {
-        if (!checkFeaturePresent()) {
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
             return;
         }
+
         Objects.requireNonNull(packageName, "package name cannot be null");
         Objects.requireNonNull(macAddress, "mac address cannot be null");
 
@@ -1327,6 +1442,11 @@
     @SystemApi
     @RequiresPermission(android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED)
     public void notifyDeviceAppeared(int associationId) {
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
+            return;
+        }
+
         try {
             mService.notifySelfManagedDeviceAppeared(associationId);
         } catch (RemoteException e) {
@@ -1349,6 +1469,11 @@
     @SystemApi
     @RequiresPermission(android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED)
     public void notifyDeviceDisappeared(int associationId) {
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
+            return;
+        }
+
         try {
             mService.notifySelfManagedDeviceDisappeared(associationId);
         } catch (RemoteException e) {
@@ -1384,6 +1509,11 @@
     @Nullable
     public IntentSender buildPermissionTransferUserConsentIntent(int associationId)
             throws DeviceNotAssociatedException {
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
+            return null;
+        }
+
         try {
             PendingIntent pendingIntent = mService.buildPermissionTransferUserConsentIntent(
                     mContext.getOpPackageName(),
@@ -1420,6 +1550,11 @@
     @UserHandleAware
     @FlaggedApi(Flags.FLAG_PERM_SYNC_USER_CONSENT)
     public boolean isPermissionTransferUserConsented(int associationId) {
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
+            return false;
+        }
+
         try {
             return mService.isPermissionTransferUserConsented(mContext.getOpPackageName(),
                     mContext.getUserId(), associationId);
@@ -1446,6 +1581,11 @@
     @Deprecated
     @UserHandleAware
     public void startSystemDataTransfer(int associationId) throws DeviceNotAssociatedException {
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
+            return;
+        }
+
         try {
             mService.startSystemDataTransfer(mContext.getOpPackageName(), mContext.getUserId(),
                     associationId, null);
@@ -1478,6 +1618,11 @@
             @NonNull Executor executor,
             @NonNull OutcomeReceiver<Void, CompanionException> result)
             throws DeviceNotAssociatedException {
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
+            return;
+        }
+
         try {
             mService.startSystemDataTransfer(mContext.getOpPackageName(), mContext.getUserId(),
                     associationId, new SystemDataTransferCallbackProxy(executor, result));
@@ -1495,6 +1640,11 @@
      */
     @UserHandleAware
     public boolean isCompanionApplicationBound() {
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
+            return false;
+        }
+
         try {
             return mService.isCompanionApplicationBound(
                     mContext.getOpPackageName(), mContext.getUserId());
@@ -1513,6 +1663,11 @@
     @TestApi
     @RequiresPermission(android.Manifest.permission.MANAGE_COMPANION_DEVICES)
     public void enableSecureTransport(boolean enabled) {
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
+            return;
+        }
+
         try {
             mService.enableSecureTransport(enabled);
         } catch (RemoteException e) {
@@ -1534,6 +1689,11 @@
     @FlaggedApi(Flags.FLAG_ASSOCIATION_TAG)
     @UserHandleAware
     public void setAssociationTag(int associationId, @NonNull String tag) {
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
+            return;
+        }
+
         Objects.requireNonNull(tag, "tag cannot be null");
 
         if (tag.length() > ASSOCIATION_TAG_LENGTH_LIMIT) {
@@ -1560,6 +1720,11 @@
     @FlaggedApi(Flags.FLAG_ASSOCIATION_TAG)
     @UserHandleAware
     public void clearAssociationTag(int associationId) {
+        if (mService == null) {
+            Log.w(TAG, "CompanionDeviceManager service is not available.");
+            return;
+        }
+
         try {
             mService.clearAssociationTag(associationId);
         } catch (RemoteException e) {
@@ -1567,15 +1732,6 @@
         }
     }
 
-    private boolean checkFeaturePresent() {
-        boolean featurePresent = mService != null;
-        if (!featurePresent && DEBUG) {
-            Log.d(LOG_TAG, "Feature " + PackageManager.FEATURE_COMPANION_DEVICE_SETUP
-                    + " not available");
-        }
-        return featurePresent;
-    }
-
     private static class AssociationRequestCallbackProxy extends IAssociationRequestCallback.Stub {
         private final Handler mHandler;
         private final Callback mCallback;
@@ -1613,7 +1769,7 @@
         private <T> void execute(Consumer<T> callback, T arg) {
             if (mExecutor != null) {
                 mExecutor.execute(() -> callback.accept(arg));
-            } else {
+            } else if (mHandler != null) {
                 mHandler.post(() -> callback.accept(arg));
             }
         }
@@ -1716,6 +1872,11 @@
         }
 
         public void start() throws IOException {
+            if (mService == null) {
+                Log.w(TAG, "CompanionDeviceManager service is not available.");
+                return;
+            }
+
             final ParcelFileDescriptor[] pair = ParcelFileDescriptor.createSocketPair();
             final ParcelFileDescriptor localFd = pair[0];
             final ParcelFileDescriptor remoteFd = pair[1];
@@ -1734,7 +1895,7 @@
                     copyWithFlushing(mLocalIn, mRemoteOut);
                 } catch (IOException e) {
                     if (!mStopped) {
-                        Log.w(LOG_TAG, "Trouble during outgoing transport", e);
+                        Log.w(TAG, "Trouble during outgoing transport", e);
                         stop();
                     }
                 }
@@ -1744,7 +1905,7 @@
                     copyWithFlushing(mRemoteIn, mLocalOut);
                 } catch (IOException e) {
                     if (!mStopped) {
-                        Log.w(LOG_TAG, "Trouble during incoming transport", e);
+                        Log.w(TAG, "Trouble during incoming transport", e);
                         stop();
                     }
                 }
@@ -1752,13 +1913,18 @@
         }
 
         public void stop() {
+            if (mService == null) {
+                Log.w(TAG, "CompanionDeviceManager service is not available.");
+                return;
+            }
+
             mStopped = true;
 
             try {
                 mService.detachSystemDataTransport(mContext.getPackageName(),
                         mContext.getUserId(), mAssociationId);
             } catch (RemoteException e) {
-                Log.w(LOG_TAG, "Failed to detach transport", e);
+                Log.w(TAG, "Failed to detach transport", e);
             }
 
             IoUtils.closeQuietly(mRemoteIn);
diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl
index bff90f1..5d4babb 100644
--- a/core/java/android/content/pm/IPackageManager.aidl
+++ b/core/java/android/content/pm/IPackageManager.aidl
@@ -847,5 +847,5 @@
     @EnforcePermission("GET_APP_METADATA")
     int getAppMetadataSource(String packageName, int userId);
 
-    ComponentName getDomainVerificationAgent();
+    ComponentName getDomainVerificationAgent(int userId);
 }
diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java
index 1f5f88f..a720b64 100644
--- a/core/java/android/content/res/Resources.java
+++ b/core/java/android/content/res/Resources.java
@@ -339,14 +339,17 @@
     public Resources(@Nullable ClassLoader classLoader) {
         mClassLoader = classLoader == null ? ClassLoader.getSystemClassLoader() : classLoader;
         sResourcesHistory.add(this);
+        ResourcesManager.getInstance().registerAllResourcesReference(this);
     }
 
     /**
-     * Only for creating the System resources.
+     * Only for creating the System resources. This is the only constructor that doesn't add
+     * Resources itself to the ResourcesManager list of all Resources references.
      */
     @UnsupportedAppUsage
     private Resources() {
-        this(null);
+        mClassLoader = ClassLoader.getSystemClassLoader();
+        sResourcesHistory.add(this);
 
         final DisplayMetrics metrics = new DisplayMetrics();
         metrics.setToDefaults();
diff --git a/core/java/android/content/res/ResourcesImpl.java b/core/java/android/content/res/ResourcesImpl.java
index 4c22fee..31cacb7 100644
--- a/core/java/android/content/res/ResourcesImpl.java
+++ b/core/java/android/content/res/ResourcesImpl.java
@@ -228,6 +228,11 @@
         return mAssets;
     }
 
+    @UnsupportedAppUsage
+    public DisplayMetrics getMetrics() {
+        return mMetrics;
+    }
+
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     DisplayMetrics getDisplayMetrics() {
         if (DEBUG_CONFIG) Slog.v(TAG, "Returning DisplayMetrics: " + mMetrics.widthPixels
@@ -235,7 +240,8 @@
         return mMetrics;
     }
 
-    Configuration getConfiguration() {
+    @UnsupportedAppUsage
+    public Configuration getConfiguration() {
         return mConfiguration;
     }
 
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index 4dbdd91..2cd7aeb 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -16,6 +16,7 @@
 
 package android.inputmethodservice;
 
+import static android.view.inputmethod.Flags.predictiveBackIme;
 import static android.inputmethodservice.InputMethodServiceProto.CANDIDATES_VIEW_STARTED;
 import static android.inputmethodservice.InputMethodServiceProto.CANDIDATES_VISIBILITY;
 import static android.inputmethodservice.InputMethodServiceProto.CONFIGURATION;
@@ -3098,7 +3099,7 @@
         cancelImeSurfaceRemoval();
         mInShowWindow = false;
         Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
-        registerCompatOnBackInvokedCallback();
+        registerDefaultOnBackInvokedCallback();
     }
 
 
@@ -3107,18 +3108,27 @@
      *  back dispatching is enabled. We keep the {@link KeyEvent#KEYCODE_BACK} based legacy code
      *  around to handle back on older devices.
      */
-    private void registerCompatOnBackInvokedCallback() {
+    private void registerDefaultOnBackInvokedCallback() {
         if (mBackCallbackRegistered) {
             return;
         }
         if (mWindow != null) {
-            mWindow.getOnBackInvokedDispatcher().registerOnBackInvokedCallback(
-                    OnBackInvokedDispatcher.PRIORITY_DEFAULT, mCompatBackCallback);
+            if (getApplicationInfo().isOnBackInvokedCallbackEnabled() && predictiveBackIme()) {
+                // Register the compat callback as system-callback if IME has opted in for
+                // predictive back (and predictiveBackIme feature flag is enabled). This indicates
+                // to the receiving process (application process) that a predictive IME dismiss
+                // animation may be played instead of invoking the callback.
+                mWindow.getOnBackInvokedDispatcher().registerSystemOnBackInvokedCallback(
+                        mCompatBackCallback);
+            } else {
+                mWindow.getOnBackInvokedDispatcher().registerOnBackInvokedCallback(
+                        OnBackInvokedDispatcher.PRIORITY_DEFAULT, mCompatBackCallback);
+            }
             mBackCallbackRegistered = true;
         }
     }
 
-    private void unregisterCompatOnBackInvokedCallback() {
+    private void unregisterDefaultOnBackInvokedCallback() {
         if (!mBackCallbackRegistered) {
             return;
         }
@@ -3251,7 +3261,7 @@
         }
         mLastWasInFullscreenMode = mIsFullscreen;
         updateFullscreenMode();
-        unregisterCompatOnBackInvokedCallback();
+        unregisterDefaultOnBackInvokedCallback();
     }
 
     /**
@@ -3328,7 +3338,7 @@
         // Back callback is typically unregistered in {@link #hideWindow()}, but it's possible
         // for {@link #doFinishInput()} to be called without {@link #hideWindow()} so we also
         // unregister here.
-        unregisterCompatOnBackInvokedCallback();
+        unregisterDefaultOnBackInvokedCallback();
     }
 
     void doStartInput(InputConnection ic, EditorInfo editorInfo, boolean restarting) {
@@ -4473,7 +4483,7 @@
     private void compatHandleBack() {
         if (!mDecorViewVisible) {
             Log.e(TAG, "Back callback invoked on a hidden IME. Removing the callback...");
-            unregisterCompatOnBackInvokedCallback();
+            unregisterDefaultOnBackInvokedCallback();
             return;
         }
         final KeyEvent downEvent = createBackKeyEvent(
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index c74d50a..363e252 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -1002,6 +1002,21 @@
             "android.settings.BLUETOOTH_SETTINGS";
 
     /**
+     * Activity Action: Show settings to allow configuration of Hearing Devices.
+     * <p>
+     * In some cases, a matching Activity may not exist, so ensure you
+     * safeguard against this.
+     * <p>
+     * Input: Nothing.
+     * <p>
+     * Output: Nothing.
+     * @hide
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_HEARING_DEVICES_SETTINGS =
+            "android.settings.HEARING_DEVICES_SETTINGS";
+
+    /**
      * Activity action: Show Settings app search UI when this action is available for device.
      * <p>
      * Input: Nothing.
diff --git a/core/java/android/view/BatchedInputEventReceiver.java b/core/java/android/view/BatchedInputEventReceiver.java
index ca2e56d..2e39f73 100644
--- a/core/java/android/view/BatchedInputEventReceiver.java
+++ b/core/java/android/view/BatchedInputEventReceiver.java
@@ -29,6 +29,7 @@
     private Choreographer mChoreographer;
     private boolean mBatchingEnabled;
     private boolean mBatchedInputScheduled;
+    private final String mTag;
     private final Handler mHandler;
     private final Runnable mConsumeBatchedInputEvents = new Runnable() {
         @Override
@@ -43,6 +44,7 @@
         super(inputChannel, looper);
         mChoreographer = choreographer;
         mBatchingEnabled = true;
+        mTag = inputChannel.getName();
         traceBoolVariable("mBatchingEnabled", mBatchingEnabled);
         traceBoolVariable("mBatchedInputScheduled", mBatchedInputScheduled);
         mHandler = new Handler(looper);
@@ -123,7 +125,12 @@
     private final class BatchedInputRunnable implements Runnable {
         @Override
         public void run() {
-            doConsumeBatchedInput(mChoreographer.getFrameTimeNanos());
+            try {
+                Trace.traceBegin(Trace.TRACE_TAG_INPUT, mTag);
+                doConsumeBatchedInput(mChoreographer.getFrameTimeNanos());
+            } finally {
+                Trace.traceEnd(Trace.TRACE_TAG_INPUT);
+            }
         }
     }
     private final BatchedInputRunnable mBatchedInputRunnable = new BatchedInputRunnable();
diff --git a/core/java/android/view/ImeBackAnimationController.java b/core/java/android/view/ImeBackAnimationController.java
new file mode 100644
index 0000000..d14e858
--- /dev/null
+++ b/core/java/android/view/ImeBackAnimationController.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.view.InsetsController.ANIMATION_TYPE_USER;
+import static android.view.WindowInsets.Type.ime;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Insets;
+import android.util.Log;
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
+import android.window.BackEvent;
+import android.window.OnBackAnimationCallback;
+
+/**
+ * Controller for IME predictive back animation
+ *
+ * @hide
+ */
+public class ImeBackAnimationController implements OnBackAnimationCallback {
+
+    private static final String TAG = "ImeBackAnimationController";
+    private static final int POST_COMMIT_DURATION_MS = 200;
+    private static final int POST_COMMIT_CANCEL_DURATION_MS = 50;
+    private static final float PEEK_FRACTION = 0.1f;
+    private static final Interpolator STANDARD_DECELERATE = new PathInterpolator(0f, 0f, 0f, 1f);
+    private static final Interpolator EMPHASIZED_DECELERATE = new PathInterpolator(
+            0.05f, 0.7f, 0.1f, 1f);
+    private static final Interpolator STANDARD_ACCELERATE = new PathInterpolator(0.3f, 0f, 1f, 1f);
+
+    private final InsetsController mInsetsController;
+    private final ViewRootImpl mViewRoot;
+    private WindowInsetsAnimationController mWindowInsetsAnimationController = null;
+    private ValueAnimator mPostCommitAnimator = null;
+    private float mLastProgress = 0f;
+    private boolean mTriggerBack = false;
+    private boolean mIsPreCommitAnimationInProgress = false;
+
+    public ImeBackAnimationController(ViewRootImpl viewRoot) {
+        mInsetsController = viewRoot.getInsetsController();
+        mViewRoot = viewRoot;
+    }
+
+    @Override
+    public void onBackStarted(@NonNull BackEvent backEvent) {
+        if (isAdjustResize()) {
+            // There is no good solution for a predictive back animation if the app uses
+            // adjustResize, since we can't relayout the whole app for every frame. We also don't
+            // want to reveal any black areas behind the IME. Therefore let's not play any animation
+            // in that case for now.
+            Log.d(TAG, "onBackStarted -> not playing predictive back animation due to softinput"
+                    + " mode adjustResize");
+            return;
+        }
+        if (isHideAnimationInProgress()) {
+            // If IME is currently animating away, skip back gesture
+            return;
+        }
+        mIsPreCommitAnimationInProgress = true;
+        if (mWindowInsetsAnimationController != null) {
+            // There's still an active animation controller. This means that a cancel post commit
+            // animation of an earlier back gesture is still in progress. Let's cancel it and let
+            // the new gesture seamlessly take over.
+            resetPostCommitAnimator();
+            setPreCommitProgress(0f);
+            return;
+        }
+        mInsetsController.controlWindowInsetsAnimation(ime(), /*cancellationSignal*/ null,
+                new WindowInsetsAnimationControlListener() {
+                    @Override
+                    public void onReady(@NonNull WindowInsetsAnimationController controller,
+                            @WindowInsets.Type.InsetsType int types) {
+                        mWindowInsetsAnimationController = controller;
+                        if (mIsPreCommitAnimationInProgress) {
+                            setPreCommitProgress(mLastProgress);
+                        } else {
+                            // gesture has already finished before IME became ready to animate
+                            startPostCommitAnim(mTriggerBack);
+                        }
+                    }
+
+                    @Override
+                    public void onFinished(@NonNull WindowInsetsAnimationController controller) {
+                        reset();
+                    }
+
+                    @Override
+                    public void onCancelled(@Nullable WindowInsetsAnimationController controller) {
+                        reset();
+                    }
+                }, /*fromIme*/ false, /*durationMs*/ -1, /*interpolator*/ null, ANIMATION_TYPE_USER,
+                /*fromPredictiveBack*/ true);
+    }
+
+    @Override
+    public void onBackProgressed(@NonNull BackEvent backEvent) {
+        mLastProgress = backEvent.getProgress();
+        setPreCommitProgress(mLastProgress);
+    }
+
+    @Override
+    public void onBackCancelled() {
+        if (isAdjustResize()) return;
+        startPostCommitAnim(/*hideIme*/ false);
+    }
+
+    @Override
+    public void onBackInvoked() {
+        if (isAdjustResize()) {
+            mInsetsController.hide(ime());
+            return;
+        }
+        startPostCommitAnim(/*hideIme*/ true);
+    }
+
+    private void setPreCommitProgress(float progress) {
+        if (isHideAnimationInProgress()) return;
+        if (mWindowInsetsAnimationController != null) {
+            float hiddenY = mWindowInsetsAnimationController.getHiddenStateInsets().bottom;
+            float shownY = mWindowInsetsAnimationController.getShownStateInsets().bottom;
+            float imeHeight = shownY - hiddenY;
+            float interpolatedProgress = STANDARD_DECELERATE.getInterpolation(progress);
+            int newY = (int) (imeHeight - interpolatedProgress * (imeHeight * PEEK_FRACTION));
+            mWindowInsetsAnimationController.setInsetsAndAlpha(Insets.of(0, 0, 0, newY), 1f,
+                    progress);
+        }
+    }
+
+    private void startPostCommitAnim(boolean triggerBack) {
+        mIsPreCommitAnimationInProgress = false;
+        if (mWindowInsetsAnimationController == null || isHideAnimationInProgress()) {
+            mTriggerBack = triggerBack;
+            return;
+        }
+        mTriggerBack = triggerBack;
+        int currentBottomInset = mWindowInsetsAnimationController.getCurrentInsets().bottom;
+        int targetBottomInset;
+        if (triggerBack) {
+            targetBottomInset = mWindowInsetsAnimationController.getHiddenStateInsets().bottom;
+        } else {
+            targetBottomInset = mWindowInsetsAnimationController.getShownStateInsets().bottom;
+        }
+        mPostCommitAnimator = ValueAnimator.ofFloat(currentBottomInset, targetBottomInset);
+        mPostCommitAnimator.setInterpolator(
+                triggerBack ? STANDARD_ACCELERATE : EMPHASIZED_DECELERATE);
+        mPostCommitAnimator.addUpdateListener(animation -> {
+            int bottomInset = (int) ((float) animation.getAnimatedValue());
+            if (mWindowInsetsAnimationController != null) {
+                mWindowInsetsAnimationController.setInsetsAndAlpha(Insets.of(0, 0, 0, bottomInset),
+                        1f, animation.getAnimatedFraction());
+            } else {
+                reset();
+            }
+        });
+        mPostCommitAnimator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animator) {
+                if (mIsPreCommitAnimationInProgress) {
+                    // this means a new gesture has started while the cancel-post-commit-animation
+                    // was in progress. Let's not reset anything and let the new user gesture take
+                    // over seamlessly
+                    return;
+                }
+                if (mWindowInsetsAnimationController != null) {
+                    mWindowInsetsAnimationController.finish(!triggerBack);
+                }
+                reset();
+            }
+        });
+        mPostCommitAnimator.setDuration(
+                triggerBack ? POST_COMMIT_DURATION_MS : POST_COMMIT_CANCEL_DURATION_MS);
+        mPostCommitAnimator.start();
+    }
+
+    private void reset() {
+        mWindowInsetsAnimationController = null;
+        resetPostCommitAnimator();
+        mLastProgress = 0f;
+        mTriggerBack = false;
+        mIsPreCommitAnimationInProgress = false;
+    }
+
+    private void resetPostCommitAnimator() {
+        if (mPostCommitAnimator != null) {
+            mPostCommitAnimator.cancel();
+            mPostCommitAnimator = null;
+        }
+    }
+
+    private boolean isAdjustResize() {
+        return (mViewRoot.mWindowAttributes.softInputMode & SOFT_INPUT_MASK_ADJUST)
+                == SOFT_INPUT_ADJUST_RESIZE;
+    }
+
+    private boolean isHideAnimationInProgress() {
+        return mPostCommitAnimator != null && mTriggerBack;
+    }
+
+}
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index 2fcffd0..90aafbd 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -29,6 +29,8 @@
 import static android.view.WindowInsets.Type.captionBar;
 import static android.view.WindowInsets.Type.ime;
 
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
+
 import android.animation.AnimationHandler;
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
@@ -322,7 +324,7 @@
     public static final int ANIMATION_TYPE_HIDE = 1;
 
     /** Running animation is controlled by user via {@link #controlWindowInsetsAnimation} */
-    @VisibleForTesting
+    @VisibleForTesting(visibility = PACKAGE)
     public static final int ANIMATION_TYPE_USER = 2;
 
     /** Running animation will resize insets */
@@ -1076,7 +1078,7 @@
         show(types, false /* fromIme */, null /* statsToken */);
     }
 
-    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    @VisibleForTesting(visibility = PACKAGE)
     public void show(@InsetsType int types, boolean fromIme,
             @Nullable ImeTracker.Token statsToken) {
         if ((types & ime()) != 0) {
@@ -1260,14 +1262,15 @@
             @Nullable CancellationSignal cancellationSignal,
             @NonNull WindowInsetsAnimationControlListener listener) {
         controlWindowInsetsAnimation(types, cancellationSignal, listener,
-                false /* fromIme */, durationMillis, interpolator, ANIMATION_TYPE_USER);
+                false /* fromIme */, durationMillis, interpolator, ANIMATION_TYPE_USER,
+                false /* fromPredictiveBack */);
     }
 
-    private void controlWindowInsetsAnimation(@InsetsType int types,
+    void controlWindowInsetsAnimation(@InsetsType int types,
             @Nullable CancellationSignal cancellationSignal,
             WindowInsetsAnimationControlListener listener,
             boolean fromIme, long durationMs, @Nullable Interpolator interpolator,
-            @AnimationType int animationType) {
+            @AnimationType int animationType, boolean fromPredictiveBack) {
         if ((mState.calculateUncontrollableInsetsFromFrame(mFrame) & types) != 0) {
             listener.onCancelled(null);
             return;
@@ -1279,7 +1282,8 @@
         }
 
         controlAnimationUnchecked(types, cancellationSignal, listener, mFrame, fromIme, durationMs,
-                interpolator, animationType, getLayoutInsetsDuringAnimationMode(types),
+                interpolator, animationType,
+                getLayoutInsetsDuringAnimationMode(types, fromPredictiveBack),
                 false /* useInsetsAnimationThread */, null /* statsToken */);
     }
 
@@ -1526,7 +1530,12 @@
     }
 
     private @LayoutInsetsDuringAnimation int getLayoutInsetsDuringAnimationMode(
-            @InsetsType int types) {
+            @InsetsType int types, boolean fromPredictiveBack) {
+        if (fromPredictiveBack) {
+            // When insets are animated by predictive back, we want insets to be shown to prevent a
+            // jump cut from shown to hidden at the start of the predictive back animation
+            return LAYOUT_INSETS_DURING_ANIMATION_SHOWN;
+        }
         // Generally, we want to layout the opposite of the current state. This is to make animation
         // callbacks easy to use: The can capture the layout values and then treat that as end-state
         // during the animation.
@@ -1730,7 +1739,7 @@
         return ANIMATION_TYPE_NONE;
     }
 
-    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    @VisibleForTesting(visibility = PACKAGE)
     public void setRequestedVisibleTypes(@InsetsType int visibleTypes, @InsetsType int mask) {
         final @InsetsType int requestedVisibleTypes =
                 (mRequestedVisibleTypes & ~mask) | (visibleTypes & mask);
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index dd548c6..0d9e471 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -110,10 +110,12 @@
 import static android.view.flags.Flags.toolkitFrameRateTypingReadOnly;
 import static android.view.flags.Flags.toolkitMetricsForFrameRateDecision;
 import static android.view.flags.Flags.toolkitSetFrameRateReadOnly;
+import static android.view.flags.Flags.toolkitFrameRateFunctionEnablingReadOnly;
 import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.IME_FOCUS_CONTROLLER;
 import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.INSETS_CONTROLLER;
 
 import static com.android.input.flags.Flags.enablePointerChoreographer;
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
 import static com.android.window.flags.Flags.activityWindowInfoFlag;
 import static com.android.window.flags.Flags.enableBufferTransformHintFromDisplay;
 import static com.android.window.flags.Flags.setScPropertiesInClient;
@@ -941,6 +943,7 @@
                     new InputEventConsistencyVerifier(this, 0) : null;
 
     private final InsetsController mInsetsController;
+    private final ImeBackAnimationController mImeBackAnimationController;
     private final ImeFocusController mImeFocusController;
 
     private boolean mIsSurfaceOpaque;
@@ -1148,6 +1151,7 @@
     private String mLargestViewTraceName;
 
     private static boolean sToolkitSetFrameRateReadOnlyFlagValue;
+    private static boolean sToolkitFrameRateFunctionEnablingReadOnlyFlagValue;
     private static boolean sToolkitMetricsForFrameRateDecisionFlagValue;
     private static boolean sToolkitFrameRateTypingReadOnlyFlagValue;
 
@@ -1155,6 +1159,8 @@
         sToolkitSetFrameRateReadOnlyFlagValue = toolkitSetFrameRateReadOnly();
         sToolkitMetricsForFrameRateDecisionFlagValue = toolkitMetricsForFrameRateDecision();
         sToolkitFrameRateTypingReadOnlyFlagValue = toolkitFrameRateTypingReadOnly();
+        sToolkitFrameRateFunctionEnablingReadOnlyFlagValue =
+                toolkitFrameRateFunctionEnablingReadOnly();
     }
 
     // The latest input event from the gesture that was used to resolve the pointer icon.
@@ -1202,6 +1208,7 @@
         // TODO(b/222696368): remove getSfInstance usage and use vsyncId for transactions
         mChoreographer = Choreographer.getInstance();
         mInsetsController = new InsetsController(new ViewRootInsetsControllerHost(this));
+        mImeBackAnimationController = new ImeBackAnimationController(this);
         mHandwritingInitiator = new HandwritingInitiator(
                 mViewConfiguration,
                 mContext.getSystemService(InputMethodManager.class));
@@ -3177,7 +3184,7 @@
                         == LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
     }
 
-    @VisibleForTesting
+    @VisibleForTesting(visibility = PACKAGE)
     public InsetsController getInsetsController() {
         return mInsetsController;
     }
@@ -10166,13 +10173,18 @@
     final class ConsumeBatchedInputRunnable implements Runnable {
         @Override
         public void run() {
-            mConsumeBatchedInputScheduled = false;
-            if (doConsumeBatchedInput(mChoreographer.getFrameTimeNanos())) {
-                // If we consumed a batch here, we want to go ahead and schedule the
-                // consumption of batched input events on the next frame. Otherwise, we would
-                // wait until we have more input events pending and might get starved by other
-                // things occurring in the process.
-                scheduleConsumeBatchedInput();
+            Trace.traceBegin(TRACE_TAG_VIEW, mTag);
+            try {
+                mConsumeBatchedInputScheduled = false;
+                if (doConsumeBatchedInput(mChoreographer.getFrameTimeNanos())) {
+                    // If we consumed a batch here, we want to go ahead and schedule the
+                    // consumption of batched input events on the next frame. Otherwise, we would
+                    // wait until we have more input events pending and might get starved by other
+                    // things occurring in the process.
+                    scheduleConsumeBatchedInput();
+                }
+            } finally {
+                Trace.traceEnd(TRACE_TAG_VIEW);
             }
         }
     }
@@ -12144,7 +12156,8 @@
                             + "IWindow:%s Session:%s",
                     mOnBackInvokedDispatcher, mBasePackageName, mWindow, mWindowSession));
         }
-        mOnBackInvokedDispatcher.attachToWindow(mWindowSession, mWindow);
+        mOnBackInvokedDispatcher.attachToWindow(mWindowSession, mWindow,
+                mImeBackAnimationController);
     }
 
     private void sendBackKeyEvent(int action) {
@@ -12788,7 +12801,9 @@
 
     private boolean shouldEnableDvrr() {
         // uncomment this when we are ready for enabling dVRR
-        // return sToolkitSetFrameRateReadOnlyFlagValue && isFrameRatePowerSavingsBalanced();
+        if (sToolkitFrameRateFunctionEnablingReadOnlyFlagValue) {
+            return sToolkitSetFrameRateReadOnlyFlagValue && isFrameRatePowerSavingsBalanced();
+        }
         return false;
     }
 
diff --git a/core/java/android/view/flags/refresh_rate_flags.aconfig b/core/java/android/view/flags/refresh_rate_flags.aconfig
index 06598b3..1d4d18b 100644
--- a/core/java/android/view/flags/refresh_rate_flags.aconfig
+++ b/core/java/android/view/flags/refresh_rate_flags.aconfig
@@ -86,4 +86,12 @@
     description: "Feature flag for suppressing boost on typing"
     bug: "239979904"
     is_fixed_read_only: true
+}
+
+flag {
+    name: "toolkit_frame_rate_function_enabling_read_only"
+    namespace: "toolkit"
+    description: "Feature flag to enable the functionality of the dVRR feature"
+    bug: "239979904"
+    is_fixed_read_only: true
 }
\ No newline at end of file
diff --git a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
index cedf8d0..f454a6a 100644
--- a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
+++ b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
@@ -770,6 +770,20 @@
     }
 
     @AnyThread
+    @RequiresPermission(Manifest.permission.TEST_INPUT_METHOD)
+    static void finishTrackingPendingImeVisibilityRequests() {
+        final var service = getImeTrackerService();
+        if (service == null) {
+            return;
+        }
+        try {
+            service.finishTrackingPendingImeVisibilityRequests();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @AnyThread
     @Nullable
     private static IImeTracker getImeTrackerService() {
         var trackerService = sTrackerServiceCache;
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index 8efb201..80b2396 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -4316,6 +4316,19 @@
     }
 
     /**
+     * A test API for CTS to finish the tracking of any pending IME visibility requests. This
+     * won't stop the actual requests, but allows resetting the state when starting up test runs.
+     *
+     * @hide
+     */
+    @SuppressLint("UnflaggedApi") // @TestApi without associated feature.
+    @TestApi
+    @RequiresPermission(Manifest.permission.TEST_INPUT_METHOD)
+    public void finishTrackingPendingImeVisibilityRequests() {
+        IInputMethodManagerGlobalInvoker.finishTrackingPendingImeVisibilityRequests();
+    }
+
+    /**
      * Show the settings for enabling subtypes of the specified input method.
      *
      * @param imiId An input method, whose subtypes settings will be shown. If imiId is null,
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index f54ef38..ab6b512 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -16,6 +16,8 @@
 
 package android.widget;
 
+import static android.view.flags.Flags.viewVelocityApi;
+
 import android.annotation.ColorInt;
 import android.annotation.DrawableRes;
 import android.annotation.NonNull;
@@ -5098,6 +5100,11 @@
                 boolean more = scroller.computeScrollOffset();
                 final int y = scroller.getCurrY();
 
+                // For variable refresh rate project to track the current velocity of this View
+                if (viewVelocityApi()) {
+                    setFrameContentVelocity(Math.abs(mScroller.getCurrVelocity()));
+                }
+
                 // Flip sign to convert finger direction to list items direction
                 // (e.g. finger moving down means list is moving towards the top)
                 int delta = consumeFlingInStretch(mLastFlingY - y);
@@ -5192,6 +5199,10 @@
                         invalidate();
                         postOnAnimation(this);
                     }
+                    // For variable refresh rate project to track the current velocity of this View
+                    if (viewVelocityApi()) {
+                        setFrameContentVelocity(Math.abs(mScroller.getCurrVelocity()));
+                    }
                 } else {
                     endFling();
                 }
diff --git a/core/java/android/window/ImeOnBackInvokedDispatcher.java b/core/java/android/window/ImeOnBackInvokedDispatcher.java
index 0cc9a0d..da1993d 100644
--- a/core/java/android/window/ImeOnBackInvokedDispatcher.java
+++ b/core/java/android/window/ImeOnBackInvokedDispatcher.java
@@ -148,8 +148,17 @@
             @OnBackInvokedDispatcher.Priority int priority,
             int callbackId,
             @NonNull WindowOnBackInvokedDispatcher receivingDispatcher) {
-        final ImeOnBackInvokedCallback imeCallback =
-                new ImeOnBackInvokedCallback(iCallback, callbackId, priority);
+        final ImeOnBackInvokedCallback imeCallback;
+        if (priority == PRIORITY_SYSTEM) {
+            // A callback registration with PRIORITY_SYSTEM indicates that a predictive back
+            // animation can be played on the IME. Therefore register the
+            // DefaultImeOnBackInvokedCallback with the receiving dispatcher and override the
+            // priority to PRIORITY_DEFAULT.
+            priority = PRIORITY_DEFAULT;
+            imeCallback = new DefaultImeOnBackAnimationCallback(iCallback, callbackId, priority);
+        } else {
+            imeCallback = new ImeOnBackInvokedCallback(iCallback, callbackId, priority);
+        }
         mImeCallbacks.add(imeCallback);
         receivingDispatcher.registerOnBackInvokedCallbackUnchecked(imeCallback, priority);
     }
@@ -230,6 +239,17 @@
     }
 
     /**
+     * Subclass of ImeOnBackInvokedCallback indicating that a predictive IME back animation may be
+     * played instead of invoking the callback.
+     */
+    static class DefaultImeOnBackAnimationCallback extends ImeOnBackInvokedCallback {
+        DefaultImeOnBackAnimationCallback(@NonNull IOnBackInvokedCallback iCallback, int id,
+                int priority) {
+            super(iCallback, id, priority);
+        }
+    }
+
+    /**
      * Transfers {@link ImeOnBackInvokedCallback}s registered on one {@link ViewRootImpl} to
      * another {@link ViewRootImpl} on focus change.
      *
diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java
index 45d7767..bcbac93 100644
--- a/core/java/android/window/WindowOnBackInvokedDispatcher.java
+++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java
@@ -31,6 +31,7 @@
 import android.util.Log;
 import android.view.IWindow;
 import android.view.IWindowSession;
+import android.view.ImeBackAnimationController;
 
 import androidx.annotation.VisibleForTesting;
 
@@ -71,6 +72,9 @@
     @Nullable
     private ImeOnBackInvokedDispatcher mImeDispatcher;
 
+    @Nullable
+    private ImeBackAnimationController mImeBackAnimationController;
+
     /** Convenience hashmap to quickly decide if a callback has been added. */
     private final HashMap<OnBackInvokedCallback, Integer> mAllCallbacks = new HashMap<>();
     /** Holds all callbacks by priorities. */
@@ -88,9 +92,11 @@
      * Sends the pending top callback (if one exists) to WM when the view root
      * is attached a window.
      */
-    public void attachToWindow(@NonNull IWindowSession windowSession, @NonNull IWindow window) {
+    public void attachToWindow(@NonNull IWindowSession windowSession, @NonNull IWindow window,
+            @Nullable ImeBackAnimationController imeBackAnimationController) {
         mWindowSession = windowSession;
         mWindow = window;
+        mImeBackAnimationController = imeBackAnimationController;
         if (!mAllCallbacks.isEmpty()) {
             setTopOnBackInvokedCallback(getTopCallback());
         }
@@ -101,6 +107,7 @@
         clear();
         mWindow = null;
         mWindowSession = null;
+        mImeBackAnimationController = null;
     }
 
     // TODO: Take an Executor for the callback to run on.
@@ -125,6 +132,9 @@
         if (!mOnBackInvokedCallbacks.containsKey(priority)) {
             mOnBackInvokedCallbacks.put(priority, new ArrayList<>());
         }
+        if (callback instanceof ImeOnBackInvokedDispatcher.DefaultImeOnBackAnimationCallback) {
+            callback = mImeBackAnimationController;
+        }
         ArrayList<OnBackInvokedCallback> callbacks = mOnBackInvokedCallbacks.get(priority);
 
         // If callback has already been added, remove it and re-add it.
@@ -152,6 +162,9 @@
             mImeDispatcher.unregisterOnBackInvokedCallback(callback);
             return;
         }
+        if (callback instanceof ImeOnBackInvokedDispatcher.DefaultImeOnBackAnimationCallback) {
+            callback = mImeBackAnimationController;
+        }
         if (!mAllCallbacks.containsKey(callback)) {
             if (DEBUG) {
                 Log.i(TAG, "Callback not found. returning...");
@@ -199,7 +212,7 @@
             }
         } else {
             Log.w(TAG, "sendCancelIfRunning: isInProgress=" + isInProgress
-                    + "callback=" + callback);
+                    + " callback=" + callback);
         }
     }
 
@@ -243,9 +256,9 @@
                 int priority = mAllCallbacks.get(callback);
                 final IOnBackInvokedCallback iCallback =
                         callback instanceof ImeOnBackInvokedDispatcher
-                                    .ImeOnBackInvokedCallback
+                                .ImeOnBackInvokedCallback
                                 ? ((ImeOnBackInvokedDispatcher.ImeOnBackInvokedCallback)
-                                        callback).getIOnBackInvokedCallback()
+                                callback).getIOnBackInvokedCallback()
                                 : new OnBackInvokedCallbackWrapper(
                                         callback,
                                         mProgressAnimator,
diff --git a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
index 7fbec67..fa0dab0 100644
--- a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
+++ b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig
@@ -86,3 +86,10 @@
   description: "Whether the blurred letterbox wallpaper background is enabled by default"
   bug: "297195682"
 }
+
+flag {
+    name: "enable_compatui_sysui_launcher"
+    namespace: "large_screen_experiences_app_compat"
+    description: "Enables sysui animation for user aspect ratio button"
+    bug: "300357441"
+}
\ No newline at end of file
diff --git a/core/java/com/android/internal/inputmethod/IImeTracker.aidl b/core/java/com/android/internal/inputmethod/IImeTracker.aidl
index b45bc1c..ab4edb6 100644
--- a/core/java/com/android/internal/inputmethod/IImeTracker.aidl
+++ b/core/java/com/android/internal/inputmethod/IImeTracker.aidl
@@ -86,4 +86,13 @@
     @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
             + "android.Manifest.permission.TEST_INPUT_METHOD)")
     boolean hasPendingImeVisibilityRequests();
+
+    /**
+     * Finishes the tracking of any pending IME visibility requests. This won't stop the actual
+     * requests, but allows resetting the state when starting up test runs.
+     */
+    @EnforcePermission("TEST_INPUT_METHOD")
+    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
+            + "android.Manifest.permission.TEST_INPUT_METHOD)")
+    oneway void finishTrackingPendingImeVisibilityRequests();
 }
diff --git a/core/java/com/android/internal/net/ConnectivityBlobStore.java b/core/java/com/android/internal/net/ConnectivityBlobStore.java
index 1b18485..f8eb5be 100644
--- a/core/java/com/android/internal/net/ConnectivityBlobStore.java
+++ b/core/java/com/android/internal/net/ConnectivityBlobStore.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.content.ContentValues;
 import android.database.Cursor;
+import android.database.DatabaseUtils;
 import android.database.SQLException;
 import android.database.sqlite.SQLiteDatabase;
 import android.os.Binder;
@@ -153,8 +154,11 @@
         final List<String> names = new ArrayList<String>();
         try (Cursor cursor = mDb.query(TABLENAME,
                 new String[] {"name"} /* columns */,
-                "owner=? AND name LIKE ?" /* selection */,
-                new String[] {Integer.toString(ownerUid), prefix + "%"} /* selectionArgs */,
+                "owner=? AND name LIKE ? ESCAPE '\\'" /* selection */,
+                new String[] {
+                        Integer.toString(ownerUid),
+                        DatabaseUtils.escapeForLike(prefix) + "%"
+                } /* selectionArgs */,
                 null /* groupBy */,
                 null /* having */,
                 "name ASC" /* orderBy */)) {
diff --git a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java
index 9df93f9..e12becd 100644
--- a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java
+++ b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java
@@ -407,8 +407,10 @@
      */
     private ParseResult<ParsingPackage> parseMonolithicPackage(ParseInput input, File apkFile,
             int flags) {
+        // The signature parsing will be done later in method parseBaseApk.
+        int liteParseFlags = flags & ~PARSE_COLLECT_CERTIFICATES;
         final ParseResult<PackageLite> liteResult =
-                ApkLiteParseUtils.parseMonolithicPackageLite(input, apkFile, flags);
+                ApkLiteParseUtils.parseMonolithicPackageLite(input, apkFile, liteParseFlags);
         if (liteResult.isError()) {
             return input.error(liteResult);
         }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/CompanionOperation.java b/core/java/com/android/internal/widget/remotecompose/core/CompanionOperation.java
index ce8ca0d..2d36536 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/CompanionOperation.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/CompanionOperation.java
@@ -21,6 +21,11 @@
  * Interface for the companion operations
  */
 public interface CompanionOperation {
+    /**
+     * Read, create and add instance to operations
+     * @param buffer data to read to create operation
+     * @param operations command is to be added
+     */
     void read(WireBuffer buffer, List<Operation> operations);
 
     // Debugging / Documentation utility functions
diff --git a/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java b/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java
index 0e4c743..55f2dee 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java
@@ -331,6 +331,7 @@
     public void initFromBuffer(RemoteComposeBuffer buffer) {
         mOperations = new ArrayList<Operation>();
         buffer.inflateFromBuffer(mOperations);
+        mBuffer = buffer;
     }
 
     /**
diff --git a/core/java/com/android/internal/widget/remotecompose/core/Operations.java b/core/java/com/android/internal/widget/remotecompose/core/Operations.java
index b8bb1f0..54b277a 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/Operations.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/Operations.java
@@ -17,8 +17,29 @@
 
 import com.android.internal.widget.remotecompose.core.operations.BitmapData;
 import com.android.internal.widget.remotecompose.core.operations.ClickArea;
+import com.android.internal.widget.remotecompose.core.operations.ClipPath;
+import com.android.internal.widget.remotecompose.core.operations.ClipRect;
+import com.android.internal.widget.remotecompose.core.operations.DrawArc;
+import com.android.internal.widget.remotecompose.core.operations.DrawBitmap;
 import com.android.internal.widget.remotecompose.core.operations.DrawBitmapInt;
+import com.android.internal.widget.remotecompose.core.operations.DrawCircle;
+import com.android.internal.widget.remotecompose.core.operations.DrawLine;
+import com.android.internal.widget.remotecompose.core.operations.DrawOval;
+import com.android.internal.widget.remotecompose.core.operations.DrawPath;
+import com.android.internal.widget.remotecompose.core.operations.DrawRect;
+import com.android.internal.widget.remotecompose.core.operations.DrawRoundRect;
+import com.android.internal.widget.remotecompose.core.operations.DrawTextOnPath;
+import com.android.internal.widget.remotecompose.core.operations.DrawTextRun;
+import com.android.internal.widget.remotecompose.core.operations.DrawTweenPath;
 import com.android.internal.widget.remotecompose.core.operations.Header;
+import com.android.internal.widget.remotecompose.core.operations.MatrixRestore;
+import com.android.internal.widget.remotecompose.core.operations.MatrixRotate;
+import com.android.internal.widget.remotecompose.core.operations.MatrixSave;
+import com.android.internal.widget.remotecompose.core.operations.MatrixScale;
+import com.android.internal.widget.remotecompose.core.operations.MatrixSkew;
+import com.android.internal.widget.remotecompose.core.operations.MatrixTranslate;
+import com.android.internal.widget.remotecompose.core.operations.PaintData;
+import com.android.internal.widget.remotecompose.core.operations.PathData;
 import com.android.internal.widget.remotecompose.core.operations.RootContentBehavior;
 import com.android.internal.widget.remotecompose.core.operations.RootContentDescription;
 import com.android.internal.widget.remotecompose.core.operations.TextData;
@@ -48,7 +69,30 @@
     public static final int DATA_BITMAP = 101;
     public static final int DATA_TEXT = 102;
 
+/////////////////////////////=====================
+    public static final int CLIP_PATH = 38;
+    public static final int CLIP_RECT = 39;
+    public static final int PAINT_VALUES = 40;
+    public static final int DRAW_RECT = 42;
+    public static final int DRAW_TEXT_RUN = 43;
+    public static final int DRAW_CIRCLE = 46;
+    public static final int DRAW_LINE = 47;
+    public static final int DRAW_ROUND_RECT = 51;
+    public static final int DRAW_ARC = 52;
+    public static final int DRAW_TEXT_ON_PATH = 53;
+    public static final int DRAW_OVAL = 56;
+    public static final int DATA_PATH = 123;
+    public static final int DRAW_PATH = 124;
+    public static final int DRAW_TWEEN_PATH = 125;
+    public static final int MATRIX_SCALE = 126;
+    public static final int MATRIX_TRANSLATE = 127;
+    public static final int MATRIX_SKEW = 128;
+    public static final int MATRIX_ROTATE = 129;
+    public static final int MATRIX_SAVE = 130;
+    public static final int MATRIX_RESTORE = 131;
+    public static final int MATRIX_SET = 132;
 
+    /////////////////////////////////////////======================
     public static IntMap<CompanionOperation> map = new IntMap<>();
 
     static {
@@ -60,6 +104,29 @@
         map.put(CLICK_AREA, ClickArea.COMPANION);
         map.put(ROOT_CONTENT_BEHAVIOR, RootContentBehavior.COMPANION);
         map.put(ROOT_CONTENT_DESCRIPTION, RootContentDescription.COMPANION);
+
+        map.put(DRAW_ARC, DrawArc.COMPANION);
+        map.put(DRAW_BITMAP, DrawBitmap.COMPANION);
+        map.put(DRAW_CIRCLE, DrawCircle.COMPANION);
+        map.put(DRAW_LINE, DrawLine.COMPANION);
+        map.put(DRAW_OVAL, DrawOval.COMPANION);
+        map.put(DRAW_PATH, DrawPath.COMPANION);
+        map.put(DRAW_RECT, DrawRect.COMPANION);
+        map.put(DRAW_ROUND_RECT, DrawRoundRect.COMPANION);
+        map.put(DRAW_TEXT_ON_PATH, DrawTextOnPath.COMPANION);
+        map.put(DRAW_TEXT_RUN, DrawTextRun.COMPANION);
+        map.put(DRAW_TWEEN_PATH, DrawTweenPath.COMPANION);
+        map.put(DATA_PATH, PathData.COMPANION);
+        map.put(PAINT_VALUES, PaintData.COMPANION);
+        map.put(MATRIX_RESTORE, MatrixRestore.COMPANION);
+        map.put(MATRIX_ROTATE, MatrixRotate.COMPANION);
+        map.put(MATRIX_SAVE, MatrixSave.COMPANION);
+        map.put(MATRIX_SCALE, MatrixScale.COMPANION);
+        map.put(MATRIX_SKEW, MatrixSkew.COMPANION);
+        map.put(MATRIX_TRANSLATE, MatrixTranslate.COMPANION);
+        map.put(CLIP_PATH, ClipPath.COMPANION);
+        map.put(CLIP_RECT, ClipRect.COMPANION);
+
     }
 
 }
diff --git a/core/java/com/android/internal/widget/remotecompose/core/PaintContext.java b/core/java/com/android/internal/widget/remotecompose/core/PaintContext.java
index 6999cde..eece8ad52 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/PaintContext.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/PaintContext.java
@@ -15,6 +15,8 @@
  */
 package com.android.internal.widget.remotecompose.core;
 
+import com.android.internal.widget.remotecompose.core.operations.paint.PaintBundle;
+
 /**
  * Specify an abstract paint context used by RemoteCompose commands to draw
  */
@@ -30,11 +32,74 @@
     }
 
     public abstract void drawBitmap(int imageId,
-                             int srcLeft, int srcTop, int srcRight, int srcBottom,
-                             int dstLeft, int dstTop, int dstRight, int dstBottom,
-                             int cdId);
+                                    int srcLeft, int srcTop, int srcRight, int srcBottom,
+                                    int dstLeft, int dstTop, int dstRight, int dstBottom,
+                                    int cdId);
 
     public abstract void scale(float scaleX, float scaleY);
+
     public abstract void translate(float translateX, float translateY);
+
+    public abstract void drawArc(float left,
+                                 float top,
+                                 float right,
+                                 float bottom,
+                                 float startAngle,
+                                 float sweepAngle);
+
+    public abstract void drawBitmap(int id, float left, float top, float right, float bottom);
+
+    public abstract void drawCircle(float centerX, float centerY, float radius);
+
+    public abstract void drawLine(float x1, float y1, float x2, float y2);
+
+    public abstract void drawOval(float left, float top, float right, float bottom);
+
+    public abstract void drawPath(int id, float start, float end);
+
+    public abstract void drawRect(float left, float top, float right, float bottom);
+
+    public abstract void drawRoundRect(float left,
+                                       float top,
+                                       float right,
+                                       float bottom,
+                                       float radiusX,
+                                       float radiusY);
+
+    public abstract void drawTextOnPath(int textId, int pathId, float hOffset, float vOffset);
+
+    public abstract void drawTextRun(int textID,
+                                     int start,
+                                     int end,
+                                     int contextStart,
+                                     int contextEnd,
+                                     float x,
+                                     float y,
+                                     boolean rtl);
+
+    public abstract void drawTweenPath(int path1Id,
+                                       int path2Id,
+                                       float tween,
+                                       float start,
+                                       float stop);
+
+    public abstract void applyPaint(PaintBundle mPaintData);
+
+    public abstract void mtrixScale(float scaleX, float scaleY, float centerX, float centerY);
+
+    public abstract void matrixTranslate(float translateX, float translateY);
+
+    public abstract void matrixSkew(float skewX, float skewY);
+
+    public abstract void matrixRotate(float rotate, float pivotX, float pivotY);
+
+    public abstract void matrixSave();
+
+    public abstract void matrixRestore();
+
+    public abstract void clipRect(float left, float top, float right, float bottom);
+
+    public abstract void clipPath(int pathId, int regionOp);
+
 }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/Platform.java b/core/java/com/android/internal/widget/remotecompose/core/Platform.java
index abda0c0..903dab4 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/Platform.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/Platform.java
@@ -20,5 +20,8 @@
  */
 public interface Platform {
     byte[] imageToByteArray(Object image);
+    int getImageWidth(Object image);
+    int getImageHeight(Object image);
+    float[] pathToFloatArray(Object image);
 }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java
index c34730f..c2e8131 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java
@@ -17,12 +17,34 @@
 
 import com.android.internal.widget.remotecompose.core.operations.BitmapData;
 import com.android.internal.widget.remotecompose.core.operations.ClickArea;
+import com.android.internal.widget.remotecompose.core.operations.ClipPath;
+import com.android.internal.widget.remotecompose.core.operations.ClipRect;
+import com.android.internal.widget.remotecompose.core.operations.DrawArc;
+import com.android.internal.widget.remotecompose.core.operations.DrawBitmap;
 import com.android.internal.widget.remotecompose.core.operations.DrawBitmapInt;
+import com.android.internal.widget.remotecompose.core.operations.DrawCircle;
+import com.android.internal.widget.remotecompose.core.operations.DrawLine;
+import com.android.internal.widget.remotecompose.core.operations.DrawOval;
+import com.android.internal.widget.remotecompose.core.operations.DrawPath;
+import com.android.internal.widget.remotecompose.core.operations.DrawRect;
+import com.android.internal.widget.remotecompose.core.operations.DrawRoundRect;
+import com.android.internal.widget.remotecompose.core.operations.DrawTextOnPath;
+import com.android.internal.widget.remotecompose.core.operations.DrawTextRun;
+import com.android.internal.widget.remotecompose.core.operations.DrawTweenPath;
 import com.android.internal.widget.remotecompose.core.operations.Header;
+import com.android.internal.widget.remotecompose.core.operations.MatrixRestore;
+import com.android.internal.widget.remotecompose.core.operations.MatrixRotate;
+import com.android.internal.widget.remotecompose.core.operations.MatrixSave;
+import com.android.internal.widget.remotecompose.core.operations.MatrixScale;
+import com.android.internal.widget.remotecompose.core.operations.MatrixSkew;
+import com.android.internal.widget.remotecompose.core.operations.MatrixTranslate;
+import com.android.internal.widget.remotecompose.core.operations.PaintData;
+import com.android.internal.widget.remotecompose.core.operations.PathData;
 import com.android.internal.widget.remotecompose.core.operations.RootContentBehavior;
 import com.android.internal.widget.remotecompose.core.operations.RootContentDescription;
 import com.android.internal.widget.remotecompose.core.operations.TextData;
 import com.android.internal.widget.remotecompose.core.operations.Theme;
+import com.android.internal.widget.remotecompose.core.operations.paint.PaintBundle;
 
 import java.io.File;
 import java.io.FileInputStream;
@@ -82,10 +104,10 @@
     /**
      * Insert a header
      *
-     * @param width        the width of the document in pixels
-     * @param height       the height of the document in pixels
+     * @param width              the width of the document in pixels
+     * @param height             the height of the document in pixels
      * @param contentDescription content description of the document
-     * @param capabilities bitmask indicating needed capabilities (unused for now)
+     * @param capabilities       bitmask indicating needed capabilities (unused for now)
      */
     public void header(int width, int height, String contentDescription, long capabilities) {
         Header.COMPANION.apply(mBuffer, width, height, capabilities);
@@ -99,8 +121,8 @@
     /**
      * Insert a header
      *
-     * @param width  the width of the document in pixels
-     * @param height the height of the document in pixels
+     * @param width              the width of the document in pixels
+     * @param height             the height of the document in pixels
      * @param contentDescription content description of the document
      */
     public void header(int width, int height, String contentDescription) {
@@ -111,7 +133,7 @@
      * Insert a bitmap
      *
      * @param image       an opaque image that we'll add to the buffer
-     * @param imageWidth the width of the image
+     * @param imageWidth  the width of the image
      * @param imageHeight the height of the image
      * @param srcLeft     left coordinate of the source area
      * @param srcTop      top coordinate of the source area
@@ -161,13 +183,13 @@
     /**
      * Add a click area to the document
      *
-     * @param id       the id of the click area, reported in the click listener callback
+     * @param id                 the id of the click area, reported in the click listener callback
      * @param contentDescription the content description of that click area (accessibility)
-     * @param left     left coordinate of the area bounds
-     * @param top      top coordinate of the area bounds
-     * @param right    right coordinate of the area bounds
-     * @param bottom   bottom coordinate of the area bounds
-     * @param metadata associated metadata, user-provided
+     * @param left               left coordinate of the area bounds
+     * @param top                top coordinate of the area bounds
+     * @param right              right coordinate of the area bounds
+     * @param bottom             bottom coordinate of the area bounds
+     * @param metadata           associated metadata, user-provided
      */
     public void addClickArea(
             int id,
@@ -193,35 +215,294 @@
     /**
      * Sets the way the player handles the content
      *
-     * @param scroll set the horizontal behavior (NONE|SCROLL_HORIZONTAL|SCROLL_VERTICAL)
+     * @param scroll    set the horizontal behavior (NONE|SCROLL_HORIZONTAL|SCROLL_VERTICAL)
      * @param alignment set the alignment of the content (TOP|CENTER|BOTTOM|START|END)
-     * @param sizing set the type of sizing for the content (NONE|SIZING_LAYOUT|SIZING_SCALE)
-     * @param mode set the mode of sizing, either LAYOUT modes or SCALE modes
-     *             the LAYOUT modes are:
-     *             - LAYOUT_MATCH_PARENT
-     *             - LAYOUT_WRAP_CONTENT
-     *             or adding an horizontal mode and a vertical mode:
-     *             - LAYOUT_HORIZONTAL_MATCH_PARENT
-     *             - LAYOUT_HORIZONTAL_WRAP_CONTENT
-     *             - LAYOUT_HORIZONTAL_FIXED
-     *             - LAYOUT_VERTICAL_MATCH_PARENT
-     *             - LAYOUT_VERTICAL_WRAP_CONTENT
-     *             - LAYOUT_VERTICAL_FIXED
-     *             The LAYOUT_*_FIXED modes will use the intrinsic document size
+     * @param sizing    set the type of sizing for the content (NONE|SIZING_LAYOUT|SIZING_SCALE)
+     * @param mode      set the mode of sizing, either LAYOUT modes or SCALE modes
+     *                  the LAYOUT modes are:
+     *                  - LAYOUT_MATCH_PARENT
+     *                  - LAYOUT_WRAP_CONTENT
+     *                  or adding an horizontal mode and a vertical mode:
+     *                  - LAYOUT_HORIZONTAL_MATCH_PARENT
+     *                  - LAYOUT_HORIZONTAL_WRAP_CONTENT
+     *                  - LAYOUT_HORIZONTAL_FIXED
+     *                  - LAYOUT_VERTICAL_MATCH_PARENT
+     *                  - LAYOUT_VERTICAL_WRAP_CONTENT
+     *                  - LAYOUT_VERTICAL_FIXED
+     *                  The LAYOUT_*_FIXED modes will use the intrinsic document size
      */
     public void setRootContentBehavior(int scroll, int alignment, int sizing, int mode) {
         RootContentBehavior.COMPANION.apply(mBuffer, scroll, alignment, sizing, mode);
     }
 
+    /**
+     * add Drawing the specified arc, which will be scaled to fit inside the specified oval.
+     * <br>
+     * If the start angle is negative or >= 360, the start angle is treated as start angle modulo
+     * 360.
+     * <br>
+     * If the sweep angle is >= 360, then the oval is drawn completely. Note that this differs
+     * slightly from SkPath::arcTo, which treats the sweep angle modulo 360. If the sweep angle is
+     * negative, the sweep angle is treated as sweep angle modulo 360
+     * <br>
+     * The arc is drawn clockwise. An angle of 0 degrees correspond to the geometric angle of 0
+     * degrees (3 o'clock on a watch.)
+     * <br>
+     *
+     * @param left       left coordinate of oval used to define the shape and size of the arc
+     * @param top        top coordinate of oval used to define the shape and size of the arc
+     * @param right      right coordinate of oval used to define the shape and size of the arc
+     * @param bottom     bottom coordinate of oval used to define the shape and size of the arc
+     * @param startAngle Starting angle (in degrees) where the arc begins
+     * @param sweepAngle Sweep angle (in degrees) measured clockwise
+     */
+    public void addDrawArc(float left,
+                           float top,
+                           float right,
+                           float bottom,
+                           float startAngle,
+                           float sweepAngle) {
+        DrawArc.COMPANION.apply(mBuffer, left, top, right, bottom, startAngle, sweepAngle);
+    }
+
+    /**
+     * @param image              The bitmap to be drawn
+     * @param left               left coordinate of rectangle that the bitmap will be to fit into
+     * @param top                top coordinate of rectangle that the bitmap will be to fit into
+     * @param right              right coordinate of rectangle that the bitmap will be to fit into
+     * @param bottom             bottom coordinate of rectangle that the bitmap will be to fit into
+     * @param contentDescription content description of the image
+     */
+    public void addDrawBitmap(Object image,
+                              float left,
+                              float top,
+                              float right,
+                              float bottom,
+                              String contentDescription) {
+        int imageId = mRemoteComposeState.dataGetId(image);
+        if (imageId == -1) {
+            imageId = mRemoteComposeState.cache(image);
+            byte[] data = mPlatform.imageToByteArray(image);
+            int imageWidth = mPlatform.getImageWidth(image);
+            int imageHeight = mPlatform.getImageHeight(image);
+
+            BitmapData.COMPANION.apply(mBuffer, imageId, imageWidth, imageHeight, data);
+        }
+        int contentDescriptionId = 0;
+        if (contentDescription != null) {
+            contentDescriptionId = addText(contentDescription);
+        }
+        DrawBitmap.COMPANION.apply(
+                mBuffer, imageId, left, top, right, bottom, contentDescriptionId
+        );
+    }
+
+    /**
+     * Draw the specified circle using the specified paint. If radius is <= 0, then nothing will be
+     * drawn.
+     *
+     * @param centerX The x-coordinate of the center of the circle to be drawn
+     * @param centerY The y-coordinate of the center of the circle to be drawn
+     * @param radius  The radius of the circle to be drawn
+     */
+    public void addDrawCircle(float centerX, float centerY, float radius) {
+        DrawCircle.COMPANION.apply(mBuffer, centerX, centerY, radius);
+    }
+
+    /**
+     * Draw a line segment with the specified start and stop x,y coordinates, using the specified
+     * paint.
+     *
+     * @param x1 The x-coordinate of the start point of the line
+     * @param y1 The y-coordinate of the start point of the line
+     * @param x2 The x-coordinate of the end point of the line
+     * @param y2 The y-coordinate of the end point of the line
+     */
+    public void addDrawLine(float x1, float y1, float x2, float y2) {
+        DrawLine.COMPANION.apply(mBuffer, x1, y1, x2, y2);
+    }
+
+    /**
+     * Draw the specified oval using the specified paint.
+     *
+     * @param left   left coordinate of oval
+     * @param top    top coordinate of oval
+     * @param right  right coordinate of oval
+     * @param bottom bottom coordinate of oval
+     */
+    public void addDrawOval(float left, float top, float right, float bottom) {
+        DrawOval.COMPANION.apply(mBuffer, left, top, right, bottom);
+    }
+
+    /**
+     * Draw the specified path
+     * <p>
+     * Note: path objects are not immutable
+     * modifying them and calling this will not change the drawing
+     *
+     * @param path The path to be drawn
+     */
+    public void addDrawPath(Object path) {
+        int id = mRemoteComposeState.dataGetId(path);
+        if (id == -1) { // never been seen before
+            id = addPathData(path);
+        }
+        addDrawPath(id);
+    }
+
+
+    /**
+     * Draw the specified path
+     *
+     * @param pathId
+     */
+    public void addDrawPath(int pathId) {
+        DrawPath.COMPANION.apply(mBuffer, pathId);
+    }
+
+    /**
+     * Draw the specified Rect
+     *
+     * @param left   left coordinate of rectangle to be drawn
+     * @param top    top coordinate of rectangle to be drawn
+     * @param right  right coordinate of rectangle to be drawn
+     * @param bottom bottom coordinate of rectangle to be drawn
+     */
+    public void addDrawRect(float left, float top, float right, float bottom) {
+        DrawRect.COMPANION.apply(mBuffer, left, top, right, bottom);
+    }
+
+    /**
+     * Draw the specified round-rect
+     *
+     * @param left    left coordinate of rectangle to be drawn
+     * @param top     left coordinate of rectangle to be drawn
+     * @param right   left coordinate of rectangle to be drawn
+     * @param bottom  left coordinate of rectangle to be drawn
+     * @param radiusX The x-radius of the oval used to round the corners
+     * @param radiusY The y-radius of the oval used to round the corners
+     */
+    public void addDrawRoundRect(float left, float top, float right, float bottom,
+                                 float radiusX, float radiusY) {
+        DrawRoundRect.COMPANION.apply(mBuffer, left, top, right, bottom, radiusX, radiusY);
+    }
+
+    /**
+     * Draw the text, with origin at (x,y) along the specified path.
+     *
+     * @param text    The text to be drawn
+     * @param path    The path the text should follow for its baseline
+     * @param hOffset The distance along the path to add to the text's starting position
+     * @param vOffset The distance above(-) or below(+) the path to position the text
+     */
+    public void addDrawTextOnPath(String text, Object path, float hOffset, float vOffset) {
+        int pathId = mRemoteComposeState.dataGetId(path);
+        if (pathId == -1) { // never been seen before
+            pathId = addPathData(path);
+        }
+        int textId = addText(text);
+        DrawTextOnPath.COMPANION.apply(mBuffer, textId, pathId, hOffset, vOffset);
+    }
+
+    /**
+     * Draw the text, with origin at (x,y). The origin is interpreted
+     * based on the Align setting in the paint.
+     *
+     * @param text         The text to be drawn
+     * @param start        The index of the first character in text to draw
+     * @param end          (end - 1) is the index of the last character in text to draw
+     * @param contextStart
+     * @param contextEnd
+     * @param x            The x-coordinate of the origin of the text being drawn
+     * @param y            The y-coordinate of the baseline of the text being drawn
+     * @param rtl          Draw RTTL
+     */
+    public void addDrawTextRun(String text,
+                               int start,
+                               int end,
+                               int contextStart,
+                               int contextEnd,
+                               float x,
+                               float y,
+                               boolean rtl) {
+        int textId = addText(text);
+        DrawTextRun.COMPANION.apply(
+                mBuffer, textId, start, end,
+                contextStart, contextEnd, x, y, rtl);
+    }
+
+    /**
+     * draw an interpolation between two paths that have the same pattern
+     * <p>
+     * Warning paths objects are not immutable and this is not taken into consideration
+     *
+     * @param path1 The path1 to be drawn between
+     * @param path2 The path2 to be drawn between
+     * @param tween The ratio of path1 and path2 to 0 = all path 1, 1 = all path2
+     * @param start The start of the subrange of paths to draw 0 = start form start 0.5 is half way
+     * @param stop  The end of the subrange of paths to draw 1 = end at the end 0.5 is end half way
+     */
+    public void addDrawTweenPath(Object path1,
+                                 Object path2,
+                                 float tween,
+                                 float start,
+                                 float stop) {
+        int path1Id = mRemoteComposeState.dataGetId(path1);
+        if (path1Id == -1) { // never been seen before
+            path1Id = addPathData(path1);
+        }
+        int path2Id = mRemoteComposeState.dataGetId(path2);
+        if (path2Id == -1) { // never been seen before
+            path2Id = addPathData(path2);
+        }
+        addDrawTweenPath(path1Id, path2Id, tween, start, stop);
+    }
+
+    /**
+     * draw an interpolation between two paths that have the same pattern
+     *
+     * @param path1Id The path1 to be drawn between
+     * @param path2Id The path2 to be drawn between
+     * @param tween   The ratio of path1 and path2 to 0 = all path 1, 1 = all path2
+     * @param start   The start of the subrange of paths to draw 0 = start form start .5 is 1/2 way
+     * @param stop    The end of the subrange of paths to draw 1 = end at the end .5 is end 1/2 way
+     */
+    public void addDrawTweenPath(int path1Id,
+                                 int path2Id,
+                                 float tween,
+                                 float start,
+                                 float stop) {
+        DrawTweenPath.COMPANION.apply(
+                mBuffer, path1Id, path2Id,
+                tween, start, stop);
+    }
+
+    /**
+     * Add a path object
+     *
+     * @param path
+     * @return the id of the path on the wire
+     */
+    public int addPathData(Object path) {
+        float[] pathData = mPlatform.pathToFloatArray(path);
+        int id = mRemoteComposeState.cache(path);
+        PathData.COMPANION.apply(mBuffer, id, pathData);
+        return id;
+    }
+
+    public void addPaint(PaintBundle paint) {
+        PaintData.COMPANION.apply(mBuffer, paint);
+    }
     ///////////////////////////////////////////////////////////////////////////////////////////////
 
     public void inflateFromBuffer(ArrayList<Operation> operations) {
         mBuffer.setIndex(0);
         while (mBuffer.available()) {
             int opId = mBuffer.readByte();
+            System.out.println(">>> " + opId);
             CompanionOperation operation = Operations.map.get(opId);
             if (operation == null) {
-                throw new RuntimeException("Unknown operation encountered");
+                throw new RuntimeException("Unknown operation encountered " + opId);
             }
             operation.read(mBuffer, operations);
         }
@@ -259,7 +540,7 @@
     }
 
     public static RemoteComposeBuffer fromInputStream(InputStream inputStream,
-                                               RemoteComposeState remoteComposeState) {
+                                                      RemoteComposeState remoteComposeState) {
         RemoteComposeBuffer buffer = new RemoteComposeBuffer(remoteComposeState);
         read(inputStream, buffer);
         return buffer;
@@ -318,5 +599,86 @@
         }
     }
 
+    /**
+     * add a Pre-concat the current matrix with the specified skew.
+     *
+     * @param skewX The amount to skew in X
+     * @param skewY The amount to skew in Y
+     */
+    public void addMatrixSkew(float skewX, float skewY) {
+        MatrixSkew.COMPANION.apply(mBuffer, skewX, skewY);
+    }
+
+    /**
+     * This call balances a previous call to save(), and is used to remove all
+     * modifications to the matrix/clip state since the last save call.
+     * Do not call restore() more times than save() was called.
+     */
+    public void addMatrixRestore() {
+        MatrixRestore.COMPANION.apply(mBuffer);
+    }
+
+    /**
+     * Add a saves the current matrix and clip onto a private stack.
+     * <p>
+     * Subsequent calls to translate,scale,rotate,skew,concat or clipRect,
+     * clipPath will all operate as usual, but when the balancing call to
+     * restore() is made, those calls will be forgotten, and the settings that
+     * existed before the save() will be reinstated.
+     */
+    public void addMatrixSave() {
+        MatrixSave.COMPANION.apply(mBuffer);
+    }
+
+    /**
+     * add a pre-concat the current matrix with the specified rotation.
+     *
+     * @param angle   The amount to rotate, in degrees
+     * @param centerX The x-coord for the pivot point (unchanged by the rotation)
+     * @param centerY The y-coord for the pivot point (unchanged by the rotation)
+     */
+    public void addMatrixRotate(float angle, float centerX, float centerY) {
+        MatrixRotate.COMPANION.apply(mBuffer, angle, centerX, centerY);
+    }
+
+    /**
+     * add a Pre-concat to the current matrix with the specified translation
+     *
+     * @param dx The distance to translate in X
+     * @param dy The distance to translate in Y
+     */
+    public void addMatrixTranslate(float dx, float dy) {
+        MatrixTranslate.COMPANION.apply(mBuffer, dx, dy);
+    }
+
+    /**
+     * Add a pre-concat of the current matrix with the specified scale.
+     *
+     * @param scaleX  The amount to scale in X
+     * @param scaleY  The amount to scale in Y
+     */
+    public void addMatrixScale(float scaleX, float scaleY) {
+        MatrixScale.COMPANION.apply(mBuffer, scaleX, scaleY, Float.NaN, Float.NaN);
+    }
+
+    /**
+     * Add a pre-concat of the current matrix with the specified scale.
+     *
+     * @param scaleX  The amount to scale in X
+     * @param scaleY  The amount to scale in Y
+     * @param centerX The x-coord for the pivot point (unchanged by the scale)
+     * @param centerY The y-coord for the pivot point (unchanged by the scale)
+     */
+    public void addMatrixScale(float scaleX, float scaleY, float centerX, float centerY) {
+        MatrixScale.COMPANION.apply(mBuffer, scaleX, scaleY, centerX, centerY);
+    }
+
+    public void addClipPath(int pathId) {
+        ClipPath.COMPANION.apply(mBuffer, pathId);
+    }
+
+    public void addClipRect(float left, float top, float right, float bottom) {
+        ClipRect.COMPANION.apply(mBuffer, left, top, right, bottom);
+    }
 }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java b/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java
index 1b7c6fd..d16cbc5 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java
@@ -37,6 +37,8 @@
     public float mWidth = 0f;
     public float mHeight = 0f;
 
+    public abstract void loadPathData(int instanceId, float[] floatPath);
+
     /**
      * The context can be used in a few different mode, allowing operations to skip being executed:
      * - UNSET : all operations will get executed
diff --git a/core/java/com/android/internal/widget/remotecompose/core/WireBuffer.java b/core/java/com/android/internal/widget/remotecompose/core/WireBuffer.java
index 7c9fda5..fc3202e 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/WireBuffer.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/WireBuffer.java
@@ -37,7 +37,7 @@
         this(BUFFER_SIZE);
     }
 
-    public void resize(int need) {
+    private void resize(int need) {
         if (mSize + need >= mMaxSize) {
             mMaxSize = Math.max(mMaxSize * 2, mSize + need);
             mBuffer = Arrays.copyOf(mBuffer, mMaxSize);
@@ -120,7 +120,7 @@
     }
 
     public int readByte() {
-        byte value = mBuffer[mIndex];
+        int value = 0xFF & mBuffer[mIndex];
         mIndex++;
         return value;
     }
@@ -130,6 +130,14 @@
         int v2 = (mBuffer[mIndex++] & 0xFF) << 0;
         return v1 + v2;
     }
+    public int peekInt() {
+        int tmp = mIndex;
+        int v1 = (mBuffer[tmp++] & 0xFF) << 24;
+        int v2 = (mBuffer[tmp++] & 0xFF) << 16;
+        int v3 = (mBuffer[tmp++] & 0xFF) << 8;
+        int v4 = (mBuffer[tmp++] & 0xFF) << 0;
+        return v1 + v2 + v3 + v4;
+    }
 
     public int readInt() {
         int v1 = (mBuffer[mIndex++] & 0xFF) << 24;
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/ClipPath.java b/core/java/com/android/internal/widget/remotecompose/core/operations/ClipPath.java
new file mode 100644
index 0000000..8d4a787
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/ClipPath.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class ClipPath extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+    int mId;
+    int mRegionOp;
+
+    public ClipPath(int pathId, int regionOp) {
+        mId = pathId;
+        mRegionOp = regionOp;
+    }
+
+    public static final int REPLACE = Companion.PATH_CLIP_REPLACE;
+    public static final int DIFFERENCE = Companion.PATH_CLIP_DIFFERENCE;
+    public static final int INTERSECT = Companion.PATH_CLIP_INTERSECT;
+    public static final int UNION = Companion.PATH_CLIP_UNION;
+    public static final int XOR = Companion.PATH_CLIP_XOR;
+    public static final int REVERSE_DIFFERENCE = Companion.PATH_CLIP_REVERSE_DIFFERENCE;
+    public static final int UNDEFINED = Companion.PATH_CLIP_UNDEFINED;
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mId);
+    }
+
+    @Override
+    public String toString() {
+        return "ClipPath " + mId + ";";
+    }
+
+    public static class Companion implements CompanionOperation {
+        public static final int PATH_CLIP_REPLACE = 0;
+        public static final int PATH_CLIP_DIFFERENCE = 1;
+        public static final int PATH_CLIP_INTERSECT = 2;
+        public static final int PATH_CLIP_UNION = 3;
+        public static final int PATH_CLIP_XOR = 4;
+        public static final int PATH_CLIP_REVERSE_DIFFERENCE = 5;
+        public static final int PATH_CLIP_UNDEFINED = 6;
+
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            int pack = buffer.readInt();
+            int id = pack & 0xFFFFF;
+            int regionOp = pack >> 24;
+            ClipPath op = new ClipPath(id, regionOp);
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "ClipPath";
+        }
+
+        @Override
+        public int id() {
+            return Operations.CLIP_PATH;
+        }
+
+        public void apply(WireBuffer buffer, int id) {
+            buffer.start(Operations.CLIP_PATH);
+            buffer.writeInt(id);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.clipPath(mId, mRegionOp);
+    }
+}
+
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/ClipRect.java b/core/java/com/android/internal/widget/remotecompose/core/operations/ClipRect.java
new file mode 100644
index 0000000..803618a
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/ClipRect.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class ClipRect extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+    float mLeft;
+    float mTop;
+    float mRight;
+    float mBottom;
+
+    public ClipRect(
+            float left,
+            float top,
+            float right,
+            float bottom) {
+        mLeft = left;
+        mTop = top;
+        mRight = right;
+        mBottom = bottom;
+
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mLeft, mTop, mRight, mBottom);
+    }
+
+    @Override
+    public String toString() {
+        return "ClipRect " + mLeft + " " + mTop
+                + " " + mRight + " " + mBottom + ";";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            float sLeft = buffer.readFloat();
+            float srcTop = buffer.readFloat();
+            float srcRight = buffer.readFloat();
+            float srcBottom = buffer.readFloat();
+
+            ClipRect op = new ClipRect(sLeft, srcTop, srcRight, srcBottom);
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "ClipRect";
+        }
+
+        @Override
+        public int id() {
+            return Operations.CLIP_RECT;
+        }
+
+        public void apply(WireBuffer buffer,
+                          float left,
+                          float top,
+                          float right,
+                          float bottom) {
+            buffer.start(Operations.CLIP_RECT);
+            buffer.writeFloat(left);
+            buffer.writeFloat(top);
+            buffer.writeFloat(right);
+            buffer.writeFloat(bottom);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.clipRect(mLeft,
+                mTop,
+                mRight,
+                mBottom);
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawArc.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawArc.java
new file mode 100644
index 0000000..e829975
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawArc.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class DrawArc extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+    float mLeft;
+    float mTop;
+    float mRight;
+    float mBottom;
+    float mStartAngle;
+    float mSweepAngle;
+
+    public DrawArc(
+            float left,
+            float top,
+            float right,
+            float bottom,
+            float startAngle,
+            float sweepAngle) {
+        mLeft = left;
+        mTop = top;
+        mRight = right;
+        mBottom = bottom;
+        mStartAngle = startAngle;
+        mSweepAngle = sweepAngle;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mLeft,
+                mTop,
+                mRight,
+                mBottom,
+                mStartAngle,
+                mSweepAngle);
+    }
+
+    @Override
+    public String toString() {
+        return "DrawArc " + mLeft + " " + mTop
+                + " " + mRight + " " + mBottom + " "
+                + "- " + mStartAngle + " " + mSweepAngle + ";";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            float sLeft = buffer.readFloat();
+            float srcTop = buffer.readFloat();
+            float srcRight = buffer.readFloat();
+            float srcBottom = buffer.readFloat();
+            float mStartAngle = buffer.readFloat();
+            float mSweepAngle = buffer.readFloat();
+            DrawArc op = new DrawArc(sLeft, srcTop, srcRight, srcBottom,
+                    mStartAngle, mSweepAngle);
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "DrawArc";
+        }
+
+        @Override
+        public int id() {
+            return Operations.DRAW_ARC;
+        }
+
+        public void apply(WireBuffer buffer,
+                          float left,
+                          float top,
+                          float right,
+                          float bottom,
+                          float startAngle,
+                          float sweepAngle) {
+            buffer.start(Operations.DRAW_ARC);
+            buffer.writeFloat(left);
+            buffer.writeFloat(top);
+            buffer.writeFloat(right);
+            buffer.writeFloat(bottom);
+            buffer.writeFloat(startAngle);
+            buffer.writeFloat(sweepAngle);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.drawArc(mLeft,
+                mTop,
+                mRight,
+                mBottom,
+                mStartAngle,
+                mSweepAngle);
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBitmap.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBitmap.java
new file mode 100644
index 0000000..2e971f5
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBitmap.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class DrawBitmap extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+    float mLeft;
+    float mTop;
+    float mRight;
+    float mBottom;
+    int mId;
+    int mDescriptionId = 0;
+
+    public DrawBitmap(
+            int imageId,
+            float left,
+            float top,
+            float right,
+            float bottom,
+            int descriptionId) {
+        mLeft = left;
+        mTop = top;
+        mRight = right;
+        mBottom = bottom;
+        mId = imageId;
+        mDescriptionId = descriptionId;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mId, mLeft, mTop, mRight, mBottom, mDescriptionId);
+    }
+
+    @Override
+    public String toString() {
+        return "DrawBitmap (desc=" + mDescriptionId + ")" + mLeft + " " + mTop
+                + " " + mRight + " " + mBottom + ";";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            int id = buffer.readInt();
+            float sLeft = buffer.readFloat();
+            float srcTop = buffer.readFloat();
+            float srcRight = buffer.readFloat();
+            float srcBottom = buffer.readFloat();
+            int discriptionId = buffer.readInt();
+
+            DrawBitmap op = new DrawBitmap(id, sLeft, srcTop, srcRight, srcBottom, discriptionId);
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "DrawOval";
+        }
+
+        @Override
+        public int id() {
+            return Operations.DRAW_BITMAP;
+        }
+
+        public void apply(WireBuffer buffer,
+                          int id,
+                          float left,
+                          float top,
+                          float right,
+                          float bottom,
+                          int descriptionId) {
+            buffer.start(Operations.DRAW_BITMAP);
+            buffer.writeInt(id);
+            buffer.writeFloat(left);
+            buffer.writeFloat(top);
+            buffer.writeFloat(right);
+            buffer.writeFloat(bottom);
+            buffer.writeInt(descriptionId);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.drawBitmap(mId, mLeft,
+                mTop,
+                mRight,
+                mBottom);
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBitmapInt.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBitmapInt.java
index 3fbdf94..c2a56e7 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBitmapInt.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBitmapInt.java
@@ -76,7 +76,8 @@
     }
 
     public static class Companion implements CompanionOperation {
-        private Companion() {}
+        private Companion() {
+        }
 
         @Override
         public String name() {
@@ -89,9 +90,9 @@
         }
 
         public void apply(WireBuffer buffer, int imageId,
-                   int srcLeft, int srcTop, int srcRight, int srcBottom,
-                   int dstLeft, int dstTop, int dstRight, int dstBottom,
-                   int cdId) {
+                          int srcLeft, int srcTop, int srcRight, int srcBottom,
+                          int dstLeft, int dstTop, int dstRight, int dstBottom,
+                          int cdId) {
             buffer.start(Operations.DRAW_BITMAP_INT);
             buffer.writeInt(imageId);
             buffer.writeInt(srcLeft);
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawCircle.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawCircle.java
new file mode 100644
index 0000000..9ce754d
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawCircle.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class DrawCircle extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+    float mCenterX;
+    float mCenterY;
+    float mRadius;
+
+    public DrawCircle(float centerX, float centerY, float radius) {
+        mCenterX = centerX;
+        mCenterY = centerY;
+        mRadius = radius;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mCenterX,
+                mCenterY,
+                mRadius);
+    }
+
+    @Override
+    public String toString() {
+        return "";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            float centerX = buffer.readFloat();
+            float centerY = buffer.readFloat();
+            float radius = buffer.readFloat();
+
+            DrawCircle op = new DrawCircle(centerX, centerY, radius);
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "";
+        }
+
+        @Override
+        public int id() {
+            return 0;
+        }
+
+        public void apply(WireBuffer buffer, float centerX, float centerY, float radius) {
+            buffer.start(Operations.DRAW_CIRCLE);
+            buffer.writeFloat(centerX);
+            buffer.writeFloat(centerY);
+            buffer.writeFloat(radius);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.drawCircle(mCenterX,
+                mCenterY,
+                mRadius);
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawLine.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawLine.java
new file mode 100644
index 0000000..c7a8315
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawLine.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class DrawLine extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+    float mX1;
+    float mY1;
+    float mX2;
+    float mY2;
+
+    public DrawLine(
+            float x1,
+            float y1,
+            float x2,
+            float y2) {
+        mX1 = x1;
+        mY1 = y1;
+        mX2 = x2;
+        mY2 = y2;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mX1,
+                mY1,
+                mX2,
+                mY2);
+    }
+
+    @Override
+    public String toString() {
+        return "DrawArc " + mX1 + " " + mY1
+                + " " + mX2 + " " + mY2 + ";";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            float x1 = buffer.readFloat();
+            float y1 = buffer.readFloat();
+            float x2 = buffer.readFloat();
+            float y2 = buffer.readFloat();
+
+            DrawLine op = new DrawLine(x1, y1, x2, y2);
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "DrawLine";
+        }
+
+        @Override
+        public int id() {
+            return Operations.DRAW_LINE;
+        }
+
+        public void apply(WireBuffer buffer,
+                          float x1,
+                          float y1,
+                          float x2,
+                          float y2) {
+            buffer.start(Operations.DRAW_LINE);
+            buffer.writeFloat(x1);
+            buffer.writeFloat(y1);
+            buffer.writeFloat(x2);
+            buffer.writeFloat(y2);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.drawLine(mX1,
+                mY1,
+                mX2,
+                mY2);
+    }
+
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawOval.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawOval.java
new file mode 100644
index 0000000..7143753
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawOval.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class DrawOval extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+    float mLeft;
+    float mTop;
+    float mRight;
+    float mBottom;
+
+
+    public DrawOval(
+            float left,
+            float top,
+            float right,
+            float bottom) {
+        mLeft = left;
+        mTop = top;
+        mRight = right;
+        mBottom = bottom;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mLeft, mTop, mRight, mBottom);
+    }
+
+    @Override
+    public String toString() {
+        return "DrawOval " + mLeft + " " + mTop
+                + " " + mRight + " " + mBottom + ";";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            float sLeft = buffer.readFloat();
+            float srcTop = buffer.readFloat();
+            float srcRight = buffer.readFloat();
+            float srcBottom = buffer.readFloat();
+
+            DrawOval op = new DrawOval(sLeft, srcTop, srcRight, srcBottom);
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "DrawOval";
+        }
+
+        @Override
+        public int id() {
+            return Operations.DRAW_OVAL;
+        }
+
+        public void apply(WireBuffer buffer,
+                          float left,
+                          float top,
+                          float right,
+                          float bottom) {
+            buffer.start(Operations.DRAW_OVAL);
+            buffer.writeFloat(left);
+            buffer.writeFloat(top);
+            buffer.writeFloat(right);
+            buffer.writeFloat(bottom);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.drawOval(mLeft,
+                mTop,
+                mRight,
+                mBottom);
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawPath.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawPath.java
new file mode 100644
index 0000000..7b8a9e9
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawPath.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class DrawPath extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+    int mId;
+    float mStart = 0;
+    float mEnd = 1;
+
+    public DrawPath(int pathId) {
+        mId = pathId;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mId);
+    }
+
+    @Override
+    public String toString() {
+        return "DrawPath " + ";";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            int id = buffer.readInt();
+            DrawPath op = new DrawPath(id);
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "DrawPath";
+        }
+
+        @Override
+        public int id() {
+            return Operations.DRAW_PATH;
+        }
+
+        public void apply(WireBuffer buffer, int id) {
+            buffer.start(Operations.DRAW_PATH);
+            buffer.writeInt(id);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.drawPath(mId, mStart, mEnd);
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawRect.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawRect.java
new file mode 100644
index 0000000..4775241
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawRect.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class DrawRect extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+    float mLeft;
+    float mTop;
+    float mRight;
+    float mBottom;
+
+    public DrawRect(
+            float left,
+            float top,
+            float right,
+            float bottom) {
+        mLeft = left;
+        mTop = top;
+        mRight = right;
+        mBottom = bottom;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mLeft, mTop, mRight, mBottom);
+    }
+
+    @Override
+    public String toString() {
+        return "DrawRect " + mLeft + " " + mTop
+                + " " + mRight + " " + mBottom + ";";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            float sLeft = buffer.readFloat();
+            float srcTop = buffer.readFloat();
+            float srcRight = buffer.readFloat();
+            float srcBottom = buffer.readFloat();
+
+            DrawRect op = new DrawRect(sLeft, srcTop, srcRight, srcBottom);
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "DrawRect";
+        }
+
+        @Override
+        public int id() {
+            return Operations.DRAW_RECT;
+        }
+
+        public void apply(WireBuffer buffer,
+                          float left,
+                          float top,
+                          float right,
+                          float bottom) {
+            buffer.start(Operations.DRAW_RECT);
+            buffer.writeFloat(left);
+            buffer.writeFloat(top);
+            buffer.writeFloat(right);
+            buffer.writeFloat(bottom);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.drawRect(mLeft,
+                mTop,
+                mRight,
+                mBottom);
+    }
+
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawRoundRect.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawRoundRect.java
new file mode 100644
index 0000000..8da16e7
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawRoundRect.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class DrawRoundRect extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+    float mLeft;
+    float mTop;
+    float mRight;
+    float mBottom;
+    float mRadiusX;
+    float mRadiusY;
+
+    public DrawRoundRect(
+            float left,
+            float top,
+            float right,
+            float bottom,
+            float radiusX,
+            float radiusY) {
+        mLeft = left;
+        mTop = top;
+        mRight = right;
+        mBottom = bottom;
+        mRadiusX = radiusX;
+        mRadiusY = radiusY;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mLeft, mTop, mRight, mBottom, mRadiusX, mRadiusY);
+    }
+
+    @Override
+    public String toString() {
+        return "DrawRoundRect " + mLeft + " " + mTop
+                + " " + mRight + " " + mBottom
+                + " (" + mRadiusX + " " + mRadiusY + ");";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            float sLeft = buffer.readFloat();
+            float srcTop = buffer.readFloat();
+            float srcRight = buffer.readFloat();
+            float srcBottom = buffer.readFloat();
+            float srcRadiusX = buffer.readFloat();
+            float srcRadiusY = buffer.readFloat();
+
+            DrawRoundRect op = new DrawRoundRect(sLeft, srcTop, srcRight,
+                    srcBottom, srcRadiusX, srcRadiusY);
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "DrawOval";
+        }
+
+        @Override
+        public int id() {
+            return Operations.DRAW_ROUND_RECT;
+        }
+
+        public void apply(WireBuffer buffer,
+                          float left,
+                          float top,
+                          float right,
+                          float bottom,
+                          float radiusX,
+                          float radiusY) {
+            buffer.start(Operations.DRAW_ROUND_RECT);
+            buffer.writeFloat(left);
+            buffer.writeFloat(top);
+            buffer.writeFloat(right);
+            buffer.writeFloat(bottom);
+            buffer.writeFloat(radiusX);
+            buffer.writeFloat(radiusY);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.drawRoundRect(mLeft,
+                mTop,
+                mRight,
+                mBottom,
+                mRadiusX,
+                mRadiusY
+        );
+    }
+
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawTextOnPath.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawTextOnPath.java
new file mode 100644
index 0000000..1856e30
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawTextOnPath.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class DrawTextOnPath extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+    int mPathId;
+    public int mTextId;
+    float mVOffset;
+    float mHOffset;
+
+    public DrawTextOnPath(int textId, int pathId, float hOffset, float vOffset) {
+        mPathId = pathId;
+        mTextId = textId;
+        mHOffset = vOffset;
+        mVOffset = hOffset;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mTextId, mPathId, mHOffset, mVOffset);
+    }
+
+    @Override
+    public String toString() {
+        return "DrawTextOnPath " + " " + mPathId + ";";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            int textId = buffer.readInt();
+            int pathId = buffer.readInt();
+            float hOffset = buffer.readFloat();
+            float vOffset = buffer.readFloat();
+            DrawTextOnPath op = new DrawTextOnPath(textId, pathId, hOffset, vOffset);
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "DrawTextOnPath";
+        }
+
+        @Override
+        public int id() {
+            return Operations.DRAW_TEXT_ON_PATH;
+        }
+
+        public void apply(WireBuffer buffer, int textId, int pathId, float hOffset, float vOffset) {
+            buffer.start(Operations.DRAW_TEXT_ON_PATH);
+            buffer.writeInt(textId);
+            buffer.writeInt(pathId);
+            buffer.writeFloat(hOffset);
+            buffer.writeFloat(vOffset);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.drawTextOnPath(mTextId, mPathId, mHOffset, mVOffset);
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawTextRun.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawTextRun.java
new file mode 100644
index 0000000..a099252
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawTextRun.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class DrawTextRun extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+    int mTextID;
+    int mStart = 0;
+    int mEnd = 0;
+    int mContextStart = 0;
+    int mContextEnd = 0;
+    float mX = 0f;
+    float mY = 0f;
+    boolean mRtl = false;
+
+    public DrawTextRun(int textID,
+                       int start,
+                       int end,
+                       int contextStart,
+                       int contextEnd,
+                       float x,
+                       float y,
+                       boolean rtl) {
+        mTextID = textID;
+        mStart = start;
+        mEnd = end;
+        mContextStart = contextStart;
+        mContextEnd = contextEnd;
+        mX = x;
+        mY = y;
+        mRtl = rtl;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mTextID, mStart, mEnd, mContextStart, mContextEnd, mX, mY, mRtl);
+
+    }
+
+    @Override
+    public String toString() {
+        return "";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            int text = buffer.readInt();
+            int start = buffer.readInt();
+            int end = buffer.readInt();
+            int contextStart = buffer.readInt();
+            int contextEnd = buffer.readInt();
+            float x = buffer.readFloat();
+            float y = buffer.readFloat();
+            boolean rtl = buffer.readBoolean();
+            DrawTextRun op = new DrawTextRun(text, start, end, contextStart, contextEnd, x, y, rtl);
+
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "";
+        }
+
+        @Override
+        public int id() {
+            return 0;
+        }
+
+        public void apply(WireBuffer buffer,
+                          int textID,
+                          int start,
+                          int end,
+                          int contextStart,
+                          int contextEnd,
+                          float x,
+                          float y,
+                          boolean rtl) {
+            buffer.start(Operations.DRAW_TEXT_RUN);
+            buffer.writeInt(textID);
+            buffer.writeInt(start);
+            buffer.writeInt(end);
+            buffer.writeInt(contextStart);
+            buffer.writeInt(contextEnd);
+            buffer.writeFloat(x);
+            buffer.writeFloat(y);
+            buffer.writeBoolean(rtl);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.drawTextRun(mTextID, mStart, mEnd, mContextStart, mContextEnd, mX, mY, mRtl);
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawTweenPath.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawTweenPath.java
new file mode 100644
index 0000000..ef0a4ad
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawTweenPath.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class DrawTweenPath extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+    float mTween;
+    float mStart;
+    float mStop;
+    int mPath1Id;
+    int mPath2Id;
+
+    public DrawTweenPath(
+            int path1Id,
+            int path2Id,
+            float tween,
+            float start,
+            float stop) {
+        mTween = tween;
+        mStart = start;
+        mStop = stop;
+        mPath1Id = path1Id;
+        mPath2Id = path2Id;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mPath1Id,
+                mPath2Id,
+                mTween,
+                mStart,
+                mStop);
+    }
+
+    @Override
+    public String toString() {
+        return "DrawTweenPath " + mPath1Id + " " + mPath2Id
+                + " " + mTween + " " + mStart + " "
+                + "- " + mStop + ";";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            int path1Id = buffer.readInt();
+            int path2Id = buffer.readInt();
+            float tween = buffer.readFloat();
+            float start = buffer.readFloat();
+            float stop = buffer.readFloat();
+            DrawTweenPath op = new DrawTweenPath(path1Id, path2Id,
+                    tween, start, stop);
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "DrawTweenPath";
+        }
+
+        @Override
+        public int id() {
+            return Operations.DRAW_TWEEN_PATH;
+        }
+
+        public void apply(WireBuffer buffer,
+                          int path1Id,
+                          int path2Id,
+                          float tween,
+                          float start,
+                          float stop) {
+            buffer.start(Operations.DRAW_TWEEN_PATH);
+            buffer.writeInt(path1Id);
+            buffer.writeInt(path2Id);
+            buffer.writeFloat(tween);
+            buffer.writeFloat(start);
+            buffer.writeFloat(stop);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.drawTweenPath(mPath1Id,
+                mPath2Id,
+                mTween,
+                mStart,
+                mStop);
+    }
+
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/Header.java b/core/java/com/android/internal/widget/remotecompose/core/operations/Header.java
index eca43c5..aabed15 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/Header.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/Header.java
@@ -26,11 +26,11 @@
 
 /**
  * Describe some basic information for a RemoteCompose document
- *
+ * <p>
  * It encodes the version of the document (following semantic versioning) as well
  * as the dimensions of the document in pixels.
  */
-public class Header  implements RemoteComposeOperation {
+public class Header implements RemoteComposeOperation {
     public static final int MAJOR_VERSION = 0;
     public static final int MINOR_VERSION = 1;
     public static final int PATCH_VERSION = 0;
@@ -89,7 +89,8 @@
     }
 
     public static class Companion implements CompanionOperation {
-        private Companion() {}
+        private Companion() {
+        }
 
         @Override
         public String name() {
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixRestore.java b/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixRestore.java
new file mode 100644
index 0000000..482e0e2
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixRestore.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class MatrixRestore extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+
+    public MatrixRestore() {
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer);
+    }
+
+    @Override
+    public String toString() {
+        return "MatrixRestore;";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+
+            MatrixRestore op = new MatrixRestore();
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "MatrixRestore";
+        }
+
+        @Override
+        public int id() {
+            return Operations.MATRIX_RESTORE;
+        }
+
+        public void apply(WireBuffer buffer) {
+            buffer.start(Operations.MATRIX_RESTORE);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.matrixRestore();
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixRotate.java b/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixRotate.java
new file mode 100644
index 0000000..d6c89e0
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixRotate.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class MatrixRotate extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+    float mRotate, mPivotX, mPivotY;
+
+    public MatrixRotate(float rotate, float pivotX, float pivotY) {
+        mRotate = rotate;
+        mPivotX = pivotX;
+        mPivotY = pivotY;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mRotate, mPivotX, mPivotY);
+    }
+
+    @Override
+    public String toString() {
+        return "DrawArc " + mRotate + ", " + mPivotX + ", " + mPivotY + ";";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            float rotate = buffer.readFloat();
+            float pivotX = buffer.readFloat();
+            float pivotY = buffer.readFloat();
+            MatrixRotate op = new MatrixRotate(rotate, pivotX, pivotY);
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "Matrix";
+        }
+
+        @Override
+        public int id() {
+            return Operations.MATRIX_ROTATE;
+        }
+
+        public void apply(WireBuffer buffer, float rotate, float pivotX, float pivotY) {
+            buffer.start(Operations.MATRIX_ROTATE);
+            buffer.writeFloat(rotate);
+            buffer.writeFloat(pivotX);
+            buffer.writeFloat(pivotY);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.matrixRotate(mRotate, mPivotX, mPivotY);
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixSave.java b/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixSave.java
new file mode 100644
index 0000000..d3d5bfb
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixSave.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class MatrixSave extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+
+    public MatrixSave() {
+
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer);
+    }
+
+    @Override
+    public String toString() {
+        return "MatrixSave;";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+
+            MatrixSave op = new MatrixSave();
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "Matrix";
+        }
+
+        @Override
+        public int id() {
+            return Operations.MATRIX_SAVE;
+        }
+
+        public void apply(WireBuffer buffer) {
+            buffer.start(Operations.MATRIX_SAVE);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.matrixSave();
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixScale.java b/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixScale.java
new file mode 100644
index 0000000..28aa68dd
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixScale.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class MatrixScale extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+    float mScaleX, mScaleY;
+    float mCenterX, mCenterY;
+
+    public MatrixScale(float scaleX, float scaleY, float centerX, float centerY) {
+        mScaleX = scaleX;
+        mScaleY = scaleY;
+        mCenterX = centerX;
+        mCenterY = centerY;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mScaleX, mScaleY, mCenterX, mCenterY);
+    }
+
+    @Override
+    public String toString() {
+        return "MatrixScale " + mScaleY + ", " + mScaleY + ";";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            float scaleX = buffer.readFloat();
+            float scaleY = buffer.readFloat();
+            float centerX = buffer.readFloat();
+            float centerY = buffer.readFloat();
+            MatrixScale op = new MatrixScale(scaleX, scaleY, centerX, centerY);
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "Matrix";
+        }
+
+        @Override
+        public int id() {
+            return Operations.MATRIX_SCALE;
+        }
+
+        public void apply(WireBuffer buffer, float scaleX, float scaleY,
+                float centerX, float centerY) {
+            buffer.start(Operations.MATRIX_SCALE);
+            buffer.writeFloat(scaleX);
+            buffer.writeFloat(scaleY);
+            buffer.writeFloat(centerX);
+            buffer.writeFloat(centerY);
+
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.mtrixScale(mScaleX, mScaleY, mCenterX, mCenterY);
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixSkew.java b/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixSkew.java
new file mode 100644
index 0000000..a388899
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixSkew.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class MatrixSkew extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+    float mSkewX, mSkewY;
+
+    public MatrixSkew(float skewX, float skewY) {
+        mSkewX = skewX;
+        mSkewY = skewY;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mSkewX, mSkewY);
+    }
+
+    @Override
+    public String toString() {
+        return "DrawArc " + mSkewY + ", " + mSkewY + ";";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            float skewX = buffer.readFloat();
+            float skewY = buffer.readFloat();
+            MatrixSkew op = new MatrixSkew(skewX, skewY);
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "Matrix";
+        }
+
+        @Override
+        public int id() {
+            return Operations.MATRIX_SKEW;
+        }
+
+        public void apply(WireBuffer buffer, float skewX, float skewY) {
+            buffer.start(Operations.MATRIX_SKEW);
+            buffer.writeFloat(skewX);
+            buffer.writeFloat(skewY);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.matrixSkew(mSkewX, mSkewY);
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixTranslate.java b/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixTranslate.java
new file mode 100644
index 0000000..3298752
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/MatrixTranslate.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class MatrixTranslate extends PaintOperation {
+    public static final Companion COMPANION = new Companion();
+    float mTranslateX, mTranslateY;
+
+    public MatrixTranslate(float translateX, float translateY) {
+        mTranslateX = translateX;
+        mTranslateY = translateY;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mTranslateX, mTranslateY);
+    }
+
+    @Override
+    public String toString() {
+        return "DrawArc " + mTranslateY + ", " + mTranslateY + ";";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            float translateX = buffer.readFloat();
+            float translateY = buffer.readFloat();
+            MatrixTranslate op = new MatrixTranslate(translateX, translateY);
+            operations.add(op);
+        }
+
+        @Override
+        public String name() {
+            return "Matrix";
+        }
+
+        @Override
+        public int id() {
+            return Operations.MATRIX_TRANSLATE;
+        }
+
+        public void apply(WireBuffer buffer, float translateX, float translateY) {
+            buffer.start(Operations.MATRIX_TRANSLATE);
+            buffer.writeFloat(translateX);
+            buffer.writeFloat(translateY);
+        }
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.matrixTranslate(mTranslateX, mTranslateY);
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/PaintData.java b/core/java/com/android/internal/widget/remotecompose/core/operations/PaintData.java
new file mode 100644
index 0000000..e5683ec
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/PaintData.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.PaintOperation;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+import com.android.internal.widget.remotecompose.core.operations.paint.PaintBundle;
+
+import java.util.List;
+
+public class PaintData extends PaintOperation {
+    public PaintBundle mPaintData = new PaintBundle();
+    public static final Companion COMPANION = new Companion();
+    public static final int MAX_STRING_SIZE = 4000;
+
+    public PaintData() {
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mPaintData);
+    }
+
+    @Override
+    public String toString() {
+        return "PaintData " + "\"" + mPaintData + "\"";
+    }
+
+    public static class Companion implements CompanionOperation {
+        private Companion() {
+        }
+
+        @Override
+        public String name() {
+            return "TextData";
+        }
+
+        @Override
+        public int id() {
+            return Operations.PAINT_VALUES;
+        }
+
+        public void apply(WireBuffer buffer, PaintBundle paintBundle) {
+            buffer.start(Operations.PAINT_VALUES);
+            paintBundle.writeBundle(buffer);
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            PaintData data = new PaintData();
+            data.mPaintData.readBundle(buffer);
+            operations.add(data);
+        }
+    }
+
+    @Override
+    public String deepToString(String indent) {
+        return indent + toString();
+    }
+
+    @Override
+    public void paint(PaintContext context) {
+        context.applyPaint(mPaintData);
+    }
+
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/PathData.java b/core/java/com/android/internal/widget/remotecompose/core/operations/PathData.java
new file mode 100644
index 0000000..2646b27
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/PathData.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations;
+
+import com.android.internal.widget.remotecompose.core.CompanionOperation;
+import com.android.internal.widget.remotecompose.core.Operation;
+import com.android.internal.widget.remotecompose.core.Operations;
+import com.android.internal.widget.remotecompose.core.PaintContext;
+import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.List;
+
+public class PathData implements Operation {
+    public static final Companion COMPANION = new Companion();
+    int mInstanceId;
+    float[] mRef;
+    float[] mFloatPath;
+    float[] mRetFloats;
+
+    PathData(int instanceId, float[] floatPath) {
+        mInstanceId = instanceId;
+        mFloatPath = floatPath;
+    }
+
+    @Override
+    public void write(WireBuffer buffer) {
+        COMPANION.apply(buffer, mInstanceId, mFloatPath);
+    }
+
+    @Override
+    public String deepToString(String indent) {
+        return pathString(mFloatPath);
+    }
+
+    public float[] getFloatPath(PaintContext context) {
+        float[] ret = mRetFloats; // Assume retFloats is declared elsewhere
+        if (ret == null) {
+            return mFloatPath; // Assume floatPath is declared elsewhere
+        }
+        float[] localRef = mRef; // Assume ref is of type Float[]
+        if (localRef == null) {
+            for (int i = 0; i < mFloatPath.length; i++) {
+                ret[i] = mFloatPath[i];
+            }
+        } else {
+            for (int i = 0; i < mFloatPath.length; i++) {
+                float lr = localRef[i];
+                if (Float.isNaN(lr)) {
+                    ret[i] = Utils.getActualValue(lr);
+                } else {
+                    ret[i] = mFloatPath[i];
+                }
+            }
+        }
+        return ret;
+    }
+
+    public static final int MOVE = 10;
+    public static final int LINE = 11;
+    public static final int QUADRATIC = 12;
+    public static final int CONIC = 13;
+    public static final int CUBIC = 14;
+    public static final int CLOSE = 15;
+    public static final int DONE = 16;
+    public static final float MOVE_NAN = Utils.asNan(MOVE);
+    public static final float LINE_NAN = Utils.asNan(LINE);
+    public static final float QUADRATIC_NAN = Utils.asNan(QUADRATIC);
+    public static final float CONIC_NAN = Utils.asNan(CONIC);
+    public static final float CUBIC_NAN = Utils.asNan(CUBIC);
+    public static final float CLOSE_NAN = Utils.asNan(CLOSE);
+    public static final float DONE_NAN = Utils.asNan(DONE);
+
+    public static class Companion implements CompanionOperation {
+
+        private Companion() {
+        }
+
+        @Override
+        public String name() {
+            return "BitmapData";
+        }
+
+        @Override
+        public int id() {
+            return Operations.DATA_PATH;
+        }
+
+        public void apply(WireBuffer buffer, int id, float[] data) {
+            buffer.start(Operations.DATA_PATH);
+            buffer.writeInt(id);
+            buffer.writeInt(data.length);
+            for (int i = 0; i < data.length; i++) {
+                buffer.writeFloat(data[i]);
+            }
+        }
+
+        @Override
+        public void read(WireBuffer buffer, List<Operation> operations) {
+            int imageId = buffer.readInt();
+            int len = buffer.readInt();
+            float[] data = new float[len];
+            for (int i = 0; i < data.length; i++) {
+                data[i] = buffer.readFloat();
+            }
+            operations.add(new PathData(imageId, data));
+        }
+    }
+
+    public static String pathString(float[] path) {
+        if (path == null) {
+            return "null";
+        }
+        StringBuilder str = new StringBuilder();
+        for (int i = 0; i < path.length; i++) {
+            if (i != 0) {
+                str.append(" ");
+            }
+            if (Float.isNaN(path[i])) {
+                int id = Utils.idFromNan(path[i]); // Assume idFromNan is defined elsewhere
+                if (id <= DONE) { // Assume DONE is a constant
+                    switch (id) {
+                        case MOVE:
+                            str.append("M");
+                            break;
+                        case LINE:
+                            str.append("L");
+                            break;
+                        case QUADRATIC:
+                            str.append("Q");
+                            break;
+                        case CONIC:
+                            str.append("R");
+                            break;
+                        case CUBIC:
+                            str.append("C");
+                            break;
+                        case CLOSE:
+                            str.append("Z");
+                            break;
+                        case DONE:
+                            str.append(".");
+                            break;
+                        default:
+                            str.append("X");
+                            break;
+                    }
+                } else {
+                    str.append("(" + id + ")");
+                }
+            } else {
+                str.append(path[i]);
+            }
+        }
+        return str.toString();
+    }
+
+    @Override
+    public void apply(RemoteContext context) {
+        context.loadPathData(mInstanceId, mFloatPath);
+    }
+
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/RootContentBehavior.java b/core/java/com/android/internal/widget/remotecompose/core/operations/RootContentBehavior.java
index ad4caea..6d924eb 100644
--- a/core/java/com/android/internal/widget/remotecompose/core/operations/RootContentBehavior.java
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/RootContentBehavior.java
@@ -28,7 +28,7 @@
 
 /**
  * Describe some basic information for a RemoteCompose document
- *
+ * <p>
  * It encodes the version of the document (following semantic versioning) as well
  * as the dimensions of the document in pixels.
  */
@@ -100,21 +100,21 @@
     /**
      * Sets the way the player handles the content
      *
-     * @param scroll set the horizontal behavior (NONE|SCROLL_HORIZONTAL|SCROLL_VERTICAL)
+     * @param scroll    set the horizontal behavior (NONE|SCROLL_HORIZONTAL|SCROLL_VERTICAL)
      * @param alignment set the alignment of the content (TOP|CENTER|BOTTOM|START|END)
-     * @param sizing set the type of sizing for the content (NONE|SIZING_LAYOUT|SIZING_SCALE)
-     * @param mode set the mode of sizing, either LAYOUT modes or SCALE modes
-     *             the LAYOUT modes are:
-     *             - LAYOUT_MATCH_PARENT
-     *             - LAYOUT_WRAP_CONTENT
-     *             or adding an horizontal mode and a vertical mode:
-     *             - LAYOUT_HORIZONTAL_MATCH_PARENT
-     *             - LAYOUT_HORIZONTAL_WRAP_CONTENT
-     *             - LAYOUT_HORIZONTAL_FIXED
-     *             - LAYOUT_VERTICAL_MATCH_PARENT
-     *             - LAYOUT_VERTICAL_WRAP_CONTENT
-     *             - LAYOUT_VERTICAL_FIXED
-     *             The LAYOUT_*_FIXED modes will use the intrinsic document size
+     * @param sizing    set the type of sizing for the content (NONE|SIZING_LAYOUT|SIZING_SCALE)
+     * @param mode      set the mode of sizing, either LAYOUT modes or SCALE modes
+     *                  the LAYOUT modes are:
+     *                  - LAYOUT_MATCH_PARENT
+     *                  - LAYOUT_WRAP_CONTENT
+     *                  or adding an horizontal mode and a vertical mode:
+     *                  - LAYOUT_HORIZONTAL_MATCH_PARENT
+     *                  - LAYOUT_HORIZONTAL_WRAP_CONTENT
+     *                  - LAYOUT_HORIZONTAL_FIXED
+     *                  - LAYOUT_VERTICAL_MATCH_PARENT
+     *                  - LAYOUT_VERTICAL_WRAP_CONTENT
+     *                  - LAYOUT_VERTICAL_FIXED
+     *                  The LAYOUT_*_FIXED modes will use the intrinsic document size
      */
     public RootContentBehavior(int scroll, int alignment, int sizing, int mode) {
         switch (scroll) {
@@ -149,10 +149,12 @@
         switch (sizing) {
             case SIZING_LAYOUT: {
                 Log.e(TAG, "sizing_layout is not yet supported");
-            } break;
+            }
+            break;
             case SIZING_SCALE: {
                 mSizing = sizing;
-            } break;
+            }
+            break;
             default: {
                 Log.e(TAG, "incorrect sizing value " + sizing);
             }
@@ -200,7 +202,8 @@
     }
 
     public static class Companion implements CompanionOperation {
-        private Companion() {}
+        private Companion() {
+        }
 
         @Override
         public String name() {
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/Utils.java b/core/java/com/android/internal/widget/remotecompose/core/operations/Utils.java
new file mode 100644
index 0000000..00e2f20
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/Utils.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations;
+
+public class Utils {
+    public static float asNan(int v) {
+        return Float.intBitsToFloat(v | -0x800000);
+    }
+
+    public static int idFromNan(float value) {
+        int b =  Float.floatToRawIntBits(value);
+        return b & 0xFFFFF;
+    }
+
+    public static float getActualValue(float lr) {
+        return 0;
+    }
+
+    String getFloatString(float value) {
+        if (Float.isNaN(value)) {
+            int id = idFromNan(value);
+            if (id > 0) {
+                return "NaN(" + id + ")";
+            }
+        }
+        return "" + value;
+    }
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintBundle.java b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintBundle.java
new file mode 100644
index 0000000..8abb0bf
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintBundle.java
@@ -0,0 +1,829 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations.paint;
+
+import com.android.internal.widget.remotecompose.core.WireBuffer;
+
+import java.util.Arrays;
+
+public class PaintBundle {
+    int[] mArray = new int[200];
+    int mPos = 0;
+
+    public void applyPaintChange(PaintChanges p) {
+        int i = 0;
+        int mask = 0;
+        while (i < mPos) {
+            int cmd = mArray[i++];
+            mask = mask | (1 << (cmd - 1));
+            switch (cmd & 0xFFFF) {
+                case TEXT_SIZE: {
+                    p.setTextSize(Float.intBitsToFloat(mArray[i++]));
+                    break;
+                }
+                case TYPEFACE:
+                    int style = (cmd >> 16);
+                    int weight = style & 0x3ff;
+                    boolean italic = (style >> 10) > 0;
+                    int font_type = mArray[i++];
+
+                    p.setTypeFace(font_type, weight, italic);
+                    break;
+                case COLOR: {
+                    p.setColor(mArray[i++]);
+                    break;
+                }
+                case STROKE_WIDTH: {
+                    p.setStrokeWidth(Float.intBitsToFloat(mArray[i++]));
+                    break;
+                }
+                case STROKE_MITER: {
+                    p.setStrokeMiter(Float.intBitsToFloat(mArray[i++]));
+                    break;
+                }
+                case STROKE_CAP: {
+                    p.setStrokeCap(cmd >> 16);
+                    break;
+                }
+                case STYLE: {
+                    p.setStyle(cmd >> 16);
+                    break;
+                }
+                case SHADER: {
+                    break;
+                }
+                case STROKE_JOIN: {
+                    p.setStrokeJoin(cmd >> 16);
+                    break;
+                }
+                case IMAGE_FILTER_QUALITY: {
+                    p.setImageFilterQuality(cmd >> 16);
+                    break;
+                }
+                case BLEND_MODE: {
+                    p.setBlendMode(cmd >> 16);
+                    break;
+                }
+                case FILTER_BITMAP: {
+                    p.setFilterBitmap(!((cmd >> 16) == 0));
+                    break;
+                }
+
+                case GRADIENT: {
+                    i = callSetGradient(cmd, mArray, i, p);
+                    break;
+                }
+                case COLOR_FILTER: {
+                    p.setColorFilter(mArray[i++], cmd >> 16);
+                    break;
+                }
+                case ALPHA: {
+                    p.setAlpha(Float.intBitsToFloat(mArray[i++]));
+                    break;
+                }
+            }
+        }
+
+        mask = (~mask) & PaintChanges.VALID_BITS;
+
+        p.clear(mask);
+    }
+
+    private String toName(int id) {
+        switch (id) {
+            case TEXT_SIZE:
+                return "TEXT_SIZE";
+
+            case COLOR:
+                return "COLOR";
+            case STROKE_WIDTH:
+                return "STROKE_WIDTH";
+            case STROKE_MITER:
+                return "STROKE_MITER";
+            case TYPEFACE:
+                return "TYPEFACE";
+            case STROKE_CAP:
+                return "CAP";
+            case STYLE:
+                return "STYLE";
+            case SHADER:
+                return "SHADER";
+            case IMAGE_FILTER_QUALITY:
+                return "IMAGE_FILTER_QUALITY";
+            case BLEND_MODE:
+                return "BLEND_MODE";
+            case FILTER_BITMAP:
+                return "FILTER_BITMAP";
+            case GRADIENT:
+                return "GRADIENT_LINEAR";
+            case ALPHA:
+                return "ALPHA";
+            case COLOR_FILTER:
+                return "COLOR_FILTER";
+
+        }
+        return "????" + id + "????";
+    }
+
+    private static String colorInt(int color) {
+        String str = "000000000000" + Integer.toHexString(color);
+        return "0x" + str.substring(str.length() - 8);
+    }
+
+    private static String colorInt(int[] color) {
+        String str = "[";
+        for (int i = 0; i < color.length; i++) {
+            if (i > 0) {
+                str += ", ";
+            }
+            str += colorInt(color[i]);
+        }
+        return str + "]";
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder ret = new StringBuilder("\n");
+        int i = 0;
+        while (i < mPos) {
+            int cmd = mArray[i++];
+            int type = cmd & 0xFFFF;
+            switch (type) {
+
+                case TEXT_SIZE: {
+                    ret.append("    TextSize(" + Float.intBitsToFloat(mArray[i++]));
+                }
+
+                break;
+                case TYPEFACE: {
+                    int style = (cmd >> 16);
+                    int weight = style & 0x3ff;
+                    boolean italic = (style >> 10) > 0;
+                    int font_type = mArray[i++];
+                    ret.append("    TypeFace(" + (font_type + ", "
+                            + weight + ", " + italic));
+                }
+                break;
+                case COLOR: {
+                    ret.append("    Color(" + colorInt(mArray[i++]));
+                }
+                break;
+                case STROKE_WIDTH: {
+                    ret.append("    StrokeWidth("
+                            + (Float.intBitsToFloat(mArray[i++])));
+                }
+                break;
+                case STROKE_MITER: {
+                    ret.append("    StrokeMiter("
+                            + (Float.intBitsToFloat(mArray[i++])));
+                }
+                break;
+                case STROKE_CAP: {
+                    ret.append("    StrokeCap("
+                            + (cmd >> 16));
+                }
+                break;
+                case STYLE: {
+                    ret.append("    Style(" + (cmd >> 16));
+                }
+                break;
+                case COLOR_FILTER: {
+                    ret.append("    ColorFilter(color="
+                            + colorInt(mArray[i++])
+                            + ", mode=" + blendModeString(cmd >> 16));
+                }
+                break;
+                case SHADER: {
+                }
+                break;
+                case ALPHA: {
+                    ret.append("    Alpha("
+                            + (Float.intBitsToFloat(mArray[i++])));
+                }
+                break;
+                case IMAGE_FILTER_QUALITY: {
+                    ret.append("    ImageFilterQuality(" + (cmd >> 16));
+                }
+                break;
+                case BLEND_MODE: {
+                    ret.append("    BlendMode(" + blendModeString(cmd >> 16));
+                }
+                break;
+                case FILTER_BITMAP: {
+                    ret.append("    FilterBitmap("
+                            + (!((cmd >> 16) == 0)));
+                }
+                break;
+                case STROKE_JOIN: {
+                    ret.append("    StrokeJoin(" + (cmd >> 16));
+                }
+                break;
+                case ANTI_ALIAS: {
+                    ret.append("    AntiAlias(" + (cmd >> 16));
+                }
+                break;
+                case GRADIENT: {
+                    i = callPrintGradient(cmd, mArray, i, ret);
+                }
+            }
+            ret.append("),\n");
+        }
+        return ret.toString();
+    }
+
+
+    int callPrintGradient(int cmd, int[] array, int i, StringBuilder p) {
+        int ret = i;
+        int type = (cmd >> 16);
+        switch (type) {
+
+            case 0: {
+                p.append("    LinearGradient(\n");
+                int len = array[ret++];
+                int[] colors = null;
+                if (len > 0) {
+                    colors = new int[len];
+                    for (int j = 0; j < colors.length; j++) {
+                        colors[j] = array[ret++];
+
+                    }
+                }
+                len = array[ret++];
+                float[] stops = null;
+                if (len > 0) {
+                    stops = new float[len];
+                    for (int j = 0; j < stops.length; j++) {
+                        stops[j] = Float.intBitsToFloat(array[ret++]);
+                    }
+                }
+
+                p.append("      colors = " + colorInt(colors) + ",\n");
+                p.append("      stops = " + Arrays.toString(stops) + ",\n");
+                p.append("      start = ");
+                p.append("[" + Float.intBitsToFloat(array[ret++]));
+                p.append(", " + Float.intBitsToFloat(array[ret++]) + "],\n");
+                p.append("      end = ");
+                p.append("[" + Float.intBitsToFloat(array[ret++]));
+                p.append(", " + Float.intBitsToFloat(array[ret++]) + "],\n");
+                int tileMode = array[ret++];
+                p.append("      tileMode = " + tileMode + "\n    ");
+            }
+
+            break;
+            case 1: {
+                p.append("    RadialGradient(\n");
+                int len = array[ret++];
+                int[] colors = null;
+                if (len > 0) {
+                    colors = new int[len];
+                    for (int j = 0; j < colors.length; j++) {
+                        colors[j] = array[ret++];
+
+                    }
+                }
+                len = array[ret++];
+                float[] stops = null;
+                if (len > 0) {
+                    stops = new float[len];
+                    for (int j = 0; j < stops.length; j++) {
+                        stops[j] = Float.intBitsToFloat(array[ret++]);
+                    }
+                }
+
+                p.append("      colors = " + colorInt(colors) + ",\n");
+                p.append("      stops = " + Arrays.toString(stops) + ",\n");
+                p.append("      center = ");
+                p.append("[" + Float.intBitsToFloat(array[ret++]));
+                p.append(", " + Float.intBitsToFloat(array[ret++]) + "],\n");
+                p.append("      radius =");
+                p.append(" " + Float.intBitsToFloat(array[ret++]) + ",\n");
+                int tileMode = array[ret++];
+                p.append("      tileMode = " + tileMode + "\n    ");
+            }
+
+            break;
+            case 2: {
+                p.append("    SweepGradient(\n");
+                int len = array[ret++];
+                int[] colors = null;
+                if (len > 0) {
+                    colors = new int[len];
+                    for (int j = 0; j < colors.length; j++) {
+                        colors[j] = array[ret++];
+
+                    }
+                }
+                len = array[ret++];
+                float[] stops = null;
+                if (len > 0) {
+                    stops = new float[len];
+                    for (int j = 0; j < stops.length; j++) {
+                        stops[j] = Float.intBitsToFloat(array[ret++]);
+                    }
+                }
+
+                p.append("      colors = " + colorInt(colors) + ",\n");
+                p.append("      stops = " + Arrays.toString(stops) + ",\n");
+                p.append("      center = ");
+                p.append("[" + Float.intBitsToFloat(array[ret++]));
+                p.append(", " + Float.intBitsToFloat(array[ret++]) + "],\n    ");
+
+            }
+            break;
+            default: {
+                p.append("GRADIENT_??????!!!!");
+            }
+        }
+
+        return ret;
+    }
+
+    int callSetGradient(int cmd, int[] array, int i, PaintChanges p) {
+        int ret = i;
+        int gradientType = (cmd >> 16);
+
+        int len = array[ret++];
+        int[] colors = null;
+        if (len > 0) {
+            colors = new int[len];
+            for (int j = 0; j < colors.length; j++) {
+                colors[j] = array[ret++];
+            }
+        }
+        len = array[ret++];
+        float[] stops = null;
+        if (len > 0) {
+            stops = new float[len];
+            for (int j = 0; j < colors.length; j++) {
+                stops[j] = Float.intBitsToFloat(array[ret++]);
+            }
+        }
+
+        if (colors == null) {
+            return ret;
+        }
+
+
+        switch (gradientType) {
+
+            case LINEAR_GRADIENT: {
+                float startX = Float.intBitsToFloat(array[ret++]);
+                float startY = Float.intBitsToFloat(array[ret++]);
+                float endX = Float.intBitsToFloat(array[ret++]);
+                float endY = Float.intBitsToFloat(array[ret++]);
+                int tileMode = array[ret++];
+                p.setLinearGradient(colors, stops, startX,
+                        startY, endX, endY, tileMode);
+            }
+
+            break;
+            case RADIAL_GRADIENT: {
+                float centerX = Float.intBitsToFloat(array[ret++]);
+                float centerY = Float.intBitsToFloat(array[ret++]);
+                float radius = Float.intBitsToFloat(array[ret++]);
+                int tileMode = array[ret++];
+                p.setRadialGradient(colors, stops, centerX, centerY,
+                        radius, tileMode);
+            }
+            break;
+            case SWEEP_GRADIENT: {
+                float centerX = Float.intBitsToFloat(array[ret++]);
+                float centerY = Float.intBitsToFloat(array[ret++]);
+                p.setSweepGradient(colors, stops, centerX, centerY);
+            }
+        }
+
+        return ret;
+    }
+
+    public void writeBundle(WireBuffer buffer) {
+        buffer.writeInt(mPos);
+        for (int index = 0; index < mPos; index++) {
+            buffer.writeInt(mArray[index]);
+        }
+    }
+
+    public void readBundle(WireBuffer buffer) {
+        int len = buffer.readInt();
+        if (len <= 0 || len > 1024) {
+            throw new RuntimeException("buffer corrupt paint len = " + len);
+        }
+        mArray = new int[len];
+        for (int i = 0; i < mArray.length; i++) {
+            mArray[i] = buffer.readInt();
+        }
+        mPos = len;
+    }
+
+    public static final int TEXT_SIZE = 1;  // float
+
+    public static final int COLOR = 4;  // int
+    public static final int STROKE_WIDTH = 5; // float
+    public static final int STROKE_MITER = 6;
+    public static final int STROKE_CAP = 7; // int
+    public static final int STYLE = 8; // int
+    public static final int SHADER = 9; // int
+    public static final int IMAGE_FILTER_QUALITY = 10; // int
+    public static final int GRADIENT = 11;
+    public static final int ALPHA = 12;
+    public static final int COLOR_FILTER = 13;
+    public static final int ANTI_ALIAS = 14;
+    public static final int STROKE_JOIN = 15;
+    public static final int TYPEFACE = 16;
+    public static final int FILTER_BITMAP = 17;
+    public static final int BLEND_MODE = 18;
+
+
+    public static final int BLEND_MODE_CLEAR = 0;
+    public static final int BLEND_MODE_SRC = 1;
+    public static final int BLEND_MODE_DST = 2;
+    public static final int BLEND_MODE_SRC_OVER = 3;
+    public static final int BLEND_MODE_DST_OVER = 4;
+    public static final int BLEND_MODE_SRC_IN = 5;
+    public static final int BLEND_MODE_DST_IN = 6;
+    public static final int BLEND_MODE_SRC_OUT = 7;
+    public static final int BLEND_MODE_DST_OUT = 8;
+    public static final int BLEND_MODE_SRC_ATOP = 9;
+    public static final int BLEND_MODE_DST_ATOP = 10;
+    public static final int BLEND_MODE_XOR = 11;
+    public static final int BLEND_MODE_PLUS = 12;
+    public static final int BLEND_MODE_MODULATE = 13;
+    public static final int BLEND_MODE_SCREEN = 14;
+    public static final int BLEND_MODE_OVERLAY = 15;
+    public static final int BLEND_MODE_DARKEN = 16;
+    public static final int BLEND_MODE_LIGHTEN = 17;
+    public static final int BLEND_MODE_COLOR_DODGE = 18;
+    public static final int BLEND_MODE_COLOR_BURN = 19;
+    public static final int BLEND_MODE_HARD_LIGHT = 20;
+    public static final int BLEND_MODE_SOFT_LIGHT = 21;
+    public static final int BLEND_MODE_DIFFERENCE = 22;
+    public static final int BLEND_MODE_EXCLUSION = 23;
+    public static final int BLEND_MODE_MULTIPLY = 24;
+    public static final int BLEND_MODE_HUE = 25;
+    public static final int BLEND_MODE_SATURATION = 26;
+    public static final int BLEND_MODE_COLOR = 27;
+    public static final int BLEND_MODE_LUMINOSITY = 28;
+    public static final int BLEND_MODE_NULL = 29;
+    public static final int PORTER_MODE_ADD = 30;
+
+    public static final int FONT_NORMAL = 0;
+    public static final int FONT_BOLD = 1;
+    public static final int FONT_ITALIC = 2;
+    public static final int FONT_BOLD_ITALIC = 3;
+
+    public static final int FONT_TYPE_DEFAULT = 0;
+    public static final int FONT_TYPE_SANS_SERIF = 1;
+    public static final int FONT_TYPE_SERIF = 2;
+    public static final int FONT_TYPE_MONOSPACE = 3;
+
+    public static final int STYLE_FILL = 0;
+    public static final int STYLE_STROKE = 1;
+    public static final int STYLE_FILL_AND_STROKE = 2;
+    public static final int LINEAR_GRADIENT = 0;
+    public static final int RADIAL_GRADIENT = 1;
+    public static final int SWEEP_GRADIENT = 2;
+
+    /**
+     * sets a shader that draws a linear gradient along a line.
+     *
+     * @param startX   The x-coordinate for the start of the gradient line
+     * @param startY   The y-coordinate for the start of the gradient line
+     * @param endX     The x-coordinate for the end of the gradient line
+     * @param endY     The y-coordinate for the end of the gradient line
+     * @param colors   The sRGB colors to be distributed along the gradient line
+     * @param stops    May be null. The relative positions [0..1] of
+     *                 each corresponding color in the colors array. If this is null,
+     *                 the colors are distributed evenly along the gradient line.
+     * @param tileMode The Shader tiling mode
+     */
+    public void setLinearGradient(int[] colors,
+                                  float[] stops,
+                                  float startX,
+                                  float startY,
+                                  float endX,
+                                  float endY,
+                                  int tileMode) {
+        int startPos = mPos;
+        int len;
+        mArray[mPos++] = GRADIENT | (LINEAR_GRADIENT << 16);
+        mArray[mPos++] = len = (colors == null) ? 0 : colors.length;
+        for (int i = 0; i < len; i++) {
+            mArray[mPos++] = colors[i];
+        }
+
+        mArray[mPos++] = len = (stops == null) ? 0 : stops.length;
+        for (int i = 0; i < len; i++) {
+            mArray[mPos++] = Float.floatToRawIntBits(stops[i]);
+        }
+        mArray[mPos++] = Float.floatToRawIntBits(startX);
+        mArray[mPos++] = Float.floatToRawIntBits(startY);
+        mArray[mPos++] = Float.floatToRawIntBits(endX);
+        mArray[mPos++] = Float.floatToRawIntBits(endY);
+        mArray[mPos++] = tileMode;
+    }
+
+    /**
+     * Set a shader that draws a sweep gradient around a center point.
+     *
+     * @param centerX The x-coordinate of the center
+     * @param centerY The y-coordinate of the center
+     * @param colors  The sRGB colors to be distributed around the center.
+     *                There must be at least 2 colors in the array.
+     * @param stops   May be NULL. The relative position of
+     *                each corresponding color in the colors array, beginning
+     *                with 0 and ending with 1.0. If the values are not
+     *                monotonic, the drawing may produce unexpected results.
+     *                If positions is NULL, then the colors are automatically
+     *                spaced evenly.
+     */
+    public void setSweepGradient(int[] colors, float[] stops, float centerX, float centerY) {
+        int startPos = mPos;
+        int len;
+        mArray[mPos++] = GRADIENT | (SWEEP_GRADIENT << 16);
+        mArray[mPos++] = len = (colors == null) ? 0 : colors.length;
+        for (int i = 0; i < len; i++) {
+            mArray[mPos++] = colors[i];
+        }
+
+        mArray[mPos++] = len = (stops == null) ? 0 : stops.length;
+        for (int i = 0; i < len; i++) {
+            mArray[mPos++] = Float.floatToRawIntBits(stops[i]);
+        }
+        mArray[mPos++] = Float.floatToRawIntBits(centerX);
+        mArray[mPos++] = Float.floatToRawIntBits(centerY);
+    }
+
+    /**
+     * Sets a shader that draws a radial gradient given the center and radius.
+     *
+     * @param centerX  The x-coordinate of the center of the radius
+     * @param centerY  The y-coordinate of the center of the radius
+     * @param radius   Must be positive. The radius of the gradient.
+     * @param colors   The sRGB colors distributed between the center and edge
+     * @param stops    May be <code>null</code>.
+     *                 Valid values are between <code>0.0f</code> and
+     *                 <code>1.0f</code>. The relative position of each
+     *                 corresponding color in
+     *                 the colors array. If <code>null</code>, colors are
+     *                 distributed evenly
+     *                 between the center and edge of the circle.
+     * @param tileMode The Shader tiling mode
+     */
+    public void setRadialGradient(int[] colors,
+                                  float[] stops,
+                                  float centerX,
+                                  float centerY,
+                                  float radius,
+                                  int tileMode) {
+        int startPos = mPos;
+        int len;
+        mArray[mPos++] = GRADIENT | (RADIAL_GRADIENT << 16);
+        mArray[mPos++] = len = (colors == null) ? 0 : colors.length;
+        for (int i = 0; i < len; i++) {
+            mArray[mPos++] = colors[i];
+        }
+        mArray[mPos++] = len = (stops == null) ? 0 : stops.length;
+
+        for (int i = 0; i < len; i++) {
+            mArray[mPos++] = Float.floatToRawIntBits(stops[i]);
+        }
+        mArray[mPos++] = Float.floatToRawIntBits(centerX);
+        mArray[mPos++] = Float.floatToRawIntBits(centerY);
+        mArray[mPos++] = Float.floatToRawIntBits(radius);
+        mArray[mPos++] = tileMode;
+
+    }
+
+    /**
+     * Create a color filter that uses the specified color and Porter-Duff mode.
+     *
+     * @param color The ARGB source color used with the Porter-Duff mode
+     * @param mode  The porter-duff mode that is applied
+     */
+    public void setColorFilter(int color, int mode) {
+        mArray[mPos] = COLOR_FILTER | (mode << 16);
+        mPos++;
+        mArray[mPos++] = color;
+    }
+
+    /**
+     * Set the paint's text size. This value must be > 0
+     *
+     * @param size set the paint's text size in pixel units.
+     */
+    public void setTextSize(float size) {
+        int p = mPos;
+        mArray[mPos] = TEXT_SIZE;
+        mPos++;
+        mArray[mPos] = Float.floatToRawIntBits(size);
+        mPos++;
+    }
+
+    /**
+     * @param fontType 0 = default 1 = sans serif 2 = serif 3 = monospace
+     * @param weight    100-1000
+     * @param italic    tur
+     */
+    public void setTextStyle(int fontType, int weight, boolean italic) {
+        int style = (weight & 0x3FF) | (italic ? 2048 : 0);  // pack the weight and italic
+        mArray[mPos++] = TYPEFACE | (style << 16);
+        mArray[mPos++] = fontType;
+    }
+
+    /**
+     * Set the width for stroking.
+     * Pass 0 to stroke in hairline mode.
+     * Hairlines always draws a single pixel independent of the canvas's matrix.
+     *
+     * @param width set the paint's stroke width, used whenever the paint's
+     *              style is Stroke or StrokeAndFill.
+     */
+    public void setStrokeWidth(float width) {
+        mArray[mPos] = STROKE_WIDTH;
+        mPos++;
+        mArray[mPos] = Float.floatToRawIntBits(width);
+        mPos++;
+    }
+
+    public void setColor(int color) {
+        mArray[mPos] = COLOR;
+        mPos++;
+        mArray[mPos] = color;
+        mPos++;
+    }
+
+    /**
+     * Set the paint's Cap.
+     *
+     * @param cap set the paint's line cap style, used whenever the paint's
+     *            style is Stroke or StrokeAndFill.
+     */
+    public void setStrokeCap(int cap) {
+        mArray[mPos] = STROKE_CAP | (cap << 16);
+        mPos++;
+    }
+
+    public void setStyle(int style) {
+        mArray[mPos] = STYLE | (style << 16);
+        mPos++;
+    }
+
+    public void setShader(int shader, String shaderString) {
+        mArray[mPos] = SHADER | (shader << 16);
+        mPos++;
+    }
+
+    public void setAlpha(float alpha) {
+        mArray[mPos] = ALPHA;
+        mPos++;
+        mArray[mPos] = Float.floatToRawIntBits(alpha);
+        mPos++;
+    }
+
+    /**
+     * Set the paint's stroke miter value. This is used to control the behavior
+     * of miter joins when the joins angle is sharp. This value must be >= 0.
+     *
+     * @param miter set the miter limit on the paint, used whenever the paint's
+     *              style is Stroke or StrokeAndFill.
+     */
+    public void setStrokeMiter(float miter) {
+        mArray[mPos] = STROKE_MITER;
+        mPos++;
+        mArray[mPos] = Float.floatToRawIntBits(miter);
+        mPos++;
+    }
+
+    /**
+     * Set the paint's Join.
+     *
+     * @param join set the paint's Join, used whenever the paint's style is
+     *             Stroke or StrokeAndFill.
+     */
+    public void setStrokeJoin(int join) {
+        mArray[mPos] = STROKE_JOIN | (join << 16);
+        mPos++;
+    }
+
+    public void setFilterBitmap(boolean filter) {
+        mArray[mPos] = FILTER_BITMAP | (filter ? (1 << 16) : 0);
+        mPos++;
+    }
+
+    /**
+     * Set or clear the blend mode. A blend mode defines how source pixels
+     * (generated by a drawing command) are composited with the
+     * destination pixels
+     * (content of the render target).
+     *
+     *
+     * @param blendmode The blend mode to be installed in the paint
+     */
+    public void setBlendMode(int blendmode) {
+        mArray[mPos] = BLEND_MODE | (blendmode << 16);
+        mPos++;
+    }
+
+    /**
+     * Helper for setFlags(), setting or clearing the ANTI_ALIAS_FLAG bit
+     * AntiAliasing smooths out the edges of what is being drawn, but is has
+     * no impact on the interior of the shape. See setDither() and
+     * setFilterBitmap() to affect how colors are treated.
+     *
+     * @param aa true to set the antialias bit in the flags, false to clear it
+     */
+    public void setAntiAlias(boolean aa) {
+        mArray[mPos] = ANTI_ALIAS | (((aa) ? 1 : 0) << 16);
+        mPos++;
+    }
+
+    public void clear(long mask) { // unused for now
+    }
+
+    public void reset() {
+        mPos = 0;
+    }
+
+    public static String blendModeString(int mode) {
+        switch (mode) {
+            case PaintBundle.BLEND_MODE_CLEAR:
+                return "CLEAR";
+            case PaintBundle.BLEND_MODE_SRC:
+                return "SRC";
+            case PaintBundle.BLEND_MODE_DST:
+                return "DST";
+            case PaintBundle.BLEND_MODE_SRC_OVER:
+                return "SRC_OVER";
+            case PaintBundle.BLEND_MODE_DST_OVER:
+                return "DST_OVER";
+            case PaintBundle.BLEND_MODE_SRC_IN:
+                return "SRC_IN";
+            case PaintBundle.BLEND_MODE_DST_IN:
+                return "DST_IN";
+            case PaintBundle.BLEND_MODE_SRC_OUT:
+                return "SRC_OUT";
+            case PaintBundle.BLEND_MODE_DST_OUT:
+                return "DST_OUT";
+            case PaintBundle.BLEND_MODE_SRC_ATOP:
+                return "SRC_ATOP";
+            case PaintBundle.BLEND_MODE_DST_ATOP:
+                return "DST_ATOP";
+            case PaintBundle.BLEND_MODE_XOR:
+                return "XOR";
+            case PaintBundle.BLEND_MODE_PLUS:
+                return "PLUS";
+            case PaintBundle.BLEND_MODE_MODULATE:
+                return "MODULATE";
+            case PaintBundle.BLEND_MODE_SCREEN:
+                return "SCREEN";
+            case PaintBundle.BLEND_MODE_OVERLAY:
+                return "OVERLAY";
+            case PaintBundle.BLEND_MODE_DARKEN:
+                return "DARKEN";
+            case PaintBundle.BLEND_MODE_LIGHTEN:
+                return "LIGHTEN";
+            case PaintBundle.BLEND_MODE_COLOR_DODGE:
+                return "COLOR_DODGE";
+            case PaintBundle.BLEND_MODE_COLOR_BURN:
+                return "COLOR_BURN";
+            case PaintBundle.BLEND_MODE_HARD_LIGHT:
+                return "HARD_LIGHT";
+            case PaintBundle.BLEND_MODE_SOFT_LIGHT:
+                return "SOFT_LIGHT";
+            case PaintBundle.BLEND_MODE_DIFFERENCE:
+                return "DIFFERENCE";
+            case PaintBundle.BLEND_MODE_EXCLUSION:
+                return "EXCLUSION";
+            case PaintBundle.BLEND_MODE_MULTIPLY:
+                return "MULTIPLY";
+            case PaintBundle.BLEND_MODE_HUE:
+                return "HUE";
+            case PaintBundle.BLEND_MODE_SATURATION:
+                return "SATURATION";
+            case PaintBundle.BLEND_MODE_COLOR:
+                return "COLOR";
+            case PaintBundle.BLEND_MODE_LUMINOSITY:
+                return "LUMINOSITY";
+            case PaintBundle.BLEND_MODE_NULL:
+                return "null";
+            case PaintBundle.PORTER_MODE_ADD:
+                return "ADD";
+        }
+        return "null";
+    }
+
+}
+
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintChangeAdapter.java b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintChangeAdapter.java
new file mode 100644
index 0000000..994bf6d
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintChangeAdapter.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations.paint;
+
+public class PaintChangeAdapter implements PaintChanges {
+
+    @Override
+    public void setTextSize(float size) {
+
+    }
+
+    @Override
+    public void setTypeFace(int fontType, int weight, boolean italic) {
+
+    }
+
+
+    @Override
+    public void setStrokeWidth(float width) {
+
+    }
+
+    @Override
+    public void setColor(int color) {
+
+    }
+
+    @Override
+    public void setStrokeCap(int cap) {
+
+    }
+
+    @Override
+    public void setStyle(int style) {
+
+    }
+
+    @Override
+    public void setShader(int shader, String shaderString) {
+
+    }
+
+    @Override
+    public void setImageFilterQuality(int quality) {
+
+    }
+
+    @Override
+    public void setAlpha(float a) {
+
+    }
+
+    @Override
+    public void setStrokeMiter(float miter) {
+
+    }
+
+    @Override
+    public void setStrokeJoin(int join) {
+
+    }
+
+    @Override
+    public void setFilterBitmap(boolean filter) {
+
+    }
+
+    @Override
+    public void setBlendMode(int blendmode) {
+
+    }
+
+    @Override
+    public void setAntiAlias(boolean aa) {
+
+    }
+
+    @Override
+    public void clear(long mask) {
+
+    }
+
+    @Override
+    public void setLinearGradient(int[] colorsArray,
+                                  float[] stopsArray,
+                                  float startX,
+                                  float startY,
+                                  float endX,
+                                  float endY,
+                                  int tileMode) {
+
+    }
+
+    @Override
+    public void setRadialGradient(int[] colorsArray,
+                                  float[] stopsArray,
+                                  float centerX,
+                                  float centerY,
+                                  float radius,
+                                  int tileMode) {
+
+    }
+
+    @Override
+    public void setSweepGradient(int[] colorsArray,
+                                 float[] stopsArray,
+                                 float centerX,
+                                 float centerY) {
+
+    }
+
+    @Override
+    public void setColorFilter(int color, int mode) {
+
+    }
+
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintChanges.java b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintChanges.java
new file mode 100644
index 0000000..87e58ac
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintChanges.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations.paint;
+
+public interface PaintChanges {
+
+
+    int CLEAR_TEXT_STYLE = 1 << (PaintBundle.TYPEFACE - 1);
+    int CLEAR_COLOR = 1 << (PaintBundle.COLOR - 1);
+    int CLEAR_STROKE_WIDTH = 1 << (PaintBundle.STROKE_WIDTH - 1);
+    int CLEAR_STROKE_MITER = 1 << (PaintBundle.STROKE_MITER - 1);
+    int CLEAR_CAP = 1 << (PaintBundle.STROKE_CAP - 1);
+    int CLEAR_STYLE = 1 << (PaintBundle.STYLE - 1);
+    int CLEAR_SHADER = 1 << (PaintBundle.SHADER - 1);
+    int CLEAR_IMAGE_FILTER_QUALITY =
+            1 << (PaintBundle.IMAGE_FILTER_QUALITY - 1);
+    int CLEAR_RADIENT = 1 << (PaintBundle.GRADIENT - 1);
+    int CLEAR_ALPHA = 1 << (PaintBundle.ALPHA - 1);
+    int CLEAR_COLOR_FILTER = 1 << (PaintBundle.COLOR_FILTER - 1);
+    int VALID_BITS = 0x1FFF; // only the first 13 bit are valid now
+
+
+    void setTextSize(float size);
+    void setStrokeWidth(float width);
+    void setColor(int color);
+    void setStrokeCap(int cap);
+    void setStyle(int style);
+    void setShader(int shader, String shaderString);
+    void setImageFilterQuality(int quality);
+    void setAlpha(float a);
+    void setStrokeMiter(float miter);
+    void setStrokeJoin(int join);
+    void setFilterBitmap(boolean filter);
+    void setBlendMode(int mode);
+    void setAntiAlias(boolean aa);
+    void clear(long mask);
+    void setLinearGradient(
+            int[] colorsArray,
+            float[] stopsArray,
+            float startX,
+            float startY,
+            float endX,
+            float endY,
+            int tileMode
+    );
+
+    void setRadialGradient(
+            int[] colorsArray,
+            float[] stopsArray,
+            float centerX,
+            float centerY,
+            float radius,
+            int tileMode
+    );
+
+    void setSweepGradient(
+            int[] colorsArray,
+            float[] stopsArray,
+            float centerX,
+            float centerY
+    );
+
+
+    void setColorFilter(int color, int mode);
+
+    void setTypeFace(int fontType, int weight, boolean italic);
+}
+
diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/paint/TextPaint.java b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/TextPaint.java
new file mode 100644
index 0000000..1c0bec7
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/TextPaint.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.core.operations.paint;
+
+public interface TextPaint {
+    void setARGB(int a, int r, int g, int b);
+
+    void setDither(boolean dither);
+
+    void setElegantTextHeight(boolean elegant);
+
+    void setEndHyphenEdit(int endHyphen);
+
+    void setFakeBoldText(boolean fakeBoldText);
+
+    void setFlags(int flags);
+
+    void setFontFeatureSettings(String settings);
+
+    void setHinting(int mode);
+
+    void setLetterSpacing(float letterSpacing);
+
+    void setLinearText(boolean linearText);
+
+    void setShadowLayer(float radius, float dx, float dy, int shadowColor);
+
+    void setStartHyphenEdit(int startHyphen);
+
+    void setStrikeThruText(boolean strikeThruText);
+
+    void setStrokeCap(int cap);
+
+    void setSubpixelText(boolean subpixelText);
+
+    void setTextAlign(int align);
+
+    void setTextLocale(int locale);
+
+    void setTextLocales(int localesArray);
+
+    void setTextScaleX(float scaleX);
+
+    void setTextSize(float textSize);
+
+    void setTextSkewX(float skewX);
+
+    void setUnderlineText(boolean underlineText);
+
+    void setWordSpacing(float wordSpacing);
+}
diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java
index 3799cf6..d0d6e69 100644
--- a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java
+++ b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java
@@ -16,12 +16,25 @@
 package com.android.internal.widget.remotecompose.player.platform;
 
 import android.graphics.Bitmap;
+import android.graphics.BlendMode;
 import android.graphics.Canvas;
+import android.graphics.LinearGradient;
 import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.RadialGradient;
 import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Shader;
+import android.graphics.SweepGradient;
+import android.graphics.Typeface;
 
 import com.android.internal.widget.remotecompose.core.PaintContext;
 import com.android.internal.widget.remotecompose.core.RemoteContext;
+import com.android.internal.widget.remotecompose.core.operations.ClipPath;
+import com.android.internal.widget.remotecompose.core.operations.paint.PaintBundle;
+import com.android.internal.widget.remotecompose.core.operations.paint.PaintChanges;
 
 /**
  * An implementation of PaintContext for the Android Canvas.
@@ -71,7 +84,8 @@
                            int cdId) {
         AndroidRemoteContext androidContext = (AndroidRemoteContext) mContext;
         if (androidContext.mRemoteComposeState.containsId(imageId)) {
-            Bitmap bitmap = (Bitmap) androidContext.mRemoteComposeState.getFromId(imageId);
+            Bitmap bitmap = (Bitmap) androidContext.mRemoteComposeState
+                    .getFromId(imageId);
             mCanvas.drawBitmap(
                     bitmap,
                     new Rect(srcLeft, srcTop, srcRight, srcBottom),
@@ -89,5 +103,501 @@
     public void translate(float translateX, float translateY) {
         mCanvas.translate(translateX, translateY);
     }
+
+    @Override
+    public void drawArc(float left,
+                        float top,
+                        float right,
+                        float bottom,
+                        float startAngle,
+                        float sweepAngle) {
+        mCanvas.drawArc(left, top, right, bottom, startAngle,
+                sweepAngle, true, mPaint);
+    }
+
+    @Override
+    public void drawBitmap(int id,
+                           float left,
+                           float top,
+                           float right,
+                           float bottom) {
+        AndroidRemoteContext androidContext = (AndroidRemoteContext) mContext;
+        if (androidContext.mRemoteComposeState.containsId(id)) {
+            Bitmap bitmap =
+                    (Bitmap) androidContext.mRemoteComposeState.getFromId(id);
+            Rect src = new Rect(0, 0,
+                    bitmap.getWidth(), bitmap.getHeight());
+            RectF dst = new RectF(left, top, right, bottom);
+            mCanvas.drawBitmap(bitmap, src, dst, mPaint);
+        }
+    }
+
+    @Override
+    public void drawCircle(float centerX, float centerY, float radius) {
+        mCanvas.drawCircle(centerX, centerY, radius, mPaint);
+    }
+
+    @Override
+    public void drawLine(float x1, float y1, float x2, float y2) {
+        mCanvas.drawLine(x1, y1, x2, y2, mPaint);
+    }
+
+    @Override
+    public void drawOval(float left, float top, float right, float bottom) {
+        mCanvas.drawOval(left, top, right, bottom, mPaint);
+    }
+
+    @Override
+    public void drawPath(int id, float start, float end) {
+        mCanvas.drawPath(getPath(id, start, end), mPaint);
+    }
+
+    @Override
+    public void drawRect(float left, float top, float right, float bottom) {
+        mCanvas.drawRect(left, top, right, bottom, mPaint);
+    }
+
+    @Override
+    public void drawRoundRect(float left,
+                              float top,
+                              float right,
+                              float bottom,
+                              float radiusX,
+                              float radiusY) {
+        mCanvas.drawRoundRect(left, top, right, bottom,
+                radiusX, radiusY, mPaint);
+    }
+
+    @Override
+    public void drawTextOnPath(int textId,
+                               int pathId,
+                               float hOffset,
+                               float vOffset) {
+        mCanvas.drawTextOnPath(getText(textId), getPath(pathId, 0, 1), hOffset, vOffset, mPaint);
+    }
+
+    @Override
+    public void drawTextRun(int textID,
+                            int start,
+                            int end,
+                            int contextStart,
+                            int contextEnd,
+                            float x,
+                            float y,
+                            boolean rtl) {
+        String textToPaint = getText(textID).substring(start, end);
+        mCanvas.drawText(textToPaint, x, y, mPaint);
+    }
+
+    @Override
+    public void drawTweenPath(int path1Id,
+                              int path2Id,
+                              float tween,
+                              float start,
+                              float end) {
+        mCanvas.drawPath(getPath(path1Id, path2Id, tween, start, end), mPaint);
+    }
+
+    private static PorterDuff.Mode origamiToPorterDuffMode(int mode) {
+        switch (mode) {
+            case PaintBundle.BLEND_MODE_CLEAR:
+                return PorterDuff.Mode.CLEAR;
+            case PaintBundle.BLEND_MODE_SRC:
+                return PorterDuff.Mode.SRC;
+            case PaintBundle.BLEND_MODE_DST:
+                return PorterDuff.Mode.DST;
+            case PaintBundle.BLEND_MODE_SRC_OVER:
+                return PorterDuff.Mode.SRC_OVER;
+            case PaintBundle.BLEND_MODE_DST_OVER:
+                return PorterDuff.Mode.DST_OVER;
+            case PaintBundle.BLEND_MODE_SRC_IN:
+                return PorterDuff.Mode.SRC_IN;
+            case PaintBundle.BLEND_MODE_DST_IN:
+                return PorterDuff.Mode.DST_IN;
+            case PaintBundle.BLEND_MODE_SRC_OUT:
+                return PorterDuff.Mode.SRC_OUT;
+            case PaintBundle.BLEND_MODE_DST_OUT:
+                return PorterDuff.Mode.DST_OUT;
+            case PaintBundle.BLEND_MODE_SRC_ATOP:
+                return PorterDuff.Mode.SRC_ATOP;
+            case PaintBundle.BLEND_MODE_DST_ATOP:
+                return PorterDuff.Mode.DST_ATOP;
+            case PaintBundle.BLEND_MODE_XOR:
+                return PorterDuff.Mode.XOR;
+            case PaintBundle.BLEND_MODE_SCREEN:
+                return PorterDuff.Mode.SCREEN;
+            case PaintBundle.BLEND_MODE_OVERLAY:
+                return PorterDuff.Mode.OVERLAY;
+            case PaintBundle.BLEND_MODE_DARKEN:
+                return PorterDuff.Mode.DARKEN;
+            case PaintBundle.BLEND_MODE_LIGHTEN:
+                return PorterDuff.Mode.LIGHTEN;
+            case PaintBundle.BLEND_MODE_MULTIPLY:
+                return PorterDuff.Mode.MULTIPLY;
+            case PaintBundle.PORTER_MODE_ADD:
+                return PorterDuff.Mode.ADD;
+        }
+        return PorterDuff.Mode.SRC_OVER;
+    }
+
+    public static BlendMode origamiToBlendMode(int mode) {
+        switch (mode) {
+            case PaintBundle.BLEND_MODE_CLEAR:
+                return BlendMode.CLEAR;
+            case PaintBundle.BLEND_MODE_SRC:
+                return BlendMode.SRC;
+            case PaintBundle.BLEND_MODE_DST:
+                return BlendMode.DST;
+            case PaintBundle.BLEND_MODE_SRC_OVER:
+                return BlendMode.SRC_OVER;
+            case PaintBundle.BLEND_MODE_DST_OVER:
+                return BlendMode.DST_OVER;
+            case PaintBundle.BLEND_MODE_SRC_IN:
+                return BlendMode.SRC_IN;
+            case PaintBundle.BLEND_MODE_DST_IN:
+                return BlendMode.DST_IN;
+            case PaintBundle.BLEND_MODE_SRC_OUT:
+                return BlendMode.SRC_OUT;
+            case PaintBundle.BLEND_MODE_DST_OUT:
+                return BlendMode.DST_OUT;
+            case PaintBundle.BLEND_MODE_SRC_ATOP:
+                return BlendMode.SRC_ATOP;
+            case PaintBundle.BLEND_MODE_DST_ATOP:
+                return BlendMode.DST_ATOP;
+            case PaintBundle.BLEND_MODE_XOR:
+                return BlendMode.XOR;
+            case PaintBundle.BLEND_MODE_PLUS:
+                return BlendMode.PLUS;
+            case PaintBundle.BLEND_MODE_MODULATE:
+                return BlendMode.MODULATE;
+            case PaintBundle.BLEND_MODE_SCREEN:
+                return BlendMode.SCREEN;
+            case PaintBundle.BLEND_MODE_OVERLAY:
+                return BlendMode.OVERLAY;
+            case PaintBundle.BLEND_MODE_DARKEN:
+                return BlendMode.DARKEN;
+            case PaintBundle.BLEND_MODE_LIGHTEN:
+                return BlendMode.LIGHTEN;
+            case PaintBundle.BLEND_MODE_COLOR_DODGE:
+                return BlendMode.COLOR_DODGE;
+            case PaintBundle.BLEND_MODE_COLOR_BURN:
+                return BlendMode.COLOR_BURN;
+            case PaintBundle.BLEND_MODE_HARD_LIGHT:
+                return BlendMode.HARD_LIGHT;
+            case PaintBundle.BLEND_MODE_SOFT_LIGHT:
+                return BlendMode.SOFT_LIGHT;
+            case PaintBundle.BLEND_MODE_DIFFERENCE:
+                return BlendMode.DIFFERENCE;
+            case PaintBundle.BLEND_MODE_EXCLUSION:
+                return BlendMode.EXCLUSION;
+            case PaintBundle.BLEND_MODE_MULTIPLY:
+                return BlendMode.MULTIPLY;
+            case PaintBundle.BLEND_MODE_HUE:
+                return BlendMode.HUE;
+            case PaintBundle.BLEND_MODE_SATURATION:
+                return BlendMode.SATURATION;
+            case PaintBundle.BLEND_MODE_COLOR:
+                return BlendMode.COLOR;
+            case PaintBundle.BLEND_MODE_LUMINOSITY:
+                return BlendMode.LUMINOSITY;
+            case PaintBundle.BLEND_MODE_NULL:
+                return null;
+        }
+        return null;
+    }
+
+    @Override
+    public void applyPaint(PaintBundle mPaintData) {
+        mPaintData.applyPaintChange(new PaintChanges() {
+            @Override
+            public void setTextSize(float size) {
+                mPaint.setTextSize(size);
+            }
+
+            @Override
+            public void setTypeFace(int fontType, int weight, boolean italic) {
+                int[] type = new int[]{Typeface.NORMAL, Typeface.BOLD,
+                        Typeface.ITALIC, Typeface.BOLD_ITALIC};
+
+                switch (fontType) {
+                    case PaintBundle.FONT_TYPE_DEFAULT: {
+                        if (weight == 400 && !italic) { // for normal case
+                            mPaint.setTypeface(Typeface.DEFAULT);
+                        } else {
+                            mPaint.setTypeface(Typeface.create(Typeface.DEFAULT,
+                                    weight, italic));
+                        }
+                        break;
+                    }
+                    case PaintBundle.FONT_TYPE_SERIF: {
+                        if (weight == 400 && !italic) { // for normal case
+                            mPaint.setTypeface(Typeface.SERIF);
+                        } else {
+                            mPaint.setTypeface(Typeface.create(Typeface.SERIF,
+                                    weight, italic));
+                        }
+                        break;
+                    }
+                    case PaintBundle.FONT_TYPE_SANS_SERIF: {
+                        if (weight == 400 && !italic) { //  for normal case
+                            mPaint.setTypeface(Typeface.SANS_SERIF);
+                        } else {
+                            mPaint.setTypeface(
+                                    Typeface.create(Typeface.SANS_SERIF,
+                                            weight, italic));
+                        }
+                        break;
+                    }
+                    case PaintBundle.FONT_TYPE_MONOSPACE: {
+                        if (weight == 400 && !italic) { //  for normal case
+                            mPaint.setTypeface(Typeface.MONOSPACE);
+                        } else {
+                            mPaint.setTypeface(
+                                    Typeface.create(Typeface.MONOSPACE,
+                                            weight, italic));
+                        }
+
+                        break;
+                    }
+                }
+
+
+            }
+
+
+            @Override
+            public void setStrokeWidth(float width) {
+                mPaint.setStrokeWidth(width);
+            }
+
+            @Override
+            public void setColor(int color) {
+                mPaint.setColor(color);
+            }
+
+            @Override
+            public void setStrokeCap(int cap) {
+                mPaint.setStrokeCap(Paint.Cap.values()[cap]);
+            }
+
+            @Override
+            public void setStyle(int style) {
+                mPaint.setStyle(Paint.Style.values()[style]);
+            }
+
+            @Override
+            public void setShader(int shader, String shaderString) {
+
+            }
+
+            @Override
+            public void setImageFilterQuality(int quality) {
+                System.out.println(">>>>>>>>>>>> ");
+            }
+
+            @Override
+            public void setBlendMode(int mode) {
+                mPaint.setBlendMode(origamiToBlendMode(mode));
+            }
+
+            @Override
+            public void setAlpha(float a) {
+                mPaint.setAlpha((int) (255 * a));
+            }
+
+            @Override
+            public void setStrokeMiter(float miter) {
+                mPaint.setStrokeMiter(miter);
+            }
+
+            @Override
+            public void setStrokeJoin(int join) {
+                mPaint.setStrokeJoin(Paint.Join.values()[join]);
+            }
+
+            @Override
+            public void setFilterBitmap(boolean filter) {
+                mPaint.setFilterBitmap(filter);
+            }
+
+
+            @Override
+            public void setAntiAlias(boolean aa) {
+                mPaint.setAntiAlias(aa);
+            }
+
+            @Override
+            public void clear(long mask) {
+                if (true) return;
+                long m = mask;
+                int k = 1;
+                while (m > 0) {
+                    if ((m & 1) == 1L) {
+                        switch (k) {
+
+                            case PaintBundle.COLOR_FILTER:
+                                mPaint.setColorFilter(null);
+                                System.out.println(">>>>>>>>>>>>> CLEAR!!!!");
+                                break;
+                        }
+                    }
+                    k++;
+                    m = m >> 1;
+                }
+            }
+
+            Shader.TileMode[] mTilesModes = new Shader.TileMode[]{
+                    Shader.TileMode.CLAMP,
+                    Shader.TileMode.REPEAT,
+                    Shader.TileMode.MIRROR};
+
+
+            @Override
+            public void setLinearGradient(int[] colors,
+                                          float[] stops,
+                                          float startX,
+                                          float startY,
+                                          float endX,
+                                          float endY,
+                                          int tileMode) {
+                mPaint.setShader(new LinearGradient(startX,
+                        startY,
+                        endX,
+                        endY, colors, stops, mTilesModes[tileMode]));
+
+            }
+
+            @Override
+            public void setRadialGradient(int[] colors,
+                                          float[] stops,
+                                          float centerX,
+                                          float centerY,
+                                          float radius,
+                                          int tileMode) {
+                mPaint.setShader(new RadialGradient(centerX, centerY, radius,
+                        colors, stops, mTilesModes[tileMode]));
+            }
+
+            @Override
+            public void setSweepGradient(int[] colors,
+                                         float[] stops,
+                                         float centerX,
+                                         float centerY) {
+                mPaint.setShader(new SweepGradient(centerX, centerY, colors, stops));
+
+            }
+
+            @Override
+            public void setColorFilter(int color, int mode) {
+                PorterDuff.Mode pmode = origamiToPorterDuffMode(mode);
+                System.out.println("setting color filter to " + pmode.name());
+                if (pmode != null) {
+                    mPaint.setColorFilter(
+                            new PorterDuffColorFilter(color, pmode));
+                }
+            }
+        });
+    }
+
+    @Override
+    public void mtrixScale(float scaleX,
+                           float scaleY,
+                           float centerX,
+                           float centerY) {
+        if (Float.isNaN(centerX)) {
+            mCanvas.scale(scaleX, scaleY);
+        } else {
+            mCanvas.scale(scaleX, scaleY, centerX, centerY);
+        }
+    }
+
+    @Override
+    public void matrixTranslate(float translateX, float translateY) {
+        mCanvas.translate(translateX, translateY);
+    }
+
+    @Override
+    public void matrixSkew(float skewX, float skewY) {
+        mCanvas.skew(skewX, skewY);
+    }
+
+    @Override
+    public void matrixRotate(float rotate, float pivotX, float pivotY) {
+        if (Float.isNaN(pivotX)) {
+            mCanvas.rotate(rotate);
+        } else {
+            mCanvas.rotate(rotate, pivotX, pivotY);
+
+        }
+    }
+
+    @Override
+    public void matrixSave() {
+        mCanvas.save();
+    }
+
+    @Override
+    public void matrixRestore() {
+        mCanvas.restore();
+    }
+
+    @Override
+    public void clipRect(float left, float top, float right, float bottom) {
+        mCanvas.clipRect(left, top, right, bottom);
+    }
+
+    @Override
+    public void clipPath(int pathId, int regionOp) {
+        Path path = getPath(pathId, 0, 1);
+        if (regionOp == ClipPath.DIFFERENCE) {
+            mCanvas.clipOutPath(path); // DIFFERENCE
+        } else {
+            mCanvas.clipPath(path);  // INTERSECT
+        }
+    }
+
+    private Path getPath(int path1Id,
+                         int path2Id,
+                         float tween,
+                         float start,
+                         float end) {
+        if (tween == 0.0f) {
+            return getPath(path1Id, start, end);
+        }
+        if (tween == 1.0f) {
+            return getPath(path2Id, start, end);
+        }
+        AndroidRemoteContext androidContext = (AndroidRemoteContext) mContext;
+        float[] data1 =
+                (float[]) androidContext.mRemoteComposeState.getFromId(path1Id);
+        float[] data2 =
+                (float[]) androidContext.mRemoteComposeState.getFromId(path2Id);
+        float[] tmp = new float[data2.length];
+        for (int i = 0; i < tmp.length; i++) {
+            if (Float.isNaN(data1[i]) || Float.isNaN(data2[i])) {
+                tmp[i] = data1[i];
+            } else {
+                tmp[i] = (data2[i] - data1[i]) * tween + data1[i];
+            }
+        }
+        Path path = new Path();
+        FloatsToPath.genPath(path, tmp, start, end);
+        return path;
+    }
+
+    private Path getPath(int id, float start, float end) {
+        AndroidRemoteContext androidContext = (AndroidRemoteContext) mContext;
+        Path path = new Path();
+        if (androidContext.mRemoteComposeState.containsId(id)) {
+            float[] data =
+                    (float[]) androidContext.mRemoteComposeState.getFromId(id);
+            FloatsToPath.genPath(path, data, start, end);
+        }
+        return path;
+    }
+
+    private String getText(int id) {
+        return (String) mContext.mRemoteComposeState.getFromId(id);
+    }
 }
 
diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java
index ce15855..270e96f 100644
--- a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java
+++ b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidRemoteContext.java
@@ -43,6 +43,13 @@
     // Data handling
     ///////////////////////////////////////////////////////////////////////////////////////////////
 
+    @Override
+    public void loadPathData(int instanceId, float[] floatPath) {
+        if (!mRemoteComposeState.containsId(instanceId)) {
+            mRemoteComposeState.cache(instanceId, floatPath);
+        }
+    }
+
     /**
      * Decode a byte array into an image and cache it using the given imageId
      *
diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/FloatsToPath.java b/core/java/com/android/internal/widget/remotecompose/player/platform/FloatsToPath.java
new file mode 100644
index 0000000..2d766f8
--- /dev/null
+++ b/core/java/com/android/internal/widget/remotecompose/player/platform/FloatsToPath.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.internal.widget.remotecompose.player.platform;
+
+import static com.android.internal.widget.remotecompose.core.operations.Utils.idFromNan;
+
+import android.graphics.Path;
+import android.graphics.PathMeasure;
+import android.os.Build;
+
+import com.android.internal.widget.remotecompose.core.operations.PathData;
+
+public class FloatsToPath {
+    public static void genPath(Path retPath,
+                               float[] floatPath,
+                               float start,
+                               float stop) {
+        int i = 0;
+        Path path = new Path(); // todo this should be cached for performance
+        while (i < floatPath.length) {
+            switch (idFromNan(floatPath[i])) {
+                case PathData.MOVE: {
+                    i++;
+                    path.moveTo(floatPath[i + 0], floatPath[i + 1]);
+                    i += 2;
+                }
+                break;
+                case PathData.LINE: {
+                    i += 3;
+                    path.lineTo(floatPath[i + 0], floatPath[i + 1]);
+                    i += 2;
+                }
+                break;
+                case PathData.QUADRATIC: {
+                    i += 3;
+                    path.quadTo(
+                            floatPath[i + 0],
+                            floatPath[i + 1],
+                            floatPath[i + 2],
+                            floatPath[i + 3]
+                    );
+                    i += 4;
+
+                }
+                break;
+                case PathData.CONIC: {
+                    i += 3;
+                    if (Build.VERSION.SDK_INT >= 34) {
+                        path.conicTo(
+                                floatPath[i + 0], floatPath[i + 1],
+                                floatPath[i + 2], floatPath[i + 3],
+                                floatPath[i + 4]
+                        );
+                    }
+                    i += 5;
+                }
+                break;
+                case PathData.CUBIC: {
+                    i += 3;
+                    path.cubicTo(
+                            floatPath[i + 0], floatPath[i + 1],
+                            floatPath[i + 2], floatPath[i + 3],
+                            floatPath[i + 4], floatPath[i + 5]
+                    );
+                    i += 6;
+                }
+                break;
+                case PathData.CLOSE: {
+
+                    path.close();
+                    i++;
+                }
+                break;
+                case PathData.DONE: {
+                    i++;
+                }
+                break;
+                default: {
+                    System.err.println(" Odd command "
+                            + idFromNan(floatPath[i]));
+                }
+            }
+        }
+
+        retPath.reset();
+        if (start > 0f || stop < 1f) {
+            if (start < stop) {
+
+                PathMeasure measure = new PathMeasure(); // todo cached
+                measure.setPath(path, false);
+                float len = measure.getLength();
+                float scaleStart = Math.max(start, 0f) * len;
+                float scaleStop = Math.min(stop, 1f) * len;
+                measure.getSegment(scaleStart, scaleStop, retPath,
+                        true);
+            }
+        } else {
+
+            retPath.addPath(path);
+        }
+    }
+}
diff --git a/core/res/res/drawable/ic_bt_hearing_aid.xml b/core/res/res/drawable/ic_bt_hearing_aid.xml
index e14c99b..1517147 100644
--- a/core/res/res/drawable/ic_bt_hearing_aid.xml
+++ b/core/res/res/drawable/ic_bt_hearing_aid.xml
@@ -14,10 +14,11 @@
      limitations under the License.
 -->
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="24dp"
-        android:height="24dp"
-        android:viewportWidth="24.0"
-        android:viewportHeight="24.0">
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24.0"
+    android:viewportHeight="24.0"
+    android:tint="?android:attr/colorControlNormal">
     <path
         android:fillColor="#FF000000"
         android:pathData="M17,20c-0.29,0 -0.56,-0.06 -0.76,-0.15 -0.71,-0.37 -1.21,-0.88 -1.71,-2.38 -0.51,-1.56 -1.47,-2.29 -2.39,-3 -0.79,-0.61 -1.61,-1.24 -2.32,-2.53C9.29,10.98 9,9.93 9,9c0,-2.8 2.2,-5 5,-5s5,2.2 5,5h2c0,-3.93 -3.07,-7 -7,-7S7,5.07 7,9c0,1.26 0.38,2.65 1.07,3.9 0.91,1.65 1.98,2.48 2.85,3.15 0.81,0.62 1.39,1.07 1.71,2.05 0.6,1.82 1.37,2.84 2.73,3.55 0.51,0.23 1.07,0.35 1.64,0.35 2.21,0 4,-1.79 4,-4h-2c0,1.1 -0.9,2 -2,2zM7.64,2.64L6.22,1.22C4.23,3.21 3,5.96 3,9s1.23,5.79 3.22,7.78l1.41,-1.41C6.01,13.74 5,11.49 5,9s1.01,-4.74 2.64,-6.36zM11.5,9c0,1.38 1.12,2.5 2.5,2.5s2.5,-1.12 2.5,-2.5 -1.12,-2.5 -2.5,-2.5 -2.5,1.12 -2.5,2.5z"/>
diff --git a/core/tests/coretests/AndroidManifest.xml b/core/tests/coretests/AndroidManifest.xml
index 62d58b6..c05ea3d 100644
--- a/core/tests/coretests/AndroidManifest.xml
+++ b/core/tests/coretests/AndroidManifest.xml
@@ -254,6 +254,17 @@
             </intent-filter>
         </activity>
 
+        <activity android:name="android.widget.AbsListViewActivity"
+                android:label="AbsListViewActivity"
+                android:screenOrientation="portrait"
+                android:exported="true"
+                android:theme="@android:style/Theme.Material.Light">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.FRAMEWORK_INSTRUMENTATION_TEST" />
+            </intent-filter>
+        </activity>
+
         <activity android:name="android.widget.DatePickerActivity"
                 android:label="DatePickerActivity"
                 android:screenOrientation="portrait"
diff --git a/core/tests/coretests/res/layout/activity_abslist_view.xml b/core/tests/coretests/res/layout/activity_abslist_view.xml
new file mode 100644
index 0000000..85b4f11
--- /dev/null
+++ b/core/tests/coretests/res/layout/activity_abslist_view.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License
+  -->
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+  <view
+      class="android.widget.AbsListViewFunctionalTest$MyListView"
+      android:id="@+id/list_view"
+      android:layout_width="match_parent"
+      android:layout_height="match_parent"
+  />
+</LinearLayout>
\ No newline at end of file
diff --git a/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java b/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java
index 0c1e879..6b3cf7b 100644
--- a/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java
+++ b/core/tests/coretests/src/android/content/res/ResourcesManagerTest.java
@@ -421,6 +421,85 @@
         ResourcesManager.setInstance(oriResourcesManager);
     }
 
+    @Test
+    @SmallTest
+    @RequiresFlagsEnabled(Flags.FLAG_REGISTER_RESOURCE_PATHS)
+    public void testExistingResourcesCreatedByConstructorAfterResourcePathsRegistration()
+            throws PackageManager.NameNotFoundException {
+        // Inject ResourcesManager instance from this test to the ResourcesManager class so that all
+        // the static method can interact with this test smoothly.
+        ResourcesManager oriResourcesManager = ResourcesManager.getInstance();
+        ResourcesManager.setInstance(mResourcesManager);
+
+        // Create a Resources through constructor directly before register resources' paths.
+        final DisplayMetrics metrics = new DisplayMetrics();
+        metrics.setToDefaults();
+        final Configuration config = new Configuration();
+        config.setToDefaults();
+        Resources resources = new Resources(new AssetManager(), metrics, config);
+        assertNotNull(resources);
+
+        ResourcesImpl oriResImpl = resources.getImpl();
+
+        ApplicationInfo appInfo = mPackageManager.getApplicationInfo(TEST_LIB, 0);
+        Resources.registerResourcePaths(TEST_LIB, appInfo);
+
+        assertNotSame(oriResImpl, resources.getImpl());
+
+        String[] resourcePaths = appInfo.getAllApkPaths();
+        resourcePaths = removeDuplicates(resourcePaths);
+        ApkAssets[] loadedAssets = resources.getAssets().getApkAssets();
+        assertTrue(allResourcePathsLoaded(resourcePaths, loadedAssets));
+
+        // Package resources' paths should be cached in ResourcesManager.
+        assertEquals(Arrays.toString(resourcePaths), Arrays.toString(ResourcesManager.getInstance()
+                .getSharedLibAssetsMap().get(TEST_LIB).getAllAssetPaths()));
+
+        // Revert the ResourcesManager instance back.
+        ResourcesManager.setInstance(oriResourcesManager);
+    }
+
+    @Test
+    @SmallTest
+    @RequiresFlagsEnabled(Flags.FLAG_REGISTER_RESOURCE_PATHS)
+    public void testNewResourcesWithOutdatedImplAfterResourcePathsRegistration()
+            throws PackageManager.NameNotFoundException {
+        ResourcesManager oriResourcesManager = ResourcesManager.getInstance();
+        ResourcesManager.setInstance(mResourcesManager);
+
+        Resources old_resources = mResourcesManager.getResources(
+                null, APP_ONE_RES_DIR, null, null, null, null, null, null,
+                CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null, null);
+        assertNotNull(old_resources);
+        ResourcesImpl oldImpl = old_resources.getImpl();
+
+        ApplicationInfo appInfo = mPackageManager.getApplicationInfo(TEST_LIB, 0);
+        Resources.registerResourcePaths(TEST_LIB, appInfo);
+
+        // Create another resources with identical parameters.
+        Resources resources = mResourcesManager.getResources(
+                null, APP_ONE_RES_DIR, null, null, null, null, null, null,
+                CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null, null);
+        assertNotNull(resources);
+        // For a normal ResourcesImpl redirect, new Resources may find an old ResourcesImpl cache
+        // and reuse it based on the ResourcesKey. But for shared library ResourcesImpl redirect,
+        // new created Resources should never reuse any old impl, it has to recreate a new impl
+        // which has proper asset paths appended.
+        assertNotSame(oldImpl, resources.getImpl());
+
+        String[] resourcePaths = appInfo.getAllApkPaths();
+        resourcePaths = removeDuplicates(resourcePaths);
+        ApkAssets[] loadedAssets = resources.getAssets().getApkAssets();
+        assertTrue(allResourcePathsLoaded(resourcePaths, loadedAssets));
+
+        // Package resources' paths should be cached in ResourcesManager.
+        assertEquals(Arrays.toString(resourcePaths), Arrays.toString(ResourcesManager.getInstance()
+                .getSharedLibAssetsMap().get(TEST_LIB).getAllAssetPaths()));
+
+        // Revert the ResourcesManager instance back.
+        ResourcesManager.setInstance(oriResourcesManager);
+    }
+
     private static boolean allResourcePathsLoaded(String[] resourcePaths, ApkAssets[] loadedAsset) {
         for (int i = 0; i < resourcePaths.length; i++) {
             if (!resourcePaths[i].endsWith(".apk")) {
diff --git a/core/tests/coretests/src/android/widget/AbsListViewActivity.java b/core/tests/coretests/src/android/widget/AbsListViewActivity.java
new file mode 100644
index 0000000..a617fa4
--- /dev/null
+++ b/core/tests/coretests/src/android/widget/AbsListViewActivity.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+import com.android.frameworks.coretests.R;
+
+/**
+ * An activity for testing the AbsListView widget.
+ */
+public class AbsListViewActivity extends Activity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_abslist_view);
+    }
+}
diff --git a/core/tests/coretests/src/android/widget/AbsListViewFunctionalTest.java b/core/tests/coretests/src/android/widget/AbsListViewFunctionalTest.java
new file mode 100644
index 0000000..ceea6ca
--- /dev/null
+++ b/core/tests/coretests/src/android/widget/AbsListViewFunctionalTest.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import static android.view.flags.Flags.FLAG_VIEW_VELOCITY_API;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.util.AttributeSet;
+import android.util.PollingCheck;
+
+import androidx.test.filters.MediumTest;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.compatibility.common.util.WidgetTestUtils;
+import com.android.frameworks.coretests.R;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class AbsListViewFunctionalTest {
+    private final String[] mCountryList = new String[] {
+        "Argentina", "Australia", "Belize", "Botswana", "Brazil", "Cameroon", "China", "Cyprus",
+        "Denmark", "Djibouti", "Ethiopia", "Fiji", "Finland", "France", "Gabon", "Germany",
+        "Ghana", "Haiti", "Honduras", "Iceland", "India", "Indonesia", "Ireland", "Italy",
+        "Japan", "Kiribati", "Laos", "Lesotho", "Liberia", "Malaysia", "Mongolia", "Myanmar",
+        "Nauru", "Norway", "Oman", "Pakistan", "Philippines", "Portugal", "Romania", "Russia",
+        "Rwanda", "Singapore", "Slovakia", "Slovenia", "Somalia", "Swaziland", "Togo", "Tuvalu",
+        "Uganda", "Ukraine", "United States", "Vanuatu", "Venezuela", "Zimbabwe"
+    };
+    private AbsListViewActivity mActivity;
+    private MyListView mMyListView;
+
+    @Rule
+    public ActivityTestRule<AbsListViewActivity> mActivityRule = new ActivityTestRule<>(
+            AbsListViewActivity.class);
+
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    @Before
+    public void setUp() throws Exception {
+        mActivity = mActivityRule.getActivity();
+        mMyListView = (MyListView) mActivity.findViewById(R.id.list_view);
+    }
+
+    @Test
+    @RequiresFlagsEnabled(FLAG_VIEW_VELOCITY_API)
+    public void testLsitViewSetVelocity() throws Throwable {
+        final ArrayList<String> items = new ArrayList<>(Arrays.asList(mCountryList));
+        final ArrayAdapter<String> adapter = new ArrayAdapter<String>(mActivity,
+                android.R.layout.simple_list_item_1, items);
+
+        WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mMyListView,
+                () -> mMyListView.setAdapter(adapter));
+        mActivityRule.runOnUiThread(() -> {
+            // Create an adapter to display the list
+            mMyListView.setFrameContentVelocity(0);
+        });
+        // set setFrameContentVelocity shouldn't do anything.
+        assertEquals(mMyListView.isSetVelocityCalled, false);
+
+        mActivityRule.runOnUiThread(() -> {
+            mMyListView.fling(100);
+        });
+        PollingCheck.waitFor(100, () -> mMyListView.isSetVelocityCalled);
+        // set setFrameContentVelocity should be called when fling.
+        assertTrue(mMyListView.isSetVelocityCalled);
+    }
+
+    public static class MyListView extends ListView {
+
+        public boolean isSetVelocityCalled;
+
+        public MyListView(Context context) {
+            super(context);
+        }
+
+        public MyListView(Context context, AttributeSet attrs) {
+            super(context, attrs);
+        }
+
+        public MyListView(Context context, AttributeSet attrs, int defStyle) {
+            super(context, attrs, defStyle);
+        }
+
+        public MyListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+            super(context, attrs, defStyleAttr, defStyleRes);
+        }
+
+        @Override
+        public void setFrameContentVelocity(float pixelsPerSecond) {
+            if (pixelsPerSecond != 0) {
+                isSetVelocityCalled = true;
+            }
+        }
+    }
+}
diff --git a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
index a709d7b..6321e5d 100644
--- a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
+++ b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
@@ -96,7 +96,7 @@
         doReturn(mApplicationInfo).when(mContext).getApplicationInfo();
 
         mDispatcher = new WindowOnBackInvokedDispatcher(mContext);
-        mDispatcher.attachToWindow(mWindowSession, mWindow);
+        mDispatcher.attachToWindow(mWindowSession, mWindow, null);
     }
 
     private void waitForIdle() {
diff --git a/core/tests/coretests/src/com/android/internal/net/ConnectivityBlobStoreTest.java b/core/tests/coretests/src/com/android/internal/net/ConnectivityBlobStoreTest.java
index 68545cf..ad4ccc9 100644
--- a/core/tests/coretests/src/com/android/internal/net/ConnectivityBlobStoreTest.java
+++ b/core/tests/coretests/src/com/android/internal/net/ConnectivityBlobStoreTest.java
@@ -153,4 +153,41 @@
         final String[] actual = connectivityBlobStore.list(TEST_NAME /* prefix */);
         assertArrayEquals(expected, actual);
     }
+
+    @Test
+    public void testList_underscoreInPrefix() throws Exception {
+        final String prefix = TEST_NAME + "_";
+        final String[] unsortedNames = new String[] {
+                prefix + "000",
+                TEST_NAME + "123",
+        };
+        // The '_' in the prefix should not be treated as a wildcard so the only match is "000".
+        final String[] expected = new String[] {"000"};
+        final ConnectivityBlobStore connectivityBlobStore = createConnectivityBlobStore();
+
+        for (int i = 0; i < unsortedNames.length; i++) {
+            assertTrue(connectivityBlobStore.put(unsortedNames[i], TEST_BLOB));
+        }
+        final String[] actual = connectivityBlobStore.list(prefix);
+        assertArrayEquals(expected, actual);
+    }
+
+    @Test
+    public void testList_percentInPrefix() throws Exception {
+        final String prefix = "%" + TEST_NAME + "%";
+        final String[] unsortedNames = new String[] {
+                TEST_NAME + "12345",
+                prefix + "0",
+                "abc" + TEST_NAME + "987",
+        };
+        // The '%' in the prefix should not be treated as a wildcard so the only match is "0".
+        final String[] expected = new String[] {"0"};
+        final ConnectivityBlobStore connectivityBlobStore = createConnectivityBlobStore();
+
+        for (int i = 0; i < unsortedNames.length; i++) {
+            assertTrue(connectivityBlobStore.put(unsortedNames[i], TEST_BLOB));
+        }
+        final String[] actual = connectivityBlobStore.list(prefix);
+        assertArrayEquals(expected, actual);
+    }
 }
diff --git a/graphics/java/android/graphics/fonts/FontFamily.java b/graphics/java/android/graphics/fonts/FontFamily.java
index e441c53..199e929 100644
--- a/graphics/java/android/graphics/fonts/FontFamily.java
+++ b/graphics/java/android/graphics/fonts/FontFamily.java
@@ -120,24 +120,6 @@
         }
 
         /**
-         * Returns true if the passed font files can be used for building a variable font family
-         * that automatically adjust the `wght` and `ital` axes value for the requested
-         * weight/italic style values.
-         *
-         * This method can be used for checking that the provided font files can be used for
-         * building a variable font family created with {@link #buildVariableFamily()}.
-         * If this function returns false, the {@link #buildVariableFamily()} will fail and
-         * return null.
-         *
-         * @return true if a variable font can be built from the given fonts. Otherwise, false.
-         */
-        @FlaggedApi(FLAG_NEW_FONTS_FALLBACK_XML)
-        public boolean canBuildVariableFamily() {
-            int variableFamilyType = analyzeAndResolveVariableType(mFonts);
-            return variableFamilyType != VARIABLE_FONT_FAMILY_TYPE_UNKNOWN;
-        }
-
-        /**
          * Build a variable font family that automatically adjust the `wght` and `ital` axes value
          * for the requested weight/italic style values.
          *
@@ -158,9 +140,11 @@
          * value of the supported `wght`axis, the maximum supported `wght` value is used. The weight
          * value of the font is ignored.
          *
-         * If none of the above conditions are met, this function return {@code null}. Please check
-         * that your font files meet the above requirements or consider using the {@link #build()}
-         * method.
+         * If none of the above conditions are met, the provided font files cannot be used for
+         * variable font family and this function returns {@code null}. Even if this function
+         * returns {@code null}, you can still use {@link #build()} method for creating FontFamily
+         * instance with manually specifying variation settings by using
+         * {@link Font.Builder#setFontVariationSettings(String)}.
          *
          * @return A variable font family. null if a variable font cannot be built from the given
          *         fonts.
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp
index 9b14ce4..b749a06 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -165,15 +165,27 @@
     },
 }
 
+filegroup {
+    name: "wm_shell-shared-aidls",
+
+    srcs: [
+        "shared/**/*.aidl",
+    ],
+
+    path: "shared/src",
+}
+
 java_library {
     name: "WindowManager-Shell-shared",
 
     srcs: [
         "shared/**/*.java",
         "shared/**/*.kt",
+        ":wm_shell-shared-aidls",
     ],
     static_libs: [
         "androidx.dynamicanimation_dynamicanimation",
+        "jsr330",
     ],
 }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IHomeTransitionListener.aidl b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IHomeTransitionListener.aidl
similarity index 91%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/transition/IHomeTransitionListener.aidl
rename to libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IHomeTransitionListener.aidl
index 72fba3b..8481c44 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IHomeTransitionListener.aidl
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IHomeTransitionListener.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.transition;
+package com.android.wm.shell.shared;
 
 import android.window.RemoteTransition;
 import android.window.TransitionFilter;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl
similarity index 93%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl
rename to libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl
index 7f4a8f1..526407e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/IShellTransitions.aidl
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/IShellTransitions.aidl
@@ -14,13 +14,13 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.transition;
+package com.android.wm.shell.shared;
 
 import android.view.SurfaceControl;
 import android.window.RemoteTransition;
 import android.window.TransitionFilter;
 
-import com.android.wm.shell.transition.IHomeTransitionListener;
+import com.android.wm.shell.shared.IHomeTransitionListener;
 
 /**
  * Interface that is exposed to remote callers to manipulate the transitions feature.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java
similarity index 87%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java
rename to libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java
index da39017..5e49f55 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ShellTransitions.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/ShellTransitions.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -14,13 +14,13 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.transition;
+package com.android.wm.shell.shared;
 
 import android.annotation.NonNull;
 import android.window.RemoteTransition;
 import android.window.TransitionFilter;
 
-import com.android.wm.shell.common.annotations.ExternalThread;
+import com.android.wm.shell.shared.annotations.ExternalThread;
 
 /**
  * Interface to manage remote transitions.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellSplashscreenThread.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ChoreographerSfVsync.java
similarity index 74%
copy from libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellSplashscreenThread.java
copy to libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ChoreographerSfVsync.java
index c2fd54f..a1496ac 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellSplashscreenThread.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ChoreographerSfVsync.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -14,8 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.common.annotations;
-
+package com.android.wm.shell.shared.annotations;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Inherited;
@@ -24,10 +23,12 @@
 
 import javax.inject.Qualifier;
 
-/** Annotates a method or qualifies a provider that runs on the Shell splashscreen-thread */
+/**
+ * Annotates a method that or qualifies a provider runs aligned to the Choreographer SF vsync
+ * instead of the app vsync.
+ */
 @Documented
 @Inherited
 @Qualifier
 @Retention(RetentionPolicy.RUNTIME)
-public @interface ShellSplashscreenThread {
-}
+public @interface ChoreographerSfVsync {}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ExternalMainThread.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ExternalMainThread.java
similarity index 89%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ExternalMainThread.java
rename to libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ExternalMainThread.java
index 9ac7a12..52a717b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ExternalMainThread.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ExternalMainThread.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.common.annotations;
+package com.android.wm.shell.shared.annotations;
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellBackgroundThread.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ExternalThread.java
similarity index 77%
copy from libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellBackgroundThread.java
copy to libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ExternalThread.java
index 4cd3c90..ae5188c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellBackgroundThread.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ExternalThread.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -14,8 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.common.annotations;
-
+package com.android.wm.shell.shared.annotations;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Inherited;
@@ -24,10 +23,9 @@
 
 import javax.inject.Qualifier;
 
-/** Annotates a method or qualifies a provider that runs on the shared background thread */
+/** Annotates a method or class that is called from an external thread to the Shell threads. */
 @Documented
 @Inherited
 @Qualifier
 @Retention(RetentionPolicy.RUNTIME)
-public @interface ShellBackgroundThread {
-}
+public @interface ExternalThread {}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellBackgroundThread.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellAnimationThread.java
similarity index 83%
copy from libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellBackgroundThread.java
copy to libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellAnimationThread.java
index 4cd3c90..bd2887e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellBackgroundThread.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellAnimationThread.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -14,8 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.common.annotations;
-
+package com.android.wm.shell.shared.annotations;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Inherited;
@@ -24,10 +23,9 @@
 
 import javax.inject.Qualifier;
 
-/** Annotates a method or qualifies a provider that runs on the shared background thread */
+/** Annotates a method or qualifies a provider that runs on the Shell animation-thread */
 @Documented
 @Inherited
 @Qualifier
 @Retention(RetentionPolicy.RUNTIME)
-public @interface ShellBackgroundThread {
-}
+public @interface ShellAnimationThread {}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellBackgroundThread.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellBackgroundThread.java
similarity index 90%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellBackgroundThread.java
rename to libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellBackgroundThread.java
index 4cd3c90..586ac82 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellBackgroundThread.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellBackgroundThread.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.common.annotations;
+package com.android.wm.shell.shared.annotations;
 
 
 import java.lang.annotation.Documented;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellBackgroundThread.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellMainThread.java
similarity index 83%
copy from libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellBackgroundThread.java
copy to libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellMainThread.java
index 4cd3c90..6c879a49 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellBackgroundThread.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellMainThread.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -14,8 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.common.annotations;
-
+package com.android.wm.shell.shared.annotations;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Inherited;
@@ -24,10 +23,9 @@
 
 import javax.inject.Qualifier;
 
-/** Annotates a method or qualifies a provider that runs on the shared background thread */
+/** Annotates a method or qualifies a provider that runs on the Shell main-thread */
 @Documented
 @Inherited
 @Qualifier
 @Retention(RetentionPolicy.RUNTIME)
-public @interface ShellBackgroundThread {
-}
+public @interface ShellMainThread {}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellSplashscreenThread.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellSplashscreenThread.java
similarity index 90%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellSplashscreenThread.java
rename to libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellSplashscreenThread.java
index c2fd54f..4887dbe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellSplashscreenThread.java
+++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellSplashscreenThread.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.wm.shell.common.annotations;
+package com.android.wm.shell.shared.annotations;
 
 
 import java.lang.annotation.Documented;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java
index 8d8dc10..2643211 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimation.java
@@ -20,7 +20,7 @@
 import android.view.MotionEvent;
 import android.window.BackEvent;
 
-import com.android.wm.shell.common.annotations.ExternalThread;
+import com.android.wm.shell.shared.annotations.ExternalThread;
 
 /**
  * Interface for external process to get access to the Back animation related methods.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index 9b9798c..ad3be3d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -69,8 +69,8 @@
 import com.android.wm.shell.common.ExternalInterfaceBinder;
 import com.android.wm.shell.common.RemoteCallable;
 import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.common.annotations.ShellBackgroundThread;
-import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
 import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt
index 7561a26..3253cac 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt
@@ -42,8 +42,8 @@
 import com.android.wm.shell.R
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
 import com.android.wm.shell.animation.Interpolators
-import com.android.wm.shell.common.annotations.ShellMainThread
 import com.android.wm.shell.protolog.ShellProtoLogGroup
+import com.android.wm.shell.shared.annotations.ShellMainThread
 import javax.inject.Inject
 import kotlin.math.abs
 import kotlin.math.max
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
index cfd9fb6..cae2e80 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
@@ -49,7 +49,7 @@
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.R;
 import com.android.wm.shell.animation.Interpolators;
-import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
 
 import javax.inject.Inject;
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java
index fcf500a..e33aa75 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java
@@ -54,7 +54,7 @@
 import com.android.internal.policy.ScreenDecorationsUtils;
 import com.android.internal.policy.TransitionAnimation;
 import com.android.internal.protolog.common.ProtoLog;
-import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
 
 import javax.inject.Inject;
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index 4455a3c..ce8a460 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -101,14 +101,14 @@
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.TaskStackListenerCallback;
 import com.android.wm.shell.common.TaskStackListenerImpl;
-import com.android.wm.shell.common.annotations.ShellBackgroundThread;
-import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.common.bubbles.BubbleBarLocation;
 import com.android.wm.shell.common.bubbles.BubbleBarUpdate;
 import com.android.wm.shell.draganddrop.DragAndDropController;
 import com.android.wm.shell.onehanded.OneHandedController;
 import com.android.wm.shell.onehanded.OneHandedTransitionCallback;
 import com.android.wm.shell.pip.PinnedStackListenerForwarder;
+import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
 import com.android.wm.shell.sysui.ConfigurationChangeListener;
 import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
index 26077cf..127a49f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
@@ -37,8 +37,8 @@
 import androidx.annotation.IntDef;
 import androidx.annotation.Nullable;
 
-import com.android.wm.shell.common.annotations.ExternalThread;
 import com.android.wm.shell.common.bubbles.BubbleBarUpdate;
+import com.android.wm.shell.shared.annotations.ExternalThread;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.Target;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java
index b828aac..2873d584 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java
@@ -28,7 +28,7 @@
 
 import androidx.annotation.BinderThread;
 
-import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
 import com.android.wm.shell.sysui.ShellInit;
 
 import java.util.concurrent.CopyOnWriteArrayList;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java
index 8353900..dcbc72a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java
@@ -34,7 +34,7 @@
 import androidx.annotation.BinderThread;
 
 import com.android.wm.shell.common.DisplayChangeController.OnDisplayChangingListener;
-import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
 import com.android.wm.shell.sysui.ShellInit;
 
 import java.util.ArrayList;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java
index ca06024..55dc793 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java
@@ -30,7 +30,7 @@
 
 import androidx.annotation.BinderThread;
 
-import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
 import com.android.wm.shell.sysui.ShellInit;
 
 import java.util.concurrent.CopyOnWriteArrayList;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java
index 53683c6..43c92ca 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/TabletopModeController.java
@@ -33,7 +33,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.protolog.common.ProtoLog;
-import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
 import com.android.wm.shell.sysui.ShellInit;
 
 import java.lang.annotation.Retention;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ChoreographerSfVsync.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ChoreographerSfVsync.java
deleted file mode 100644
index 4009ad2..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ChoreographerSfVsync.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.android.wm.shell.common.annotations;
-
-import java.lang.annotation.Documented;
-import java.lang.annotation.Inherited;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-import javax.inject.Qualifier;
-
-/**
- * Annotates a method that or qualifies a provider runs aligned to the Choreographer SF vsync
- * instead of the app vsync.
- */
-@Documented
-@Inherited
-@Qualifier
-@Retention(RetentionPolicy.RUNTIME)
-public @interface ChoreographerSfVsync {}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ExternalThread.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ExternalThread.java
deleted file mode 100644
index 7560f71..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ExternalThread.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.android.wm.shell.common.annotations;
-
-import java.lang.annotation.Documented;
-import java.lang.annotation.Inherited;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-import javax.inject.Qualifier;
-
-/** Annotates a method or class that is called from an external thread to the Shell threads. */
-@Documented
-@Inherited
-@Qualifier
-@Retention(RetentionPolicy.RUNTIME)
-public @interface ExternalThread {}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellAnimationThread.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellAnimationThread.java
deleted file mode 100644
index 0479f87..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellAnimationThread.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.android.wm.shell.common.annotations;
-
-import java.lang.annotation.Documented;
-import java.lang.annotation.Inherited;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-import javax.inject.Qualifier;
-
-/** Annotates a method or qualifies a provider that runs on the Shell animation-thread */
-@Documented
-@Inherited
-@Qualifier
-@Retention(RetentionPolicy.RUNTIME)
-public @interface ShellAnimationThread {}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellMainThread.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellMainThread.java
deleted file mode 100644
index 423f4ce..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/annotations/ShellMainThread.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.android.wm.shell.common.annotations;
-
-import java.lang.annotation.Documented;
-import java.lang.annotation.Inherited;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-import javax.inject.Qualifier;
-
-/** Annotates a method or qualifies a provider that runs on the Shell main-thread */
-@Documented
-@Inherited
-@Qualifier
-@Retention(RetentionPolicy.RUNTIME)
-public @interface ShellMainThread {}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipPerfHintController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipPerfHintController.java
index 317e48e..c421dec 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipPerfHintController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipPerfHintController.java
@@ -28,7 +28,7 @@
 
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
 
 import java.io.PrintWriter;
 import java.util.Map;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java
index fa2e236..cf3ad42 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIConfiguration.java
@@ -24,8 +24,8 @@
 
 import com.android.wm.shell.R;
 import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.dagger.WMSingleton;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
 
 import javax.inject.Inject;
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java
index 216da07..0110937 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java
@@ -31,10 +31,9 @@
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.SystemWindows;
 import com.android.wm.shell.common.TransactionPool;
-import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.dagger.pip.TvPipModule;
-import com.android.wm.shell.draganddrop.DragAndDropController;
 import com.android.wm.shell.recents.RecentTasksController;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.splitscreen.tv.TvSplitScreenController;
 import com.android.wm.shell.startingsurface.StartingWindowTypeAlgorithm;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 5122114..73228de 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -58,10 +58,6 @@
 import com.android.wm.shell.common.TabletopModeController;
 import com.android.wm.shell.common.TaskStackListenerImpl;
 import com.android.wm.shell.common.TransactionPool;
-import com.android.wm.shell.common.annotations.ShellAnimationThread;
-import com.android.wm.shell.common.annotations.ShellBackgroundThread;
-import com.android.wm.shell.common.annotations.ShellMainThread;
-import com.android.wm.shell.common.annotations.ShellSplashscreenThread;
 import com.android.wm.shell.common.pip.PhonePipKeepClearAlgorithm;
 import com.android.wm.shell.common.pip.PhoneSizeSpecSource;
 import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
@@ -92,6 +88,11 @@
 import com.android.wm.shell.recents.RecentTasks;
 import com.android.wm.shell.recents.RecentTasksController;
 import com.android.wm.shell.recents.RecentsTransitionHandler;
+import com.android.wm.shell.shared.ShellTransitions;
+import com.android.wm.shell.shared.annotations.ShellAnimationThread;
+import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
+import com.android.wm.shell.shared.annotations.ShellSplashscreenThread;
 import com.android.wm.shell.splitscreen.SplitScreen;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.startingsurface.StartingSurface;
@@ -106,7 +107,6 @@
 import com.android.wm.shell.taskview.TaskViewFactoryController;
 import com.android.wm.shell.taskview.TaskViewTransitions;
 import com.android.wm.shell.transition.HomeTransitionObserver;
-import com.android.wm.shell.transition.ShellTransitions;
 import com.android.wm.shell.transition.Transitions;
 import com.android.wm.shell.unfold.ShellUnfoldProgressProvider;
 import com.android.wm.shell.unfold.UnfoldAnimationController;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java
index 0cc545a..c5644a8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java
@@ -33,11 +33,11 @@
 import com.android.wm.shell.R;
 import com.android.wm.shell.common.HandlerExecutor;
 import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.common.annotations.ExternalMainThread;
-import com.android.wm.shell.common.annotations.ShellAnimationThread;
-import com.android.wm.shell.common.annotations.ShellBackgroundThread;
-import com.android.wm.shell.common.annotations.ShellMainThread;
-import com.android.wm.shell.common.annotations.ShellSplashscreenThread;
+import com.android.wm.shell.shared.annotations.ExternalMainThread;
+import com.android.wm.shell.shared.annotations.ShellAnimationThread;
+import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
+import com.android.wm.shell.shared.annotations.ShellSplashscreenThread;
 
 import dagger.Module;
 import dagger.Provides;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 04f0f44..b933e5d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -52,9 +52,6 @@
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.TaskStackListenerImpl;
 import com.android.wm.shell.common.TransactionPool;
-import com.android.wm.shell.common.annotations.ShellAnimationThread;
-import com.android.wm.shell.common.annotations.ShellBackgroundThread;
-import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.dagger.back.ShellBackAnimationModule;
 import com.android.wm.shell.dagger.pip.PipModule;
 import com.android.wm.shell.desktopmode.DesktopModeEventLogger;
@@ -77,6 +74,9 @@
 import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.recents.RecentTasksController;
 import com.android.wm.shell.recents.RecentsTransitionHandler;
+import com.android.wm.shell.shared.annotations.ShellAnimationThread;
+import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java
index 1e3d7fb..d644006 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip1Module.java
@@ -29,7 +29,6 @@
 import com.android.wm.shell.common.SystemWindows;
 import com.android.wm.shell.common.TabletopModeController;
 import com.android.wm.shell.common.TaskStackListenerImpl;
-import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.common.pip.PhonePipKeepClearAlgorithm;
 import com.android.wm.shell.common.pip.PipAppOpsListener;
 import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
@@ -56,6 +55,7 @@
 import com.android.wm.shell.pip.phone.PipController;
 import com.android.wm.shell.pip.phone.PipMotionHelper;
 import com.android.wm.shell.pip.phone.PipTouchHandler;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
index 458ea05..ae07812 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java
@@ -25,7 +25,6 @@
 import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SystemWindows;
-import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.common.pip.PipBoundsState;
 import com.android.wm.shell.common.pip.PipDisplayLayoutState;
@@ -38,6 +37,7 @@
 import com.android.wm.shell.pip2.phone.PipController;
 import com.android.wm.shell.pip2.phone.PipScheduler;
 import com.android.wm.shell.pip2.phone.PipTransition;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java
index 54c2aea..8d1b15c1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/TvPipModule.java
@@ -29,7 +29,6 @@
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.SystemWindows;
 import com.android.wm.shell.common.TaskStackListenerImpl;
-import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.common.pip.LegacySizeSpecSource;
 import com.android.wm.shell.common.pip.PipAppOpsListener;
 import com.android.wm.shell.common.pip.PipDisplayLayoutState;
@@ -53,6 +52,7 @@
 import com.android.wm.shell.pip.tv.TvPipNotificationController;
 import com.android.wm.shell.pip.tv.TvPipTaskOrganizer;
 import com.android.wm.shell.pip.tv.TvPipTransition;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
index 5889da1..df1b062 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java
@@ -18,7 +18,7 @@
 
 import android.graphics.Region;
 
-import com.android.wm.shell.common.annotations.ExternalThread;
+import com.android.wm.shell.shared.annotations.ExternalThread;
 
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 1b1c967..c369061 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -61,8 +61,6 @@
 import com.android.wm.shell.common.ShellExecutor
 import com.android.wm.shell.common.SingleInstanceRemoteListener
 import com.android.wm.shell.common.SyncTransactionQueue
-import com.android.wm.shell.common.annotations.ExternalThread
-import com.android.wm.shell.common.annotations.ShellMainThread
 import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT
 import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT
 import com.android.wm.shell.compatui.isSingleTopActivityTranslucent
@@ -72,6 +70,8 @@
 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
 import com.android.wm.shell.recents.RecentsTransitionHandler
 import com.android.wm.shell.recents.RecentsTransitionStateListener
+import com.android.wm.shell.shared.annotations.ExternalThread
+import com.android.wm.shell.shared.annotations.ShellMainThread
 import com.android.wm.shell.splitscreen.SplitScreenController
 import com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DESKTOP_MODE
 import com.android.wm.shell.sysui.ShellCommandHandler
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
index 7da1b23..165feec 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
@@ -67,8 +67,8 @@
 import com.android.wm.shell.common.ExternalInterfaceBinder;
 import com.android.wm.shell.common.RemoteCallable;
 import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.common.annotations.ExternalMainThread;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.annotations.ExternalMainThread;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
index 73de231..863a51a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java
@@ -48,8 +48,8 @@
 
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.common.annotations.ExternalThread;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.annotations.ExternalThread;
 import com.android.wm.shell.sysui.KeyguardChangeListener;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitions.java
index 33c299f..4215b2c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitions.java
@@ -19,7 +19,7 @@
 import android.annotation.NonNull;
 import android.window.IRemoteTransition;
 
-import com.android.wm.shell.common.annotations.ExternalThread;
+import com.android.wm.shell.shared.annotations.ExternalThread;
 
 /**
  * Interface exposed to SystemUI Keyguard to register handlers for running
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java
index 2ee3348..b000e32 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHanded.java
@@ -18,7 +18,7 @@
 
 import android.os.SystemProperties;
 
-import com.android.wm.shell.common.annotations.ExternalThread;
+import com.android.wm.shell.shared.annotations.ExternalThread;
 
 /**
  * Interface to engage one handed feature.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java
index 679d4ca..39b9000 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java
@@ -55,7 +55,7 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.TaskStackListenerCallback;
 import com.android.wm.shell.common.TaskStackListenerImpl;
-import com.android.wm.shell.common.annotations.ExternalThread;
+import com.android.wm.shell.shared.annotations.ExternalThread;
 import com.android.wm.shell.sysui.ConfigurationChangeListener;
 import com.android.wm.shell.sysui.KeyguardChangeListener;
 import com.android.wm.shell.sysui.ShellCommandHandler;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
index a9aa6ba..7b1ef5c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
@@ -18,7 +18,7 @@
 
 import android.graphics.Rect;
 
-import com.android.wm.shell.common.annotations.ExternalThread;
+import com.android.wm.shell.shared.annotations.ExternalThread;
 
 import java.util.function.Consumer;
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index bd186ba..cdeb00b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -82,7 +82,6 @@
 import com.android.wm.shell.common.ScreenshotUtils;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
 import com.android.wm.shell.common.pip.PipBoundsState;
 import com.android.wm.shell.common.pip.PipDisplayLayoutState;
@@ -92,6 +91,7 @@
 import com.android.wm.shell.common.pip.PipUtils;
 import com.android.wm.shell.pip.phone.PipMotionHelper;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.transition.Transitions;
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java
index 2616b8b..eebd133 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasks.java
@@ -16,7 +16,7 @@
 
 package com.android.wm.shell.recents;
 
-import com.android.wm.shell.common.annotations.ExternalThread;
+import com.android.wm.shell.shared.annotations.ExternalThread;
 import com.android.wm.shell.util.GroupedRecentTaskInfo;
 
 import java.util.List;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index 3707207..f9fcfac 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -49,11 +49,11 @@
 import com.android.wm.shell.common.SingleInstanceRemoteListener;
 import com.android.wm.shell.common.TaskStackListenerCallback;
 import com.android.wm.shell.common.TaskStackListenerImpl;
-import com.android.wm.shell.common.annotations.ExternalThread;
-import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.desktopmode.DesktopModeStatus;
 import com.android.wm.shell.desktopmode.DesktopModeTaskRepository;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.annotations.ExternalThread;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
 import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
index 2b433e9..5762197 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java
@@ -21,8 +21,8 @@
 import android.app.ActivityManager;
 import android.graphics.Rect;
 
-import com.android.wm.shell.common.annotations.ExternalThread;
 import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition;
+import com.android.wm.shell.shared.annotations.ExternalThread;
 
 import java.util.concurrent.Executor;
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index 3e34c30..c3261bb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -90,7 +90,6 @@
 import com.android.wm.shell.common.SingleInstanceRemoteListener;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.TransactionPool;
-import com.android.wm.shell.common.annotations.ExternalThread;
 import com.android.wm.shell.common.split.SplitScreenConstants.PersistentSnapPosition;
 import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition;
 import com.android.wm.shell.common.split.SplitScreenUtils;
@@ -99,6 +98,7 @@
 import com.android.wm.shell.draganddrop.DragAndDropPolicy;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.recents.RecentTasksController;
+import com.android.wm.shell.shared.annotations.ExternalThread;
 import com.android.wm.shell.splitscreen.SplitScreen.StageType;
 import com.android.wm.shell.sysui.KeyguardChangeListener;
 import com.android.wm.shell.sysui.ShellCommandHandler;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
index 4465aef..3353c7b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingSurfaceDrawer.java
@@ -47,8 +47,8 @@
 import com.android.launcher3.icons.IconProvider;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.TransactionPool;
-import com.android.wm.shell.common.annotations.ShellSplashscreenThread;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.annotations.ShellSplashscreenThread;
 
 /**
  * A class which able to draw splash screen or snapshot as the starting window for a task.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java
index 2f6edc2..5ced1fb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java
@@ -45,7 +45,7 @@
 import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener;
 import com.android.wm.shell.common.ExternalInterfaceBinder;
 import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.common.annotations.ExternalThread;
+import com.android.wm.shell.shared.annotations.ExternalThread;
 
 import java.io.PrintWriter;
 import java.util.List;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactory.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactory.java
index a7e4b01..f0a2315 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactory.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactory.java
@@ -19,7 +19,7 @@
 import android.annotation.UiContext;
 import android.content.Context;
 
-import com.android.wm.shell.common.annotations.ExternalThread;
+import com.android.wm.shell.shared.annotations.ExternalThread;
 
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactoryController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactoryController.java
index 7eed588..e4fcff0c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactoryController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewFactoryController.java
@@ -22,7 +22,7 @@
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.common.annotations.ExternalThread;
+import com.android.wm.shell.shared.annotations.ExternalThread;
 
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
index cb2944c..c9185ae 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/HomeTransitionObserver.java
@@ -32,6 +32,7 @@
 import com.android.wm.shell.common.RemoteCallable;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SingleInstanceRemoteListener;
+import com.android.wm.shell.shared.IHomeTransitionListener;
 import com.android.wm.shell.shared.TransitionUtil;
 
 /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index a77602b..437a00e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -76,10 +76,13 @@
 import com.android.wm.shell.common.RemoteCallable;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.TransactionPool;
-import com.android.wm.shell.common.annotations.ExternalThread;
 import com.android.wm.shell.keyguard.KeyguardTransitionHandler;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.shared.IHomeTransitionListener;
+import com.android.wm.shell.shared.IShellTransitions;
+import com.android.wm.shell.shared.ShellTransitions;
 import com.android.wm.shell.shared.TransitionUtil;
+import com.android.wm.shell.shared.annotations.ExternalThread;
 import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
index 66efa02..e7d37ad 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java
@@ -51,6 +51,7 @@
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.shared.IHomeTransitionListener;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
 
diff --git a/libs/hwui/WebViewFunctorManager.cpp b/libs/hwui/WebViewFunctorManager.cpp
index 5b4ab5f..efa9b11 100644
--- a/libs/hwui/WebViewFunctorManager.cpp
+++ b/libs/hwui/WebViewFunctorManager.cpp
@@ -16,15 +16,16 @@
 
 #include "WebViewFunctorManager.h"
 
+#include <log/log.h>
 #include <private/hwui/WebViewFunctor.h>
+#include <utils/Trace.h>
+
+#include <atomic>
+
 #include "Properties.h"
 #include "renderthread/CanvasContext.h"
 #include "renderthread/RenderThread.h"
 
-#include <log/log.h>
-#include <utils/Trace.h>
-#include <atomic>
-
 namespace android::uirenderer {
 
 namespace {
@@ -265,7 +266,7 @@
 }
 
 void WebViewFunctor::reportRenderingThreads(const int32_t* thread_ids, size_t size) {
-    // TODO(b/329219352): Pass the threads to HWUI and update the ADPF session.
+    mRenderingThreads = std::vector<int32_t>(thread_ids, thread_ids + size);
 }
 
 WebViewFunctorManager& WebViewFunctorManager::instance() {
@@ -365,6 +366,21 @@
     }
 }
 
+std::vector<int32_t> WebViewFunctorManager::getRenderingThreadsForActiveFunctors() {
+    std::vector<int32_t> renderingThreads;
+    std::lock_guard _lock{mLock};
+    for (const auto& iter : mActiveFunctors) {
+        const auto& functorThreads = iter->getRenderingThreads();
+        for (const auto& tid : functorThreads) {
+            if (std::find(renderingThreads.begin(), renderingThreads.end(), tid) ==
+                renderingThreads.end()) {
+                renderingThreads.push_back(tid);
+            }
+        }
+    }
+    return renderingThreads;
+}
+
 sp<WebViewFunctor::Handle> WebViewFunctorManager::handleFor(int functor) {
     std::lock_guard _lock{mLock};
     for (auto& iter : mActiveFunctors) {
diff --git a/libs/hwui/WebViewFunctorManager.h b/libs/hwui/WebViewFunctorManager.h
index 1bf2c1f..2d77dd8 100644
--- a/libs/hwui/WebViewFunctorManager.h
+++ b/libs/hwui/WebViewFunctorManager.h
@@ -60,6 +60,10 @@
 
         void onRemovedFromTree() { mReference.onRemovedFromTree(); }
 
+        const std::vector<int32_t>& getRenderingThreads() const {
+            return mReference.getRenderingThreads();
+        }
+
     private:
         friend class WebViewFunctor;
 
@@ -82,6 +86,7 @@
     void mergeTransaction(ASurfaceTransaction* transaction);
 
     void reportRenderingThreads(const int32_t* thread_ids, size_t size);
+    const std::vector<int32_t>& getRenderingThreads() const { return mRenderingThreads; }
 
     sp<Handle> createHandle() {
         LOG_ALWAYS_FATAL_IF(mCreatedHandle);
@@ -102,6 +107,7 @@
     bool mCreatedHandle = false;
     int32_t mParentSurfaceControlGenerationId = 0;
     ASurfaceControl* mSurfaceControl = nullptr;
+    std::vector<int32_t> mRenderingThreads;
 };
 
 class WebViewFunctorManager {
@@ -113,6 +119,7 @@
     void onContextDestroyed();
     void destroyFunctor(int functor);
     void reportRenderingThreads(int functor, const int32_t* thread_ids, size_t size);
+    std::vector<int32_t> getRenderingThreadsForActiveFunctors();
 
     sp<WebViewFunctor::Handle> handleFor(int functor);
 
diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp
index abf64d0..1fbd580 100644
--- a/libs/hwui/renderthread/CanvasContext.cpp
+++ b/libs/hwui/renderthread/CanvasContext.cpp
@@ -777,6 +777,8 @@
                                  (std::min(syncDelayDuration, mLastDequeueBufferDuration)) -
                                  dequeueBufferDuration - idleDuration;
         mHintSessionWrapper->reportActualWorkDuration(actualDuration);
+        mHintSessionWrapper->setActiveFunctorThreads(
+                WebViewFunctorManager::instance().getRenderingThreadsForActiveFunctors());
     }
 
     mLastDequeueBufferDuration = dequeueBufferDuration;
diff --git a/libs/hwui/renderthread/HintSessionWrapper.cpp b/libs/hwui/renderthread/HintSessionWrapper.cpp
index 2362331..6993d52 100644
--- a/libs/hwui/renderthread/HintSessionWrapper.cpp
+++ b/libs/hwui/renderthread/HintSessionWrapper.cpp
@@ -20,6 +20,7 @@
 #include <private/performance_hint_private.h>
 #include <utils/Log.h>
 
+#include <algorithm>
 #include <chrono>
 #include <vector>
 
@@ -49,6 +50,7 @@
     BIND_APH_METHOD(updateTargetWorkDuration);
     BIND_APH_METHOD(reportActualWorkDuration);
     BIND_APH_METHOD(sendHint);
+    BIND_APH_METHOD(setThreads);
 
     mInitialized = true;
 }
@@ -67,6 +69,10 @@
         mHintSession = mHintSessionFuture->get();
         mHintSessionFuture = std::nullopt;
     }
+    if (mSetThreadsFuture.has_value()) {
+        mSetThreadsFuture->wait();
+        mSetThreadsFuture = std::nullopt;
+    }
     if (mHintSession) {
         mBinding->closeSession(mHintSession);
         mSessionValid = true;
@@ -106,16 +112,16 @@
     APerformanceHintManager* manager = mBinding->getManager();
     if (!manager) return false;
 
-    std::vector<pid_t> tids = CommonPool::getThreadIds();
-    tids.push_back(mUiThreadId);
-    tids.push_back(mRenderThreadId);
+    mPermanentSessionTids = CommonPool::getThreadIds();
+    mPermanentSessionTids.push_back(mUiThreadId);
+    mPermanentSessionTids.push_back(mRenderThreadId);
 
     // Use the cached target value if there is one, otherwise use a default. This is to ensure
     // the cached target and target in PowerHAL are consistent, and that it updates correctly
     // whenever there is a change.
     int64_t targetDurationNanos =
             mLastTargetWorkDuration == 0 ? kDefaultTargetDuration : mLastTargetWorkDuration;
-    mHintSessionFuture = CommonPool::async([=, this, tids = std::move(tids)] {
+    mHintSessionFuture = CommonPool::async([=, this, tids = mPermanentSessionTids] {
         return mBinding->createSession(manager, tids.data(), tids.size(), targetDurationNanos);
     });
     return false;
@@ -143,6 +149,23 @@
     mLastFrameNotification = systemTime();
 }
 
+void HintSessionWrapper::setActiveFunctorThreads(std::vector<pid_t> threadIds) {
+    if (!init()) return;
+    if (!mBinding || !mHintSession) return;
+    // Sort the vector to make sure they're compared as sets.
+    std::sort(threadIds.begin(), threadIds.end());
+    if (threadIds == mActiveFunctorTids) return;
+    mActiveFunctorTids = std::move(threadIds);
+    std::vector<pid_t> combinedTids = mPermanentSessionTids;
+    std::copy(mActiveFunctorTids.begin(), mActiveFunctorTids.end(),
+              std::back_inserter(combinedTids));
+    mSetThreadsFuture = CommonPool::async([this, tids = std::move(combinedTids)] {
+        int ret = mBinding->setThreads(mHintSession, tids.data(), tids.size());
+        ALOGE_IF(ret != 0, "APerformaceHint_setThreads failed: %d", ret);
+        return ret;
+    });
+}
+
 void HintSessionWrapper::sendLoadResetHint() {
     static constexpr int kMaxResetsSinceLastReport = 2;
     if (!init()) return;
diff --git a/libs/hwui/renderthread/HintSessionWrapper.h b/libs/hwui/renderthread/HintSessionWrapper.h
index 41891cd..14e7a53 100644
--- a/libs/hwui/renderthread/HintSessionWrapper.h
+++ b/libs/hwui/renderthread/HintSessionWrapper.h
@@ -20,6 +20,7 @@
 
 #include <future>
 #include <optional>
+#include <vector>
 
 #include "utils/TimeUtils.h"
 
@@ -47,11 +48,15 @@
     nsecs_t getLastUpdate();
     void delayedDestroy(renderthread::RenderThread& rt, nsecs_t delay,
                         std::shared_ptr<HintSessionWrapper> wrapperPtr);
+    // Must be called on Render thread. Otherwise can cause a race condition.
+    void setActiveFunctorThreads(std::vector<pid_t> threadIds);
 
 private:
     APerformanceHintSession* mHintSession = nullptr;
     // This needs to work concurrently for testing
     std::optional<std::shared_future<APerformanceHintSession*>> mHintSessionFuture;
+    // This needs to work concurrently for testing
+    std::optional<std::shared_future<int>> mSetThreadsFuture;
 
     int mResetsSinceLastReport = 0;
     nsecs_t mLastFrameNotification = 0;
@@ -59,6 +64,8 @@
 
     pid_t mUiThreadId;
     pid_t mRenderThreadId;
+    std::vector<pid_t> mPermanentSessionTids;
+    std::vector<pid_t> mActiveFunctorTids;
 
     bool mSessionValid = true;
 
@@ -82,6 +89,8 @@
         void (*reportActualWorkDuration)(APerformanceHintSession* session,
                                          int64_t actualDuration) = nullptr;
         void (*sendHint)(APerformanceHintSession* session, int32_t hintId) = nullptr;
+        int (*setThreads)(APerformanceHintSession* session, const pid_t* tids,
+                          size_t size) = nullptr;
 
     private:
         bool mInitialized = false;
diff --git a/libs/hwui/tests/unit/HintSessionWrapperTests.cpp b/libs/hwui/tests/unit/HintSessionWrapperTests.cpp
index 10a740a1..c16602c 100644
--- a/libs/hwui/tests/unit/HintSessionWrapperTests.cpp
+++ b/libs/hwui/tests/unit/HintSessionWrapperTests.cpp
@@ -58,6 +58,7 @@
         MOCK_METHOD(void, fakeUpdateTargetWorkDuration, (APerformanceHintSession*, int64_t));
         MOCK_METHOD(void, fakeReportActualWorkDuration, (APerformanceHintSession*, int64_t));
         MOCK_METHOD(void, fakeSendHint, (APerformanceHintSession*, int32_t));
+        MOCK_METHOD(int, fakeSetThreads, (APerformanceHintSession*, const std::vector<pid_t>&));
         // Needs to be on the binding so it can be accessed from static methods
         std::promise<int> allowCreationToFinish;
     };
@@ -102,11 +103,20 @@
     static void stubSendHint(APerformanceHintSession* session, int32_t hintId) {
         sMockBinding->fakeSendHint(session, hintId);
     };
+    static int stubSetThreads(APerformanceHintSession* session, const pid_t* ids, size_t size) {
+        std::vector<pid_t> tids(ids, ids + size);
+        return sMockBinding->fakeSetThreads(session, tids);
+    }
     void waitForWrapperReady() {
         if (mWrapper->mHintSessionFuture.has_value()) {
             mWrapper->mHintSessionFuture->wait();
         }
     }
+    void waitForSetThreadsReady() {
+        if (mWrapper->mSetThreadsFuture.has_value()) {
+            mWrapper->mSetThreadsFuture->wait();
+        }
+    }
     void scheduleDelayedDestroyManaged() {
         TestUtils::runOnRenderThread([&](renderthread::RenderThread& rt) {
             // Guaranteed to be scheduled first, allows destruction to start
@@ -130,6 +140,7 @@
     mWrapper->mBinding = sMockBinding;
     EXPECT_CALL(*sMockBinding, fakeGetManager).WillOnce(Return(managerPtr));
     ON_CALL(*sMockBinding, fakeCreateSession).WillByDefault(Return(sessionPtr));
+    ON_CALL(*sMockBinding, fakeSetThreads).WillByDefault(Return(0));
 }
 
 void HintSessionWrapperTests::MockHintSessionBinding::init() {
@@ -141,6 +152,7 @@
     sMockBinding->updateTargetWorkDuration = &stubUpdateTargetWorkDuration;
     sMockBinding->reportActualWorkDuration = &stubReportActualWorkDuration;
     sMockBinding->sendHint = &stubSendHint;
+    sMockBinding->setThreads = &stubSetThreads;
 }
 
 void HintSessionWrapperTests::TearDown() {
@@ -339,4 +351,44 @@
     EXPECT_EQ(mWrapper->alive(), false);
 }
 
+TEST_F(HintSessionWrapperTests, setThreadsUpdatesSessionThreads) {
+    EXPECT_CALL(*sMockBinding, fakeCreateSession(managerPtr, _, Gt(1), _)).Times(1);
+    EXPECT_CALL(*sMockBinding, fakeSetThreads(sessionPtr, testing::IsSupersetOf({11, 22})))
+            .Times(1);
+    mWrapper->init();
+    waitForWrapperReady();
+
+    // This changes the overall set of threads in the session, so the session wrapper should call
+    // setThreads.
+    mWrapper->setActiveFunctorThreads({11, 22});
+    waitForSetThreadsReady();
+
+    // The set of threads doesn't change, so the session wrapper should not call setThreads this
+    // time. The order of the threads shouldn't matter.
+    mWrapper->setActiveFunctorThreads({22, 11});
+    waitForSetThreadsReady();
+}
+
+TEST_F(HintSessionWrapperTests, setThreadsDoesntCrashAfterDestroy) {
+    EXPECT_CALL(*sMockBinding, fakeCloseSession(sessionPtr)).Times(1);
+
+    mWrapper->init();
+    waitForWrapperReady();
+    // Init a second time just to grab the wrapper from the promise
+    mWrapper->init();
+    EXPECT_EQ(mWrapper->alive(), true);
+
+    // Then, kill the session
+    mWrapper->destroy();
+
+    // Verify it died
+    Mock::VerifyAndClearExpectations(sMockBinding.get());
+    EXPECT_EQ(mWrapper->alive(), false);
+
+    // setActiveFunctorThreads shouldn't do anything, and shouldn't crash.
+    EXPECT_CALL(*sMockBinding, fakeSetThreads(_, _)).Times(0);
+    mWrapper->setActiveFunctorThreads({11, 22});
+    waitForSetThreadsReady();
+}
+
 }  // namespace android::uirenderer::renderthread
\ No newline at end of file
diff --git a/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java
index 1905fa8..82d43bc 100644
--- a/media/java/android/media/MediaCodec.java
+++ b/media/java/android/media/MediaCodec.java
@@ -65,6 +65,7 @@
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Supplier;
 
 /**
  MediaCodec class can be used to access low-level media codecs, i.e. encoder/decoder components.
@@ -2014,6 +2015,23 @@
         }
     }
 
+    // HACKY(b/325389296): aconfig flag accessors may not work in all contexts where MediaCodec API
+    // is used, so allow accessors to fail. In those contexts use a default value, normally false.
+
+    /* package private */
+    static boolean GetFlag(Supplier<Boolean> flagValueSupplier) {
+        return GetFlag(flagValueSupplier, false /* defaultValue */);
+    }
+
+    /* package private */
+    static boolean GetFlag(Supplier<Boolean> flagValueSupplier, boolean defaultValue) {
+        try {
+            return flagValueSupplier.get();
+        } catch (java.lang.RuntimeException e) {
+            return defaultValue;
+        }
+    }
+
     private boolean mHasSurface = false;
 
     /**
@@ -2346,7 +2364,7 @@
         }
 
         // at the moment no codecs support detachable surface
-        if (android.media.codec.Flags.nullOutputSurface()) {
+        if (GetFlag(() -> android.media.codec.Flags.nullOutputSurface())) {
             // Detached surface flag is only meaningful if surface is null. Otherwise, it is
             // ignored.
             if (surface == null && (flags & CONFIGURE_FLAG_DETACHED_SURFACE) != 0) {
diff --git a/media/java/android/media/MediaCodecInfo.java b/media/java/android/media/MediaCodecInfo.java
index abad460..8ff4305 100644
--- a/media/java/android/media/MediaCodecInfo.java
+++ b/media/java/android/media/MediaCodecInfo.java
@@ -23,6 +23,7 @@
 import static android.media.codec.Flags.FLAG_IN_PROCESS_SW_AUDIO_CODEC;
 import static android.media.codec.Flags.FLAG_NULL_OUTPUT_SURFACE;
 import static android.media.codec.Flags.FLAG_REGION_OF_INTEREST;
+import static android.media.MediaCodec.GetFlag;
 
 import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
@@ -827,10 +828,10 @@
                 features.add(new Feature(FEATURE_MultipleFrames,   (1 << 5), false));
                 features.add(new Feature(FEATURE_DynamicTimestamp, (1 << 6), false));
                 features.add(new Feature(FEATURE_LowLatency,       (1 << 7), true));
-                if (android.media.codec.Flags.dynamicColorAspects()) {
+                if (GetFlag(() -> android.media.codec.Flags.dynamicColorAspects())) {
                     features.add(new Feature(FEATURE_DynamicColorAspects, (1 << 8), true));
                 }
-                if (android.media.codec.Flags.nullOutputSurface()) {
+                if (GetFlag(() -> android.media.codec.Flags.nullOutputSurface())) {
                     features.add(new Feature(FEATURE_DetachedSurface,     (1 << 9), true));
                 }
 
@@ -851,10 +852,10 @@
                 features.add(new Feature(FEATURE_QpBounds, (1 << 3), false));
                 features.add(new Feature(FEATURE_EncodingStatistics, (1 << 4), false));
                 features.add(new Feature(FEATURE_HdrEditing, (1 << 5), false));
-                if (android.media.codec.Flags.hlgEditing()) {
+                if (GetFlag(() -> android.media.codec.Flags.hlgEditing())) {
                     features.add(new Feature(FEATURE_HlgEditing, (1 << 6), true));
                 }
-                if (android.media.codec.Flags.regionOfInterest()) {
+                if (GetFlag(() -> android.media.codec.Flags.regionOfInterest())) {
                     features.add(new Feature(FEATURE_Roi, (1 << 7), true));
                 }
 
diff --git a/media/jni/Android.bp b/media/jni/Android.bp
index d6d74e8..94fce79 100644
--- a/media/jni/Android.bp
+++ b/media/jni/Android.bp
@@ -122,9 +122,6 @@
         "-Wunused",
         "-Wunreachable-code",
     ],
-
-    // TODO(b/330503129) Workaround build breakage.
-    lto_O0: true,
 }
 
 cc_library_shared {
diff --git a/media/jni/audioeffect/Android.bp b/media/jni/audioeffect/Android.bp
index 7caa9e4..cf5059c 100644
--- a/media/jni/audioeffect/Android.bp
+++ b/media/jni/audioeffect/Android.bp
@@ -44,7 +44,4 @@
         "-Wunreachable-code",
         "-DANDROID_UTILS_REF_BASE_DISABLE_IMPLICIT_CONSTRUCTION",
     ],
-
-    // TODO(b/330503129) Workaround LTO build breakage.
-    lto_O0: true,
 }
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageUtil.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageUtil.java
index 976a3ad..bcc737a 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageUtil.java
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageUtil.java
@@ -30,6 +30,8 @@
 import android.content.pm.ProviderInfo;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.BitmapFactory;
 import android.graphics.Canvas;
 import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
@@ -46,6 +48,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.StringRes;
 
+import java.io.ByteArrayOutputStream;
 import java.io.Closeable;
 import java.io.File;
 import java.io.IOException;
@@ -135,10 +138,10 @@
 
     static final class AppSnippet implements Parcelable {
         @NonNull public CharSequence label;
-        @Nullable public Drawable icon;
+        @NonNull public Drawable icon;
         public int iconSize;
 
-        AppSnippet(@NonNull CharSequence label, @Nullable Drawable icon, Context context) {
+        AppSnippet(@NonNull CharSequence label, @NonNull Drawable icon, Context context) {
             this.label = label;
             this.icon = icon;
             final ActivityManager am = context.getSystemService(ActivityManager.class);
@@ -147,14 +150,15 @@
 
         private AppSnippet(Parcel in) {
             label = in.readString();
-            Bitmap bmp = in.readParcelable(getClass().getClassLoader(), Bitmap.class);
+            byte[] b = in.readBlob();
+            Bitmap bmp = BitmapFactory.decodeByteArray(b, 0, b.length);
             icon = new BitmapDrawable(Resources.getSystem(), bmp);
             iconSize = in.readInt();
         }
 
         @Override
         public String toString() {
-            return "AppSnippet[" + label + (icon != null ? "(has" : "(no ") + " icon)]";
+            return "AppSnippet[" + label + " (has icon)]";
         }
 
         @Override
@@ -165,16 +169,18 @@
         @Override
         public void writeToParcel(@NonNull Parcel dest, int flags) {
             dest.writeString(label.toString());
+
             Bitmap bmp = getBitmapFromDrawable(icon);
-            dest.writeParcelable(bmp, 0);
+            dest.writeBlob(getBytesFromBitmap(bmp));
+            bmp.recycle();
+
             dest.writeInt(iconSize);
         }
 
         private Bitmap getBitmapFromDrawable(Drawable drawable) {
             // Create an empty bitmap with the dimensions of our drawable
             final Bitmap bmp = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
-                    drawable.getIntrinsicHeight(),
-                    Bitmap.Config.ARGB_8888);
+                drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
             // Associate it with a canvas. This canvas will draw the icon on the bitmap
             final Canvas canvas = new Canvas(bmp);
             // Draw the drawable in the canvas. The canvas will ultimately paint the drawable in the
@@ -192,6 +198,23 @@
             return bmp;
         }
 
+        private byte[] getBytesFromBitmap(Bitmap bmp) {
+            ByteArrayOutputStream baos = null;
+            try {
+                baos = new ByteArrayOutputStream();
+                bmp.compress(CompressFormat.PNG, 100, baos);
+            } finally {
+                try {
+                    if (baos != null) {
+                        baos.close();
+                    }
+                } catch (IOException e) {
+                    Log.e(LOG_TAG, "ByteArrayOutputStream was not closed");
+                }
+            }
+            return baos.toByteArray();
+        }
+
         public static final Parcelable.Creator<AppSnippet> CREATOR = new Parcelable.Creator<>() {
             public AppSnippet createFromParcel(Parcel in) {
                 return new AppSnippet(in);
diff --git a/packages/SettingsLib/FooterPreference/res/drawable-v35/settingslib_ic_info_outline_24.xml b/packages/SettingsLib/FooterPreference/res/drawable-v35/settingslib_ic_info_outline_24.xml
new file mode 100644
index 0000000..c7fbb5f
--- /dev/null
+++ b/packages/SettingsLib/FooterPreference/res/drawable-v35/settingslib_ic_info_outline_24.xml
@@ -0,0 +1,26 @@
+<?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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="@color/settingslib_materialColorOnSurfaceVariant"
+        android:pathData="M11,17h2v-6h-2v6zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM11,9h2L13,7h-2v2z"/>
+</vector>
diff --git a/packages/SettingsLib/FooterPreference/res/layout-v35/preference_footer.xml b/packages/SettingsLib/FooterPreference/res/layout-v35/preference_footer.xml
new file mode 100644
index 0000000..a2b9648
--- /dev/null
+++ b/packages/SettingsLib/FooterPreference/res/layout-v35/preference_footer.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright (C) 2024 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:minHeight="?android:attr/listPreferredItemHeight"
+    android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+    android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+    android:background="?android:attr/selectableItemBackground"
+    android:orientation="vertical"
+    android:clipToPadding="false">
+
+    <LinearLayout
+        android:id="@+id/icon_frame"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:minWidth="56dp"
+        android:gravity="start|top"
+        android:orientation="horizontal"
+        android:paddingEnd="12dp"
+        android:paddingTop="16dp"
+        android:paddingBottom="4dp">
+        <ImageView
+            android:id="@android:id/icon"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"/>
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+        <TextView
+            android:id="@android:id/title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="start"
+            android:textAlignment="viewStart"
+            android:paddingTop="16dp"
+            android:paddingBottom="8dp"
+            android:textColor="@color/settingslib_materialColorOnSurfaceVariant"
+            android:hyphenationFrequency="normalFast"
+            android:lineBreakWordStyle="phrase"
+            android:ellipsize="marquee" />
+
+        <com.android.settingslib.widget.LinkTextView
+            android:id="@+id/settingslib_learn_more"
+            android:text="@string/settingslib_learn_more_text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="start"
+            android:textAlignment="viewStart"
+            android:paddingBottom="8dp"
+            android:clickable="true"
+            android:visibility="gone" />
+    </LinearLayout>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SettingsLib/SettingsTheme/res/values-v35/styles.xml b/packages/SettingsLib/SettingsTheme/res/values-v35/styles.xml
new file mode 100644
index 0000000..fff41c3
--- /dev/null
+++ b/packages/SettingsLib/SettingsTheme/res/values-v35/styles.xml
@@ -0,0 +1,24 @@
+<?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.
+  -->
+<resources>
+    <style name="TextAppearance.TopIntroText"
+        parent="@android:style/TextAppearance.DeviceDefault">
+        <item name="android:textSize">14sp</item>
+        <item name="android:textColor">@color/settingslib_materialColorOnSurfaceVariant</item>
+    </style>
+
+</resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index 8a5dfef..69e4bd7 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1030,13 +1030,6 @@
     <!-- Settings item title to select whether to disable cache for transcoding. [CHAR LIMIT=85] -->
     <string name="transcode_disable_cache">Disable transcoding cache</string>
 
-    <!-- Developer settings title: widevine settings screen. [CHAR LIMIT=50] -->
-    <string name="widevine_settings_title">Widevine settings</string>
-     <!-- Developer settings title: select whether to enable Force L3 fallback. [CHAR LIMIT=50] -->
-    <string name="force_l3_fallback_title">Force L3 fallback</string>
-     <!-- Developer settings summary: select whether to enable Force L3 fallback.[CHAR LIMIT=NONE] -->
-    <string name="force_l3_fallback_summary">Select to force L3 fallback</string>
-
     <!-- Services settings screen, setting option name for the user to go to the screen to view running services -->
     <string name="runningservices_settings_title">Running services</string>
     <!-- Services settings screen, setting option summary for the user to go to the screen to view running services  -->
diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
index 2889ce2..56118da 100644
--- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
+++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java
@@ -51,9 +51,11 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.text.format.Formatter;
+import android.util.ArrayMap;
 import android.util.Log;
 import android.util.SparseArray;
 
+import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
 import androidx.lifecycle.Lifecycle;
 import androidx.lifecycle.LifecycleObserver;
@@ -990,11 +992,22 @@
                 apps = new ArrayList<>(mAppEntries);
             }
 
+            ArrayMap<UserHandle, Boolean> profileHideInQuietModeStatus = new ArrayMap<>();
             ArrayList<AppEntry> filteredApps = new ArrayList<>();
             if (DEBUG) {
                 Log.i(TAG, "Rebuilding...");
             }
             for (AppEntry entry : apps) {
+                if (android.multiuser.Flags.enablePrivateSpaceFeatures()
+                        && android.multiuser.Flags.handleInterleavedSettingsForPrivateSpace()) {
+                    UserHandle userHandle = UserHandle.of(UserHandle.getUserId(entry.info.uid));
+                    if (!profileHideInQuietModeStatus.containsKey(userHandle)) {
+                        profileHideInQuietModeStatus.put(
+                                userHandle, isHideInQuietEnabledForProfile(mUm, userHandle));
+                    }
+                    filter.refreshAppEntryOnRebuild(
+                            entry, profileHideInQuietModeStatus.get(userHandle));
+                }
                 if (entry != null && (filter == null || filter.filterApp(entry))) {
                     synchronized (mEntriesMap) {
                         if (DEBUG_LOCKING) {
@@ -1648,6 +1661,11 @@
          */
         public boolean isHomeApp;
 
+        /**
+         * Whether the app should be hidden for user when quiet mode is enabled.
+         */
+        public boolean hideInQuietMode;
+
         public String getNormalizedLabel() {
             if (normalizedLabel != null) {
                 return normalizedLabel;
@@ -1691,6 +1709,7 @@
             UserInfo userInfo = um.getUserInfo(UserHandle.getUserId(info.uid));
             mProfileType = userInfo.userType;
             this.showInPersonalTab = shouldShowInPersonalTab(um, info.uid);
+            hideInQuietMode = shouldHideInQuietMode(um, info.uid);
         }
 
         public boolean isClonedProfile() {
@@ -1800,12 +1819,32 @@
                 this.labelDescription = this.label;
             }
         }
+
+        /**
+         * Returns true if profile is in quiet mode and the profile should not be visible when the
+         * quiet mode is enabled, false otherwise.
+         */
+        private boolean shouldHideInQuietMode(@NonNull UserManager userManager, int uid) {
+            if (android.multiuser.Flags.enablePrivateSpaceFeatures()
+                    && android.multiuser.Flags.handleInterleavedSettingsForPrivateSpace()) {
+                UserHandle userHandle = UserHandle.of(UserHandle.getUserId(uid));
+                return isHideInQuietEnabledForProfile(userManager, userHandle);
+            }
+            return false;
+        }
     }
 
     private static boolean hasFlag(int flags, int flag) {
         return (flags & flag) != 0;
     }
 
+    private static boolean isHideInQuietEnabledForProfile(
+            UserManager userManager, UserHandle userHandle) {
+        return userManager.isQuietModeEnabled(userHandle)
+                && userManager.getUserProperties(userHandle).getShowInQuietMode()
+                == UserProperties.SHOW_IN_QUIET_MODE_HIDDEN;
+    }
+
     /**
      * Compare by label, then package name, then uid.
      */
@@ -1868,6 +1907,15 @@
         }
 
         boolean filterApp(AppEntry info);
+
+        /**
+         * Updates AppEntry based on whether quiet mode is enabled and should not be
+         * visible for the corresponding profile.
+         */
+        default void refreshAppEntryOnRebuild(
+                @NonNull AppEntry appEntry,
+                boolean hideInQuietMode) {
+        }
     }
 
     public static final AppFilter FILTER_PERSONAL = new AppFilter() {
@@ -2010,6 +2058,25 @@
         }
     };
 
+    public static final AppFilter FILTER_ENABLED_NOT_QUIET = new AppFilter() {
+        @Override
+        public void init() {
+        }
+
+        @Override
+        public boolean filterApp(@NonNull AppEntry entry) {
+            return entry.info.enabled && !AppUtils.isInstant(entry.info)
+                    && !entry.hideInQuietMode;
+        }
+
+        @Override
+        public void refreshAppEntryOnRebuild(
+                @NonNull AppEntry appEntry,
+                boolean hideInQuietMode) {
+            appEntry.hideInQuietMode = hideInQuietMode;
+        }
+    };
+
     public static final AppFilter FILTER_EVERYTHING = new AppFilter() {
         @Override
         public void init() {
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt
index e7fec69..65a5317 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt
@@ -73,6 +73,10 @@
     suspend fun setVolume(audioStream: AudioStream, volume: Int)
 
     suspend fun setMuted(audioStream: AudioStream, isMuted: Boolean)
+
+    suspend fun setRingerMode(audioStream: AudioStream, mode: RingerMode)
+
+    suspend fun isAffectedByMute(audioStream: AudioStream): Boolean
 }
 
 class AudioRepositoryImpl(
@@ -169,6 +173,16 @@
             )
         }
 
+    override suspend fun setRingerMode(audioStream: AudioStream, mode: RingerMode) {
+        withContext(backgroundCoroutineContext) { audioManager.ringerMode = mode.value }
+    }
+
+    override suspend fun isAffectedByMute(audioStream: AudioStream): Boolean {
+        return withContext(backgroundCoroutineContext) {
+            audioManager.isStreamAffectedByMute(audioStream.value)
+        }
+    }
+
     private fun getMinVolume(stream: AudioStream): Int =
         try {
             audioManager.getStreamMinVolume(stream.value)
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt b/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt
index 778653b..33f917e 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt
@@ -49,8 +49,14 @@
     suspend fun setVolume(audioStream: AudioStream, volume: Int) =
         audioRepository.setVolume(audioStream, volume)
 
-    suspend fun setMuted(audioStream: AudioStream, isMuted: Boolean) =
+    suspend fun setMuted(audioStream: AudioStream, isMuted: Boolean) {
+        if (audioStream.value == AudioManager.STREAM_RING) {
+            val mode =
+                if (isMuted) AudioManager.RINGER_MODE_VIBRATE else AudioManager.RINGER_MODE_NORMAL
+            audioRepository.setRingerMode(audioStream, RingerMode(mode))
+        }
         audioRepository.setMuted(audioStream, isMuted)
+    }
 
     /** Checks if the volume can be changed via the UI. */
     fun canChangeVolume(audioStream: AudioStream): Flow<Boolean> {
@@ -66,9 +72,8 @@
         }
     }
 
-    fun isMutable(audioStream: AudioStream): Boolean =
-        // Alarm stream doesn't support muting
-        audioStream.value != AudioManager.STREAM_ALARM
+    suspend fun isAffectedByMute(audioStream: AudioStream): Boolean =
+        audioRepository.isAffectedByMute(audioStream)
 
     private suspend fun processVolume(
         audioStreamModel: AudioStreamModel,
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java
index fe83ffb..b974888 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/applications/ApplicationsStateTest.java
@@ -267,6 +267,16 @@
     }
 
     @Test
+    public void testEnabledFilterNotQuietRejectsInstantApp() {
+        mSetFlagsRule.enableFlags(android.multiuser.Flags.FLAG_SUPPORT_AUTOLOCK_FOR_PRIVATE_SPACE,
+                android.multiuser.Flags.FLAG_HANDLE_INTERLEAVED_SETTINGS_FOR_PRIVATE_SPACE);
+        mEntry.info.enabled = true;
+        assertThat(ApplicationsState.FILTER_ENABLED_NOT_QUIET.filterApp(mEntry)).isTrue();
+        when(mEntry.info.isInstantApp()).thenReturn(true);
+        assertThat(ApplicationsState.FILTER_ENABLED_NOT_QUIET.filterApp(mEntry)).isFalse();
+    }
+
+    @Test
     public void testFilterWithDomainUrls() {
         mEntry.info.privateFlags |= ApplicationInfo.PRIVATE_FLAG_HAS_DOMAIN_URLS;
         // should included updated system apps
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java
index f70ad9e..5f3d1ea 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java
+++ b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java
@@ -62,6 +62,7 @@
 import org.junit.Assume;
 import org.junit.Before;
 import org.junit.BeforeClass;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -452,6 +453,8 @@
     }
 
     @Test
+    @Ignore("Test failure in pre/postsubmit cannot be replicated on local devices. "
+            + "Coverage is low-impact.")
     public void testOnScreenLock_cannotOpenMenu() throws Throwable {
         closeScreen();
         wakeUpScreen();
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
index dbdf970..24cc8a4 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogTransitionAnimator.kt
@@ -711,7 +711,6 @@
         dialog.setDismissOverride(this::onDialogDismissed)
 
         if (featureFlags.isPredictiveBackQsDialogAnim) {
-            // TODO(b/265923095) Improve animations for QS dialogs on configuration change
             dialog.registerAnimationOnBackInvoked(targetView = dialogContentWithBackground)
         }
 
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpec.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpec.kt
index dd32851..4d327e1 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpec.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpec.kt
@@ -37,7 +37,7 @@
 
 /** Create a [BackAnimationSpec] from [displayMetrics] and design specs. */
 fun BackAnimationSpec.Companion.createFloatingSurfaceAnimationSpec(
-    displayMetrics: DisplayMetrics,
+    displayMetricsProvider: () -> DisplayMetrics,
     maxMarginXdp: Float,
     maxMarginYdp: Float,
     minScale: Float,
@@ -45,18 +45,19 @@
     translateYEasing: Interpolator = Interpolators.LINEAR,
     scaleEasing: Interpolator = Interpolators.STANDARD_DECELERATE,
 ): BackAnimationSpec {
-    val screenWidthPx = displayMetrics.widthPixels
-    val screenHeightPx = displayMetrics.heightPixels
-
-    val maxMarginXPx = maxMarginXdp.dpToPx(displayMetrics)
-    val maxMarginYPx = maxMarginYdp.dpToPx(displayMetrics)
-    val maxTranslationXByScale = (screenWidthPx - screenWidthPx * minScale) / 2
-    val maxTranslationX = maxTranslationXByScale - maxMarginXPx
-    val maxTranslationYByScale = (screenHeightPx - screenHeightPx * minScale) / 2
-    val maxTranslationY = maxTranslationYByScale - maxMarginYPx
-    val minScaleReversed = 1f - minScale
-
     return BackAnimationSpec { backEvent, progressY, result ->
+        val displayMetrics = displayMetricsProvider()
+        val screenWidthPx = displayMetrics.widthPixels
+        val screenHeightPx = displayMetrics.heightPixels
+
+        val maxMarginXPx = maxMarginXdp.dpToPx(displayMetrics)
+        val maxMarginYPx = maxMarginYdp.dpToPx(displayMetrics)
+        val maxTranslationXByScale = (screenWidthPx - screenWidthPx * minScale) / 2
+        val maxTranslationX = maxTranslationXByScale - maxMarginXPx
+        val maxTranslationYByScale = (screenHeightPx - screenHeightPx * minScale) / 2
+        val maxTranslationY = maxTranslationYByScale - maxMarginYPx
+        val minScaleReversed = 1f - minScale
+
         val direction = if (backEvent.swipeEdge == BackEvent.EDGE_LEFT) 1 else -1
         val progressX = backEvent.progress
 
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpecForSysUi.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpecForSysUi.kt
index b8d4469..b057296 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpecForSysUi.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/back/BackAnimationSpecForSysUi.kt
@@ -23,10 +23,10 @@
  * https://carbon.googleplex.com/predictive-back-for-apps/pages/st-1-dismiss-app
  */
 fun BackAnimationSpec.Companion.dismissAppForSysUi(
-    displayMetrics: DisplayMetrics,
+    displayMetricsProvider: () -> DisplayMetrics,
 ): BackAnimationSpec =
     BackAnimationSpec.createFloatingSurfaceAnimationSpec(
-        displayMetrics = displayMetrics,
+        displayMetricsProvider = displayMetricsProvider,
         maxMarginXdp = 8f,
         maxMarginYdp = 8f,
         minScale = 0.8f,
@@ -37,10 +37,10 @@
  * https://carbon.googleplex.com/predictive-back-for-apps/pages/st-2-cross-task
  */
 fun BackAnimationSpec.Companion.crossTaskForSysUi(
-    displayMetrics: DisplayMetrics,
+    displayMetricsProvider: () -> DisplayMetrics,
 ): BackAnimationSpec =
     BackAnimationSpec.createFloatingSurfaceAnimationSpec(
-        displayMetrics = displayMetrics,
+        displayMetricsProvider = displayMetricsProvider,
         maxMarginXdp = 8f,
         maxMarginYdp = 8f,
         minScale = 0.8f,
@@ -51,10 +51,10 @@
  * https://carbon.googleplex.com/predictive-back-for-apps/pages/st-3-inner-area-dismiss
  */
 fun BackAnimationSpec.Companion.innerAreaDismissForSysUi(
-    displayMetrics: DisplayMetrics,
+    displayMetricsProvider: () -> DisplayMetrics,
 ): BackAnimationSpec =
     BackAnimationSpec.createFloatingSurfaceAnimationSpec(
-        displayMetrics = displayMetrics,
+        displayMetricsProvider = displayMetricsProvider,
         maxMarginXdp = 0f,
         maxMarginYdp = 0f,
         minScale = 0.9f,
@@ -65,10 +65,10 @@
  * https://carbon.googleplex.com/predictive-back-for-apps/pages/st-4-floating-system-surfaces
  */
 fun BackAnimationSpec.Companion.floatingSystemSurfacesForSysUi(
-    displayMetrics: DisplayMetrics,
+    displayMetricsProvider: () -> DisplayMetrics,
 ): BackAnimationSpec =
     BackAnimationSpec.createFloatingSurfaceAnimationSpec(
-        displayMetrics = displayMetrics,
+        displayMetricsProvider = displayMetricsProvider,
         maxMarginXdp = 8f,
         maxMarginYdp = 8f,
         minScale = 0.9f,
diff --git a/packages/SystemUI/animation/src/com/android/systemui/util/Dialog.kt b/packages/SystemUI/animation/src/com/android/systemui/util/Dialog.kt
index 0f63548..9dd2328 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/util/Dialog.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/util/Dialog.kt
@@ -39,7 +39,7 @@
     targetView: View,
     backAnimationSpec: BackAnimationSpec =
         BackAnimationSpec.floatingSystemSurfacesForSysUi(
-            displayMetrics = targetView.resources.displayMetrics,
+            displayMetricsProvider = { targetView.resources.displayMetrics },
         ),
 ) {
     targetView.registerOnBackInvokedCallbackOnViewAttached(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
index a1d8c29..bdd888f 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt
@@ -8,12 +8,15 @@
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.dimensionResource
 import com.android.compose.animation.scene.Edge
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.FixedSizeEdgeDetector
 import com.android.compose.animation.scene.LowestZIndexScenePicker
+import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
 import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.SceneScope
 import com.android.compose.animation.scene.SceneTransitionLayout
@@ -21,12 +24,13 @@
 import com.android.compose.animation.scene.SwipeDirection
 import com.android.compose.animation.scene.observableTransitionState
 import com.android.compose.animation.scene.transitions
-import com.android.compose.animation.scene.updateSceneTransitionLayoutState
 import com.android.compose.theme.LocalAndroidColorScheme
 import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel
 import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
 import com.android.systemui.res.R
+import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
+import com.android.systemui.scene.ui.composable.SceneTransitionLayoutDataSource
 import com.android.systemui.statusbar.phone.SystemUIDialogFactory
 
 object Communal {
@@ -63,27 +67,35 @@
 fun CommunalContainer(
     modifier: Modifier = Modifier,
     viewModel: CommunalViewModel,
+    dataSourceDelegator: SceneDataSourceDelegator,
     dialogFactory: SystemUIDialogFactory,
 ) {
-    val currentScene: SceneKey by viewModel.currentScene.collectAsState(CommunalScenes.Blank)
-    val sceneTransitionLayoutState =
-        updateSceneTransitionLayoutState(
-            currentScene,
-            onChangeScene = { viewModel.onSceneChanged(it) },
+    val coroutineScope = rememberCoroutineScope()
+    val currentSceneKey: SceneKey by viewModel.currentScene.collectAsState(CommunalScenes.Blank)
+    val touchesAllowed by viewModel.touchesAllowed.collectAsState(initial = false)
+    val state: MutableSceneTransitionLayoutState = remember {
+        MutableSceneTransitionLayoutState(
+            initialScene = currentSceneKey,
             transitions = sceneTransitions,
             enableInterruptions = false,
         )
-    val touchesAllowed by viewModel.touchesAllowed.collectAsState(initial = false)
+    }
+
+    DisposableEffect(state) {
+        val dataSource = SceneTransitionLayoutDataSource(state, coroutineScope)
+        dataSourceDelegator.setDelegate(dataSource)
+        onDispose { dataSourceDelegator.setDelegate(null) }
+    }
 
     // This effect exposes the SceneTransitionLayout's observable transition state to the rest of
     // the system, and unsets it when the view is disposed to avoid a memory leak.
-    DisposableEffect(viewModel, sceneTransitionLayoutState) {
-        viewModel.setTransitionState(sceneTransitionLayoutState.observableTransitionState())
+    DisposableEffect(viewModel, state) {
+        viewModel.setTransitionState(state.observableTransitionState())
         onDispose { viewModel.setTransitionState(null) }
     }
 
     SceneTransitionLayout(
-        state = sceneTransitionLayoutState,
+        state = state,
         modifier = modifier.fillMaxSize(),
         swipeSourceDetector =
             FixedSizeEdgeDetector(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index 503779c..ed80277 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -482,7 +482,7 @@
     Card(
         modifier = Modifier.height(Dimensions.GridHeight).padding(contentPadding),
         colors = CardDefaults.cardColors(containerColor = Color.Transparent),
-        border = BorderStroke(3.dp, colors.primaryFixedDim),
+        border = BorderStroke(3.dp, colors.secondary),
         shape = RoundedCornerShape(size = 80.dp)
     ) {
         Column(
@@ -495,7 +495,7 @@
                 text = stringResource(R.string.title_for_empty_state_cta),
                 style = MaterialTheme.typography.displaySmall,
                 textAlign = TextAlign.Center,
-                color = colors.secondaryFixed,
+                color = colors.secondary,
             )
             Row(
                 modifier = Modifier.fillMaxWidth(),
@@ -505,8 +505,8 @@
                     modifier = Modifier.height(56.dp),
                     colors =
                         ButtonDefaults.buttonColors(
-                            containerColor = colors.primaryFixed,
-                            contentColor = colors.onPrimaryFixed,
+                            containerColor = colors.primary,
+                            contentColor = colors.onPrimary,
                         ),
                     onClick = {
                         viewModel.onOpenWidgetEditor(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
index 1178cc8..4bef9ef 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.keyguard.ui.composable
 
-import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.collectAsState
@@ -24,9 +23,7 @@
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.LocalView
-import com.android.compose.animation.scene.SceneKey
-import com.android.compose.animation.scene.SceneTransitionLayout
-import com.android.compose.animation.scene.transitions
+import com.android.compose.animation.scene.SceneScope
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.ui.composable.blueprint.ComposableLockscreenSceneBlueprint
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
@@ -45,16 +42,12 @@
     private val blueprints: Set<@JvmSuppressWildcards ComposableLockscreenSceneBlueprint>,
     private val clockInteractor: KeyguardClockInteractor,
 ) {
-
-    private val sceneKeyByBlueprint: Map<ComposableLockscreenSceneBlueprint, SceneKey> by lazy {
-        blueprints.associateWith { blueprint -> SceneKey(blueprint.id) }
-    }
-    private val sceneKeyByBlueprintId: Map<String, SceneKey> by lazy {
-        sceneKeyByBlueprint.entries.associate { (blueprint, sceneKey) -> blueprint.id to sceneKey }
+    private val blueprintByBlueprintId: Map<String, ComposableLockscreenSceneBlueprint> by lazy {
+        blueprints.associateBy { it.id }
     }
 
     @Composable
-    fun Content(
+    fun SceneScope.Content(
         modifier: Modifier = Modifier,
     ) {
         val coroutineScope = rememberCoroutineScope()
@@ -66,19 +59,7 @@
             onDispose { clockInteractor.clockEventController.unregisterListeners() }
         }
 
-        // Switch smoothly between blueprints, any composable tagged with element() will be
-        // transition-animated between any two blueprints, if they both display the same element.
-        SceneTransitionLayout(
-            currentScene = checkNotNull(sceneKeyByBlueprintId[blueprintId]),
-            onChangeScene = {},
-            transitions =
-                transitions { sceneKeyByBlueprintId.values.forEach { sceneKey -> to(sceneKey) } },
-            modifier = modifier,
-            enableInterruptions = false,
-        ) {
-            sceneKeyByBlueprint.entries.forEach { (blueprint, sceneKey) ->
-                scene(sceneKey) { with(blueprint) { Content(Modifier.fillMaxSize()) } }
-            }
-        }
+        val blueprint = blueprintByBlueprintId[blueprintId] ?: return
+        with(blueprint) { Content(modifier) }
     }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
index 7acb4d5..c241f9c 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenScene.kt
@@ -66,9 +66,5 @@
         key = QuickSettings.SharedValues.TilesSquishiness,
     )
 
-    lockscreenContent
-        .get()
-        .Content(
-            modifier = modifier.fillMaxSize(),
-        )
+    with(lockscreenContent.get()) { Content(modifier = modifier.fillMaxSize()) }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
index 320c455..c008a1a 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
@@ -16,10 +16,15 @@
 
 package com.android.systemui.keyguard.ui.composable.blueprint
 
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.unit.IntRect
@@ -28,6 +33,7 @@
 import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection
 import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection
 import com.android.systemui.keyguard.ui.composable.section.LockSection
+import com.android.systemui.keyguard.ui.composable.section.NotificationSection
 import com.android.systemui.keyguard.ui.composable.section.SettingsMenuSection
 import com.android.systemui.keyguard.ui.composable.section.StatusBarSection
 import com.android.systemui.keyguard.ui.composable.section.TopAreaSection
@@ -52,6 +58,7 @@
     private val bottomAreaSection: BottomAreaSection,
     private val settingsMenuSection: SettingsMenuSection,
     private val topAreaSection: TopAreaSection,
+    private val notificationSection: NotificationSection,
 ) : ComposableLockscreenSceneBlueprint {
 
     override val id: String = "default"
@@ -59,6 +66,8 @@
     @Composable
     override fun SceneScope.Content(modifier: Modifier) {
         val isUdfpsVisible = viewModel.isUdfpsVisible
+        val shouldUseSplitNotificationShade by
+            viewModel.shouldUseSplitNotificationShade.collectAsState()
 
         LockscreenLongPress(
             viewModel = viewModel.longPress,
@@ -68,10 +77,27 @@
                 content = {
                     // Constrained to above the lock icon.
                     Column(
-                        modifier = Modifier.fillMaxWidth(),
+                        modifier = Modifier.fillMaxSize(),
                     ) {
                         with(statusBarSection) { StatusBar(modifier = Modifier.fillMaxWidth()) }
-                        with(topAreaSection) { DefaultClockLayoutWithNotifications() }
+
+                        Box {
+                            with(topAreaSection) { DefaultClockLayout() }
+                            if (shouldUseSplitNotificationShade) {
+                                with(notificationSection) {
+                                    Notifications(
+                                        Modifier.fillMaxWidth(0.5f)
+                                            .fillMaxHeight()
+                                            .align(alignment = Alignment.TopEnd)
+                                    )
+                                }
+                            }
+                        }
+                        if (!shouldUseSplitNotificationShade) {
+                            with(notificationSection) {
+                                Notifications(Modifier.weight(weight = 1f))
+                            }
+                        }
                         if (!isUdfpsVisible && ambientIndicationSectionOptional.isPresent) {
                             with(ambientIndicationSectionOptional.get()) {
                                 AmbientIndication(modifier = Modifier.fillMaxWidth())
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
index 64c2cb3..091a439 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
@@ -16,10 +16,15 @@
 
 package com.android.systemui.keyguard.ui.composable.blueprint
 
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.unit.IntRect
@@ -28,6 +33,7 @@
 import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection
 import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection
 import com.android.systemui.keyguard.ui.composable.section.LockSection
+import com.android.systemui.keyguard.ui.composable.section.NotificationSection
 import com.android.systemui.keyguard.ui.composable.section.SettingsMenuSection
 import com.android.systemui.keyguard.ui.composable.section.StatusBarSection
 import com.android.systemui.keyguard.ui.composable.section.TopAreaSection
@@ -52,6 +58,7 @@
     private val bottomAreaSection: BottomAreaSection,
     private val settingsMenuSection: SettingsMenuSection,
     private val topAreaSection: TopAreaSection,
+    private val notificationSection: NotificationSection,
 ) : ComposableLockscreenSceneBlueprint {
 
     override val id: String = "shortcuts-besides-udfps"
@@ -59,6 +66,8 @@
     @Composable
     override fun SceneScope.Content(modifier: Modifier) {
         val isUdfpsVisible = viewModel.isUdfpsVisible
+        val shouldUseSplitNotificationShade by
+            viewModel.shouldUseSplitNotificationShade.collectAsState()
 
         LockscreenLongPress(
             viewModel = viewModel.longPress,
@@ -68,11 +77,27 @@
                 content = {
                     // Constrained to above the lock icon.
                     Column(
-                        modifier = Modifier.fillMaxWidth(),
+                        modifier = Modifier.fillMaxSize(),
                     ) {
                         with(statusBarSection) { StatusBar(modifier = Modifier.fillMaxWidth()) }
-                        with(topAreaSection) { DefaultClockLayoutWithNotifications() }
 
+                        Box {
+                            with(topAreaSection) { DefaultClockLayout() }
+                            if (shouldUseSplitNotificationShade) {
+                                with(notificationSection) {
+                                    Notifications(
+                                        Modifier.fillMaxWidth(0.5f)
+                                            .fillMaxHeight()
+                                            .align(alignment = Alignment.TopEnd)
+                                    )
+                                }
+                            }
+                        }
+                        if (!shouldUseSplitNotificationShade) {
+                            with(notificationSection) {
+                                Notifications(Modifier.weight(weight = 1f))
+                            }
+                        }
                         if (!isUdfpsVisible && ambientIndicationSectionOptional.isPresent) {
                             with(ambientIndicationSectionOptional.get()) {
                                 AmbientIndication(modifier = Modifier.fillMaxWidth())
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/WeatherClockBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/WeatherClockBlueprint.kt
index fe774a0..09d76a3 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/WeatherClockBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/WeatherClockBlueprint.kt
@@ -86,6 +86,7 @@
         val burnIn = rememberBurnIn(clockInteractor)
         val resources = LocalContext.current.resources
         val currentClockState = clockViewModel.currentClock.collectAsState()
+        val areNotificationsVisible by viewModel.areNotificationsVisible.collectAsState()
         LockscreenLongPress(
             viewModel = viewModel.longPress,
             modifier = modifier,
@@ -145,7 +146,7 @@
 
                         with(mediaCarouselSection) { MediaCarousel() }
 
-                        if (viewModel.areNotificationsVisible) {
+                        if (areNotificationsVisible) {
                             with(notificationSection) {
                                 Notifications(
                                     modifier = Modifier.fillMaxWidth().weight(weight = 1f)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
index 6b86a48..fa0a1c4 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
@@ -17,12 +17,25 @@
 package com.android.systemui.keyguard.ui.composable.section
 
 import android.view.ViewGroup
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
 import com.android.compose.animation.scene.SceneScope
+import com.android.compose.modifiers.thenIf
+import com.android.systemui.Flags
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.MigrateClocksToBlueprint
+import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
 import com.android.systemui.notifications.ui.composable.NotificationStack
+import com.android.systemui.res.R
+import com.android.systemui.shade.LargeScreenHeaderHelper
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
 import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
 import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder
@@ -39,6 +52,7 @@
     sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
     stackScrollLayout: NotificationStackScrollLayout,
     sharedNotificationContainerBinder: SharedNotificationContainerBinder,
+    private val lockscreenContentViewModel: LockscreenContentViewModel,
 ) {
 
     init {
@@ -65,9 +79,27 @@
 
     @Composable
     fun SceneScope.Notifications(modifier: Modifier = Modifier) {
+        val shouldUseSplitNotificationShade by
+            lockscreenContentViewModel.shouldUseSplitNotificationShade.collectAsState()
+        val areNotificationsVisible by
+            lockscreenContentViewModel.areNotificationsVisible.collectAsState()
+        val splitShadeTopMargin: Dp =
+            if (Flags.centralizedStatusBarHeightFix()) {
+                LargeScreenHeaderHelper.getLargeScreenHeaderHeight(LocalContext.current).dp
+            } else {
+                dimensionResource(id = R.dimen.large_screen_shade_header_height)
+            }
+
+        if (!areNotificationsVisible) {
+            return
+        }
+
         NotificationStack(
             viewModel = viewModel,
-            modifier = modifier,
+            modifier =
+                modifier.fillMaxWidth().thenIf(shouldUseSplitNotificationShade) {
+                    Modifier.padding(top = splitShadeTopMargin)
+                },
         )
     }
 }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt
index b4472fc..f8e6341 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt
@@ -16,30 +16,20 @@
 
 package com.android.systemui.keyguard.ui.composable.section
 
-import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.offset
-import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
-import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.dimensionResource
-import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.unit.dp
+import com.android.compose.animation.scene.SceneScope
 import com.android.compose.animation.scene.SceneTransitionLayout
 import com.android.compose.modifiers.thenIf
-import com.android.systemui.Flags
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.keyguard.ui.composable.blueprint.ClockScenes.largeClockScene
 import com.android.systemui.keyguard.ui.composable.blueprint.ClockScenes.smallClockScene
@@ -48,8 +38,6 @@
 import com.android.systemui.keyguard.ui.composable.blueprint.ClockTransition
 import com.android.systemui.keyguard.ui.composable.blueprint.rememberBurnIn
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
-import com.android.systemui.res.R
-import com.android.systemui.shade.LargeScreenHeaderHelper
 import javax.inject.Inject
 
 class TopAreaSection
@@ -58,19 +46,16 @@
     private val clockViewModel: KeyguardClockViewModel,
     private val smartSpaceSection: SmartSpaceSection,
     private val mediaCarouselSection: MediaCarouselSection,
-    private val notificationSection: NotificationSection,
     private val clockSection: DefaultClockSection,
     private val clockInteractor: KeyguardClockInteractor,
 ) {
     @Composable
-    fun DefaultClockLayoutWithNotifications(
+    fun DefaultClockLayout(
         modifier: Modifier = Modifier,
     ) {
-        val isLargeClockVisible by clockViewModel.isLargeClockVisible.collectAsState()
         val currentClockLayout by clockViewModel.currentClockLayout.collectAsState()
         val hasCustomPositionUpdatedAnimation by
             clockViewModel.hasCustomPositionUpdatedAnimation.collectAsState()
-
         val currentScene =
             when (currentClockLayout) {
                 KeyguardClockViewModel.ClockLayout.SPLIT_SHADE_LARGE_CLOCK ->
@@ -81,144 +66,83 @@
                 KeyguardClockViewModel.ClockLayout.SMALL_CLOCK -> smallClockScene
             }
 
-        val splitShadeTopMargin: Dp =
-            if (Flags.centralizedStatusBarHeightFix()) {
-                LargeScreenHeaderHelper.getLargeScreenHeaderHeight(LocalContext.current).dp
-            } else {
-                dimensionResource(id = R.dimen.large_screen_shade_header_height)
+        SceneTransitionLayout(
+            modifier = modifier,
+            currentScene = currentScene,
+            onChangeScene = {},
+            transitions = ClockTransition.defaultClockTransitions,
+            enableInterruptions = false,
+        ) {
+            scene(splitShadeLargeClockScene) {
+                LargeClockWithSmartSpace(
+                    shouldOffSetClockToOneHalf = !hasCustomPositionUpdatedAnimation
+                )
             }
+
+            scene(splitShadeSmallClockScene) {
+                SmallClockWithSmartSpace(modifier = Modifier.fillMaxWidth(0.5f))
+            }
+
+            scene(smallClockScene) { SmallClockWithSmartSpace() }
+
+            scene(largeClockScene) { LargeClockWithSmartSpace() }
+        }
+    }
+
+    @Composable
+    private fun SceneScope.SmallClockWithSmartSpace(modifier: Modifier = Modifier) {
         val burnIn = rememberBurnIn(clockInteractor)
 
+        Column(modifier = modifier) {
+            with(clockSection) {
+                SmallClock(
+                    burnInParams = burnIn.parameters,
+                    onTopChanged = burnIn.onSmallClockTopChanged,
+                    modifier = Modifier.wrapContentSize()
+                )
+            }
+            with(smartSpaceSection) {
+                SmartSpace(
+                    burnInParams = burnIn.parameters,
+                    onTopChanged = burnIn.onSmartspaceTopChanged,
+                )
+            }
+            with(mediaCarouselSection) { MediaCarousel() }
+        }
+    }
+
+    @Composable
+    private fun SceneScope.LargeClockWithSmartSpace(shouldOffSetClockToOneHalf: Boolean = false) {
+        val burnIn = rememberBurnIn(clockInteractor)
+        val isLargeClockVisible by clockViewModel.isLargeClockVisible.collectAsState()
+
         LaunchedEffect(isLargeClockVisible) {
             if (isLargeClockVisible) {
                 burnIn.onSmallClockTopChanged(null)
             }
         }
 
-        SceneTransitionLayout(
-            modifier = modifier.fillMaxSize(),
-            currentScene = currentScene,
-            onChangeScene = {},
-            transitions = ClockTransition.defaultClockTransitions,
-            enableInterruptions = false,
-        ) {
-            scene(splitShadeLargeClockScene) {
-                Box(modifier = Modifier.fillMaxSize()) {
-                    Column(
-                        modifier = Modifier.fillMaxSize(),
-                        horizontalAlignment = Alignment.CenterHorizontally,
-                    ) {
-                        with(smartSpaceSection) {
-                            SmartSpace(
-                                burnInParams = burnIn.parameters,
-                                onTopChanged = burnIn.onSmartspaceTopChanged,
-                            )
-                        }
-
-                        with(clockSection) {
-                            LargeClock(
-                                modifier =
-                                    Modifier.fillMaxSize().thenIf(
-                                        !hasCustomPositionUpdatedAnimation
-                                    ) {
-                                        // If we do not have a custom position animation, we want
-                                        // the clock to be on one half of the screen.
-                                        Modifier.offset {
-                                            IntOffset(
-                                                x =
-                                                    -clockSection
-                                                        .getClockCenteringDistance()
-                                                        .toInt(),
-                                                y = 0,
-                                            )
-                                        }
-                                    }
-                            )
-                        }
-                    }
-                }
-
-                Row(
-                    modifier = Modifier.fillMaxSize(),
-                ) {
-                    Spacer(modifier = Modifier.weight(weight = 1f))
-                    with(notificationSection) {
-                        Notifications(
-                            modifier =
-                                Modifier.fillMaxHeight()
-                                    .weight(weight = 1f)
-                                    .padding(top = splitShadeTopMargin)
-                        )
-                    }
-                }
+        Column {
+            with(smartSpaceSection) {
+                SmartSpace(
+                    burnInParams = burnIn.parameters,
+                    onTopChanged = burnIn.onSmartspaceTopChanged,
+                )
             }
-
-            scene(splitShadeSmallClockScene) {
-                Row(
-                    modifier = Modifier.fillMaxSize(),
-                ) {
-                    Column(
-                        modifier = Modifier.fillMaxHeight().weight(weight = 1f),
-                        horizontalAlignment = Alignment.CenterHorizontally,
-                    ) {
-                        with(clockSection) {
-                            SmallClock(
-                                burnInParams = burnIn.parameters,
-                                onTopChanged = burnIn.onSmallClockTopChanged,
-                                modifier = Modifier.wrapContentSize()
-                            )
+            with(clockSection) {
+                LargeClock(
+                    modifier =
+                        Modifier.fillMaxSize().thenIf(shouldOffSetClockToOneHalf) {
+                            // If we do not have a custom position animation, we want
+                            // the clock to be on one half of the screen.
+                            Modifier.offset {
+                                IntOffset(
+                                    x = -clockSection.getClockCenteringDistance().toInt(),
+                                    y = 0,
+                                )
+                            }
                         }
-                        with(smartSpaceSection) {
-                            SmartSpace(
-                                burnInParams = burnIn.parameters,
-                                onTopChanged = burnIn.onSmartspaceTopChanged,
-                            )
-                        }
-                        with(mediaCarouselSection) { MediaCarousel() }
-                    }
-                    with(notificationSection) {
-                        Notifications(
-                            modifier =
-                                Modifier.fillMaxHeight()
-                                    .weight(weight = 1f)
-                                    .padding(top = splitShadeTopMargin)
-                        )
-                    }
-                }
-            }
-
-            scene(smallClockScene) {
-                Column {
-                    with(clockSection) {
-                        SmallClock(
-                            burnInParams = burnIn.parameters,
-                            onTopChanged = burnIn.onSmallClockTopChanged,
-                            modifier = Modifier.wrapContentSize()
-                        )
-                    }
-                    with(smartSpaceSection) {
-                        SmartSpace(
-                            burnInParams = burnIn.parameters,
-                            onTopChanged = burnIn.onSmartspaceTopChanged,
-                        )
-                    }
-                    with(mediaCarouselSection) { MediaCarousel() }
-                    with(notificationSection) {
-                        Notifications(modifier = Modifier.fillMaxWidth().weight(weight = 1f))
-                    }
-                }
-            }
-
-            scene(largeClockScene) {
-                Column {
-                    with(smartSpaceSection) {
-                        SmartSpace(
-                            burnInParams = burnIn.parameters,
-                            onTopChanged = burnIn.onSmartspaceTopChanged,
-                        )
-                    }
-                    with(clockSection) { LargeClock(modifier = Modifier.fillMaxSize()) }
-                }
+                )
             }
         }
     }
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index 25c649a..e086cda 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -27,7 +27,9 @@
 import android.text.format.DateFormat
 import android.util.AttributeSet
 import android.util.MathUtils.constrainedMap
+import android.util.TypedValue
 import android.view.View
+import android.view.View.MeasureSpec.EXACTLY
 import android.widget.TextView
 import com.android.app.animation.Interpolators
 import com.android.internal.annotations.VisibleForTesting
@@ -42,6 +44,7 @@
 import java.util.Calendar
 import java.util.Locale
 import java.util.TimeZone
+import kotlin.math.min
 
 /**
  * Displays the time with the hour positioned above the minutes. (ie: 09 above 30 is 9:30)
@@ -85,6 +88,8 @@
     private var textAnimator: TextAnimator? = null
     private var onTextAnimatorInitialized: Runnable? = null
 
+    // last text size which is not constrained by view height
+    private var lastUnconstrainedTextSize: Float = Float.MAX_VALUE
     @VisibleForTesting var textAnimatorFactory: (Layout, () -> Unit) -> TextAnimator =
         { layout, invalidateCb ->
             TextAnimator(layout, NUM_CLOCK_FONT_ANIMATION_STEPS, invalidateCb) }
@@ -188,6 +193,11 @@
     @SuppressLint("DrawAllocation")
     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
         logger.d("onMeasure")
+        if (migratedClocks && !isSingleLineInternal &&
+                MeasureSpec.getMode(heightMeasureSpec) == EXACTLY) {
+            setTextSize(TypedValue.COMPLEX_UNIT_PX,
+                    min(lastUnconstrainedTextSize, MeasureSpec.getSize(heightMeasureSpec) / 2F))
+        }
         super.onMeasure(widthMeasureSpec, heightMeasureSpec)
         val animator = textAnimator
         if (animator == null) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
index 43266bf..a944afb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
@@ -88,7 +88,7 @@
             testScope.runTest {
                 val scene by collectLastValue(communalInteractor.desiredScene)
 
-                communalInteractor.onSceneChanged(CommunalScenes.Communal)
+                communalInteractor.changeScene(CommunalScenes.Communal)
                 assertThat(scene).isEqualTo(CommunalScenes.Communal)
 
                 fakeKeyguardTransitionRepository.sendTransitionSteps(
@@ -158,7 +158,7 @@
         with(kosmos) {
             testScope.runTest {
                 val scene by collectLastValue(communalInteractor.desiredScene)
-                communalInteractor.onSceneChanged(CommunalScenes.Communal)
+                communalInteractor.changeScene(CommunalScenes.Communal)
                 assertThat(scene).isEqualTo(CommunalScenes.Communal)
 
                 fakeKeyguardTransitionRepository.sendTransitionSteps(
@@ -179,7 +179,7 @@
         with(kosmos) {
             testScope.runTest {
                 val scene by collectLastValue(communalInteractor.desiredScene)
-                communalInteractor.onSceneChanged(CommunalScenes.Communal)
+                communalInteractor.changeScene(CommunalScenes.Communal)
                 assertThat(scene).isEqualTo(CommunalScenes.Communal)
 
                 fakeKeyguardTransitionRepository.sendTransitionSteps(
@@ -207,7 +207,7 @@
     fun dockingOnLockscreen_forcesCommunal() =
         with(kosmos) {
             testScope.runTest {
-                communalInteractor.onSceneChanged(CommunalScenes.Blank)
+                communalInteractor.changeScene(CommunalScenes.Blank)
                 val scene by collectLastValue(communalInteractor.desiredScene)
 
                 // device is docked while on the lockscreen
@@ -229,7 +229,7 @@
     fun dockingOnLockscreen_doesNotForceCommunalIfDreamStarts() =
         with(kosmos) {
             testScope.runTest {
-                communalInteractor.onSceneChanged(CommunalScenes.Blank)
+                communalInteractor.changeScene(CommunalScenes.Blank)
                 val scene by collectLastValue(communalInteractor.desiredScene)
 
                 // device is docked while on the lockscreen
@@ -261,7 +261,7 @@
             testScope.runTest {
                 // Device is dreaming and on communal.
                 fakeKeyguardRepository.setDreaming(true)
-                communalInteractor.onSceneChanged(CommunalScenes.Communal)
+                communalInteractor.changeScene(CommunalScenes.Communal)
 
                 val scene by collectLastValue(communalInteractor.desiredScene)
                 assertThat(scene).isEqualTo(CommunalScenes.Communal)
@@ -278,7 +278,7 @@
             testScope.runTest {
                 // Device is not dreaming and on communal.
                 fakeKeyguardRepository.setDreaming(false)
-                communalInteractor.onSceneChanged(CommunalScenes.Communal)
+                communalInteractor.changeScene(CommunalScenes.Communal)
 
                 // Scene stays as Communal
                 advanceTimeBy(SCREEN_TIMEOUT.milliseconds)
@@ -293,7 +293,7 @@
             testScope.runTest {
                 // Device is dreaming and on communal.
                 fakeKeyguardRepository.setDreaming(true)
-                communalInteractor.onSceneChanged(CommunalScenes.Communal)
+                communalInteractor.changeScene(CommunalScenes.Communal)
 
                 val scene by collectLastValue(communalInteractor.desiredScene)
                 assertThat(scene).isEqualTo(CommunalScenes.Communal)
@@ -316,7 +316,7 @@
             testScope.runTest {
                 // Device is on communal, but not dreaming.
                 fakeKeyguardRepository.setDreaming(false)
-                communalInteractor.onSceneChanged(CommunalScenes.Communal)
+                communalInteractor.changeScene(CommunalScenes.Communal)
 
                 val scene by collectLastValue(communalInteractor.desiredScene)
                 assertThat(scene).isEqualTo(CommunalScenes.Communal)
@@ -338,7 +338,7 @@
             testScope.runTest {
                 // Device is dreaming and on communal.
                 fakeKeyguardRepository.setDreaming(true)
-                communalInteractor.onSceneChanged(CommunalScenes.Communal)
+                communalInteractor.changeScene(CommunalScenes.Communal)
 
                 val scene by collectLastValue(communalInteractor.desiredScene)
                 assertThat(scene).isEqualTo(CommunalScenes.Communal)
@@ -367,7 +367,7 @@
 
                 // Device is dreaming and on communal.
                 fakeKeyguardRepository.setDreaming(true)
-                communalInteractor.onSceneChanged(CommunalScenes.Communal)
+                communalInteractor.changeScene(CommunalScenes.Communal)
 
                 val scene by collectLastValue(communalInteractor.desiredScene)
                 assertThat(scene).isEqualTo(CommunalScenes.Communal)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt
index 43acf31..2d78a9b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt
@@ -22,36 +22,26 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.scene.data.repository.sceneContainerRepository
-import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags
+import com.android.systemui.scene.shared.model.sceneDataSource
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.test.runTest
-import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class CommunalRepositoryImplTest : SysuiTestCase() {
-    private lateinit var underTest: CommunalRepositoryImpl
 
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private val sceneContainerRepository = kosmos.sceneContainerRepository
-
-    @Before
-    fun setUp() {
-        underTest = createRepositoryImpl(false)
-    }
-
-    private fun createRepositoryImpl(sceneContainerEnabled: Boolean): CommunalRepositoryImpl {
-        return CommunalRepositoryImpl(
-            testScope.backgroundScope,
-            kosmos.fakeSceneContainerFlags.apply { enabled = sceneContainerEnabled },
-            sceneContainerRepository,
+    private val underTest by lazy {
+        CommunalRepositoryImpl(
+            kosmos.applicationCoroutineScope,
+            kosmos.sceneDataSource,
         )
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
index 8e9d769..e7ccde2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
@@ -482,7 +482,7 @@
             assertThat(desiredScene()).isEqualTo(CommunalScenes.Blank)
 
             val targetScene = CommunalScenes.Communal
-            communalRepository.setDesiredScene(targetScene)
+            communalRepository.changeScene(targetScene)
             desiredScene = collectLastValue(underTest.desiredScene)
             runCurrent()
             assertThat(desiredScene()).isEqualTo(targetScene)
@@ -493,9 +493,9 @@
         testScope.runTest {
             val targetScene = CommunalScenes.Communal
 
-            underTest.onSceneChanged(targetScene)
+            underTest.changeScene(targetScene)
 
-            val desiredScene = collectLastValue(communalRepository.desiredScene)
+            val desiredScene = collectLastValue(communalRepository.currentScene)
             runCurrent()
             assertThat(desiredScene()).isEqualTo(targetScene)
         }
@@ -508,7 +508,7 @@
 
             val desiredScene by collectLastValue(underTest.desiredScene)
 
-            underTest.onSceneChanged(CommunalScenes.Communal)
+            underTest.changeScene(CommunalScenes.Communal)
             assertThat(desiredScene).isEqualTo(CommunalScenes.Communal)
 
             kosmos.setCommunalAvailable(false)
@@ -659,7 +659,7 @@
             runCurrent()
             assertThat(isCommunalShowing()).isEqualTo(false)
 
-            underTest.onSceneChanged(CommunalScenes.Communal)
+            underTest.changeScene(CommunalScenes.Communal)
 
             isCommunalShowing = collectLastValue(underTest.isCommunalShowing)
             runCurrent()
@@ -683,12 +683,12 @@
             assertThat(isCommunalShowing).isFalse()
 
             // Verify scene changes (without the flag) to communal sets the value to true
-            underTest.onSceneChanged(CommunalScenes.Communal)
+            underTest.changeScene(CommunalScenes.Communal)
             runCurrent()
             assertThat(isCommunalShowing).isTrue()
 
             // Verify scene changes (without the flag) to blank sets the value back to false
-            underTest.onSceneChanged(CommunalScenes.Blank)
+            underTest.changeScene(CommunalScenes.Blank)
             runCurrent()
             assertThat(isCommunalShowing).isFalse()
         }
@@ -704,7 +704,7 @@
             assertThat(isCommunalShowing).isFalse()
 
             // Verify scene changes without the flag doesn't have any impact
-            underTest.onSceneChanged(CommunalScenes.Communal)
+            underTest.changeScene(CommunalScenes.Communal)
             runCurrent()
             assertThat(isCommunalShowing).isFalse()
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt
index 50b8da6..3a23e14 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt
@@ -158,7 +158,7 @@
             kosmos.setCommunalAvailable(true)
             communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_NOT_STARTED)
 
-            communalInteractor.onSceneChanged(CommunalScenes.Blank)
+            communalInteractor.changeScene(CommunalScenes.Blank)
 
             assertThat(tutorialSettingState).isEqualTo(HUB_MODE_TUTORIAL_NOT_STARTED)
         }
@@ -171,7 +171,7 @@
             goToCommunal()
             communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_STARTED)
 
-            communalInteractor.onSceneChanged(CommunalScenes.Blank)
+            communalInteractor.changeScene(CommunalScenes.Blank)
 
             assertThat(tutorialSettingState).isEqualTo(HUB_MODE_TUTORIAL_COMPLETED)
         }
@@ -184,13 +184,13 @@
             goToCommunal()
             communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED)
 
-            communalInteractor.onSceneChanged(CommunalScenes.Blank)
+            communalInteractor.changeScene(CommunalScenes.Blank)
 
             assertThat(tutorialSettingState).isEqualTo(HUB_MODE_TUTORIAL_COMPLETED)
         }
 
     private suspend fun goToCommunal() {
         kosmos.setCommunalAvailable(true)
-        communalInteractor.onSceneChanged(CommunalScenes.Communal)
+        communalInteractor.changeScene(CommunalScenes.Communal)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt
similarity index 78%
rename from packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt
index 8a77ed2..056a401 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractorTest.kt
@@ -30,17 +30,24 @@
  * limitations under the License.
  */
 
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
 package com.android.systemui.keyguard.domain.interactor
 
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
+import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
 import com.android.systemui.power.domain.interactor.powerInteractor
@@ -49,22 +56,33 @@
 import com.google.common.truth.Truth.assertThat
 import junit.framework.Assert.assertFalse
 import junit.framework.Assert.assertTrue
-import kotlin.test.Test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
 import org.junit.runner.RunWith
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
 class KeyguardOcclusionInteractorTest : SysuiTestCase() {
+
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
-    private val underTest = kosmos.keyguardOcclusionInteractor
-    private val powerInteractor = kosmos.powerInteractor
-    private val transitionRepository = kosmos.fakeKeyguardTransitionRepository
+
+    private lateinit var underTest: KeyguardOcclusionInteractor
+    private lateinit var powerInteractor: PowerInteractor
+    private lateinit var transitionRepository: FakeKeyguardTransitionRepository
+
+    @Before
+    fun setUp() {
+        powerInteractor = kosmos.powerInteractor
+        transitionRepository = kosmos.fakeKeyguardTransitionRepository
+        underTest = kosmos.keyguardOcclusionInteractor
+    }
 
     @Test
-    fun testTransitionFromPowerGesture_whileGoingToSleep_isTrue() =
+    fun transitionFromPowerGesture_whileGoingToSleep_isTrue() =
         testScope.runTest {
             powerInteractor.setAwakeForTest()
             transitionRepository.sendTransitionSteps(
@@ -81,7 +99,7 @@
         }
 
     @Test
-    fun testTransitionFromPowerGesture_whileAsleep_isTrue() =
+    fun transitionFromPowerGesture_whileAsleep_isTrue() =
         testScope.runTest {
             powerInteractor.setAwakeForTest()
             transitionRepository.sendTransitionSteps(
@@ -97,7 +115,7 @@
         }
 
     @Test
-    fun testTransitionFromPowerGesture_whileWaking_isFalse() =
+    fun transitionFromPowerGesture_whileWaking_isFalse() =
         testScope.runTest {
             powerInteractor.setAwakeForTest()
             transitionRepository.sendTransitionSteps(
@@ -119,7 +137,7 @@
         }
 
     @Test
-    fun testTransitionFromPowerGesture_whileAwake_isFalse() =
+    fun transitionFromPowerGesture_whileAwake_isFalse() =
         testScope.runTest {
             powerInteractor.setAwakeForTest()
             transitionRepository.sendTransitionSteps(
@@ -140,7 +158,7 @@
         }
 
     @Test
-    fun testShowWhenLockedActivityLaunchedFromPowerGesture_notTrueSecondTime() =
+    fun showWhenLockedActivityLaunchedFromPowerGesture_notTrueSecondTime() =
         testScope.runTest {
             val values by collectValues(underTest.showWhenLockedActivityLaunchedFromPowerGesture)
             powerInteractor.setAsleepForTest()
@@ -187,7 +205,7 @@
         }
 
     @Test
-    fun testShowWhenLockedActivityLaunchedFromPowerGesture_falseIfReturningToGone() =
+    fun showWhenLockedActivityLaunchedFromPowerGesture_falseIfReturningToGone() =
         testScope.runTest {
             val values by collectValues(underTest.showWhenLockedActivityLaunchedFromPowerGesture)
             powerInteractor.setAwakeForTest()
@@ -221,4 +239,23 @@
                     false,
                 )
         }
+
+    @Test
+    @EnableSceneContainer
+    fun occludingActivityWillDismissKeyguard() =
+        testScope.runTest {
+            val occludingActivityWillDismissKeyguard by
+                collectLastValue(underTest.occludingActivityWillDismissKeyguard)
+            assertThat(occludingActivityWillDismissKeyguard).isFalse()
+
+            // Unlock device:
+            kosmos.fakeDeviceEntryRepository.setUnlocked(true)
+            runCurrent()
+            assertThat(occludingActivityWillDismissKeyguard).isTrue()
+
+            // Re-lock device:
+            kosmos.fakeDeviceEntryRepository.setUnlocked(false)
+            runCurrent()
+            assertThat(occludingActivityWillDismissKeyguard).isFalse()
+        }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelTest.kt
index 0cc0c2f..78bdfb3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelTest.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.sysuiStatusBarStateController
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -51,6 +52,7 @@
         }
     private val testScope = kosmos.testScope
     private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+    private val sysuiStatusBarStateController = kosmos.sysuiStatusBarStateController
     private val underTest by lazy { kosmos.alternateBouncerToGoneTransitionViewModel }
 
     @Test
@@ -112,6 +114,21 @@
         }
 
     @Test
+    fun notificationAlpha_leaveShadeOpen() =
+        testScope.runTest {
+            val values by collectValues(underTest.notificationAlpha(ViewStateAccessor()))
+            runCurrent()
+
+            sysuiStatusBarStateController.setLeaveOpenOnKeyguardHide(true)
+
+            keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            keyguardTransitionRepository.sendTransitionStep(step(1f))
+
+            assertThat(values.size).isEqualTo(2)
+            values.forEach { assertThat(it).isEqualTo(1f) }
+        }
+
+    @Test
     fun lockscreenAlpha_zeroInitialAlpha() =
         testScope.runTest {
             // ViewState starts at 0 alpha.
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt
index ad2ae8b..e6b3017 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt
@@ -20,6 +20,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.bouncer.domain.interactor.mockPrimaryBouncerInteractor
+import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
 import com.android.systemui.flags.Flags
 import com.android.systemui.flags.fakeFeatureFlagsClassic
@@ -149,6 +150,38 @@
         }
 
     @Test
+    fun showAllNotifications_isTrue_whenLeaveShadeOpen() =
+        testScope.runTest {
+            val showAllNotifications by
+                collectLastValue(underTest.showAllNotifications(500.milliseconds, PRIMARY_BOUNCER))
+
+            sysuiStatusBarStateController.setLeaveOpenOnKeyguardHide(true)
+
+            keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            keyguardTransitionRepository.sendTransitionStep(step(0.1f))
+
+            assertThat(showAllNotifications).isTrue()
+            keyguardTransitionRepository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+            assertThat(showAllNotifications).isFalse()
+        }
+
+    @Test
+    fun showAllNotifications_isFalse_whenLeaveShadeIsNotOpen() =
+        testScope.runTest {
+            val showAllNotifications by
+                collectLastValue(underTest.showAllNotifications(500.milliseconds, PRIMARY_BOUNCER))
+
+            sysuiStatusBarStateController.setLeaveOpenOnKeyguardHide(false)
+
+            keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            keyguardTransitionRepository.sendTransitionStep(step(0.1f))
+
+            assertThat(showAllNotifications).isFalse()
+            keyguardTransitionRepository.sendTransitionStep(step(1f, TransitionState.FINISHED))
+            assertThat(showAllNotifications).isFalse()
+        }
+
+    @Test
     fun scrimBehindAlpha_doNotLeaveShadeOpen() =
         testScope.runTest {
             val values by collectValues(underTest.scrimAlpha(500.milliseconds, PRIMARY_BOUNCER))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
index 751ac1d..e9a8257 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt
@@ -21,6 +21,7 @@
 import com.android.keyguard.KeyguardClockSwitch
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.biometrics.authController
+import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.Flags
 import com.android.systemui.flags.fakeFeatureFlagsClassic
 import com.android.systemui.keyguard.data.repository.fakeKeyguardClockRepository
@@ -96,7 +97,7 @@
                 shadeRepository.setShadeMode(ShadeMode.Split)
                 kosmos.fakeKeyguardClockRepository.setClockSize(KeyguardClockSwitch.LARGE)
 
-                assertThat(underTest.areNotificationsVisible).isTrue()
+                assertThat(collectLastValue(underTest.areNotificationsVisible).invoke()).isTrue()
             }
         }
     @Test
@@ -104,7 +105,7 @@
         with(kosmos) {
             testScope.runTest {
                 kosmos.fakeKeyguardClockRepository.setClockSize(KeyguardClockSwitch.SMALL)
-                assertThat(underTest.areNotificationsVisible).isTrue()
+                assertThat(collectLastValue(underTest.areNotificationsVisible).invoke()).isTrue()
             }
         }
 
@@ -113,7 +114,7 @@
         with(kosmos) {
             testScope.runTest {
                 kosmos.fakeKeyguardClockRepository.setClockSize(KeyguardClockSwitch.LARGE)
-                assertThat(underTest.areNotificationsVisible).isFalse()
+                assertThat(collectLastValue(underTest.areNotificationsVisible).invoke()).isFalse()
             }
         }
 
@@ -122,7 +123,8 @@
         with(kosmos) {
             testScope.runTest {
                 shadeRepository.setShadeMode(ShadeMode.Split)
-                assertThat(underTest.shouldUseSplitNotificationShade).isTrue()
+                assertThat(collectLastValue(underTest.shouldUseSplitNotificationShade).invoke())
+                    .isTrue()
             }
         }
 
@@ -131,16 +133,8 @@
         with(kosmos) {
             testScope.runTest {
                 shadeRepository.setShadeMode(ShadeMode.Single)
-                assertThat(underTest.shouldUseSplitNotificationShade).isFalse()
-            }
-        }
-
-    @Test
-    fun sceneKey() =
-        with(kosmos) {
-            testScope.runTest {
-                shadeRepository.setShadeMode(ShadeMode.Single)
-                assertThat(underTest.shouldUseSplitNotificationShade).isFalse()
+                assertThat(collectLastValue(underTest.shouldUseSplitNotificationShade).invoke())
+                    .isFalse()
             }
         }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelTest.kt
index 857b9f8..26cb485 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelTest.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.sysuiStatusBarStateController
 import com.android.systemui.testKosmos
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -40,6 +41,7 @@
     private val kosmos = testKosmos()
     private val testScope = kosmos.testScope
     private val repository = kosmos.fakeKeyguardTransitionRepository
+    private val sysuiStatusBarStateController = kosmos.sysuiStatusBarStateController
     private val underTest = kosmos.lockscreenToGoneTransitionViewModel
 
     @Test
@@ -90,6 +92,20 @@
             assertThat(alpha).isEqualTo(0f)
         }
 
+    @Test
+    fun notificationAlpha_leaveShadeOpen() =
+        testScope.runTest {
+            val values by collectValues(underTest.notificationAlpha(ViewStateAccessor()))
+
+            sysuiStatusBarStateController.setLeaveOpenOnKeyguardHide(true)
+
+            repository.sendTransitionStep(step(0f, TransitionState.STARTED))
+            repository.sendTransitionStep(step(1f))
+
+            assertThat(values.size).isEqualTo(2)
+            values.forEach { assertThat(it).isEqualTo(1f) }
+        }
+
     private fun step(
         value: Float,
         state: TransitionState = TransitionState.RUNNING
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index c80835d..efbdb7d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -64,6 +64,7 @@
 import com.android.systemui.qs.footerActionsController
 import com.android.systemui.qs.footerActionsViewModelFactory
 import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter
+import com.android.systemui.scene.domain.interactor.sceneContainerOcclusionInteractor
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.domain.startable.SceneContainerStartable
 import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags
@@ -286,6 +287,7 @@
                 deviceProvisioningInteractor = kosmos.deviceProvisioningInteractor,
                 centralSurfaces = mock(),
                 headsUpInteractor = kosmos.headsUpNotificationInteractor,
+                occlusionInteractor = kosmos.sceneContainerOcclusionInteractor,
             )
         startable.start()
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractorTest.kt
new file mode 100644
index 0000000..c3366ad
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractorTest.kt
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.scene.domain.interactor
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.shared.model.sceneDataSource
+import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class SceneContainerOcclusionInteractorTest : SysuiTestCase() {
+
+    private val kosmos = testKosmos()
+    private val testScope = kosmos.testScope
+    private val keyguardOcclusionInteractor = kosmos.keyguardOcclusionInteractor
+    private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+    private val keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor
+    private val mutableTransitionState =
+        MutableStateFlow<ObservableTransitionState>(
+            ObservableTransitionState.Idle(Scenes.Lockscreen)
+        )
+    private val sceneInteractor =
+        kosmos.sceneInteractor.apply { setTransitionState(mutableTransitionState) }
+    private val sceneDataSource =
+        kosmos.sceneDataSource.apply { changeScene(toScene = Scenes.Lockscreen) }
+
+    private val underTest = kosmos.sceneContainerOcclusionInteractor
+
+    @Test
+    fun invisibleDueToOcclusion() =
+        testScope.runTest {
+            val invisibleDueToOcclusion by collectLastValue(underTest.invisibleDueToOcclusion)
+            val keyguardState by collectLastValue(keyguardTransitionInteractor.currentKeyguardState)
+
+            // Assert that we have the desired preconditions:
+            assertThat(keyguardState).isEqualTo(KeyguardState.LOCKSCREEN)
+            assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Lockscreen)
+            assertThat(sceneInteractor.transitionState.value)
+                .isEqualTo(ObservableTransitionState.Idle(Scenes.Lockscreen))
+            assertWithMessage("Should start unoccluded").that(invisibleDueToOcclusion).isFalse()
+
+            // Actual testing starts here:
+            showOccludingActivity()
+            assertWithMessage("Should become occluded when occluding activity is shown")
+                .that(invisibleDueToOcclusion)
+                .isTrue()
+
+            transitionIntoAod {
+                assertWithMessage("Should become unoccluded when transitioning into AOD")
+                    .that(invisibleDueToOcclusion)
+                    .isFalse()
+            }
+            assertWithMessage("Should stay unoccluded when in AOD")
+                .that(invisibleDueToOcclusion)
+                .isFalse()
+
+            transitionOutOfAod {
+                assertWithMessage("Should remain unoccluded while transitioning away from AOD")
+                    .that(invisibleDueToOcclusion)
+                    .isFalse()
+            }
+            assertWithMessage("Should become occluded now that no longer in AOD")
+                .that(invisibleDueToOcclusion)
+                .isTrue()
+
+            expandShade {
+                assertWithMessage("Should become unoccluded once shade begins to expand")
+                    .that(invisibleDueToOcclusion)
+                    .isFalse()
+            }
+            assertWithMessage("Should be unoccluded when shade is fully expanded")
+                .that(invisibleDueToOcclusion)
+                .isFalse()
+
+            collapseShade {
+                assertWithMessage("Should remain unoccluded while shade is collapsing")
+                    .that(invisibleDueToOcclusion)
+                    .isFalse()
+            }
+            assertWithMessage("Should become occluded now that shade is fully collapsed")
+                .that(invisibleDueToOcclusion)
+                .isTrue()
+
+            hideOccludingActivity()
+            assertWithMessage("Should become unoccluded once the occluding activity is hidden")
+                .that(invisibleDueToOcclusion)
+                .isFalse()
+        }
+
+    /** Simulates the appearance of a show-when-locked `Activity` in the foreground. */
+    private fun TestScope.showOccludingActivity() {
+        keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop(
+            showWhenLockedActivityOnTop = true,
+            taskInfo = mock(),
+        )
+        runCurrent()
+    }
+
+    /** Simulates the disappearance of a show-when-locked `Activity` from the foreground. */
+    private fun TestScope.hideOccludingActivity() {
+        keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop(
+            showWhenLockedActivityOnTop = false,
+        )
+        runCurrent()
+    }
+
+    /** Simulates a user-driven gradual expansion of the shade. */
+    private fun TestScope.expandShade(
+        assertMidTransition: () -> Unit = {},
+    ) {
+        val progress = MutableStateFlow(0f)
+        mutableTransitionState.value =
+            ObservableTransitionState.Transition(
+                fromScene = sceneDataSource.currentScene.value,
+                toScene = Scenes.Shade,
+                progress = progress,
+                isInitiatedByUserInput = true,
+                isUserInputOngoing = flowOf(true),
+            )
+        runCurrent()
+
+        progress.value = 0.5f
+        runCurrent()
+        assertMidTransition()
+
+        progress.value = 1f
+        runCurrent()
+
+        mutableTransitionState.value = ObservableTransitionState.Idle(Scenes.Shade)
+        runCurrent()
+    }
+
+    /** Simulates a user-driven gradual collapse of the shade. */
+    private fun TestScope.collapseShade(
+        assertMidTransition: () -> Unit = {},
+    ) {
+        val progress = MutableStateFlow(0f)
+        mutableTransitionState.value =
+            ObservableTransitionState.Transition(
+                fromScene = Scenes.Shade,
+                toScene = Scenes.Lockscreen,
+                progress = progress,
+                isInitiatedByUserInput = true,
+                isUserInputOngoing = flowOf(true),
+            )
+        runCurrent()
+
+        progress.value = 0.5f
+        runCurrent()
+        assertMidTransition()
+
+        progress.value = 1f
+        runCurrent()
+
+        mutableTransitionState.value = ObservableTransitionState.Idle(Scenes.Lockscreen)
+        runCurrent()
+    }
+
+    /** Simulates a transition into AOD. */
+    private suspend fun TestScope.transitionIntoAod(
+        assertMidTransition: () -> Unit = {},
+    ) {
+        val currentKeyguardState = keyguardTransitionInteractor.getCurrentState()
+        keyguardTransitionRepository.sendTransitionStep(
+            TransitionStep(
+                from = currentKeyguardState,
+                to = KeyguardState.AOD,
+                value = 0f,
+                transitionState = TransitionState.STARTED,
+            )
+        )
+        runCurrent()
+
+        keyguardTransitionRepository.sendTransitionStep(
+            TransitionStep(
+                from = currentKeyguardState,
+                to = KeyguardState.AOD,
+                value = 0.5f,
+                transitionState = TransitionState.RUNNING,
+            )
+        )
+        runCurrent()
+        assertMidTransition()
+
+        keyguardTransitionRepository.sendTransitionStep(
+            TransitionStep(
+                from = currentKeyguardState,
+                to = KeyguardState.AOD,
+                value = 1f,
+                transitionState = TransitionState.FINISHED,
+            )
+        )
+        runCurrent()
+    }
+
+    /** Simulates a transition away from AOD. */
+    private suspend fun TestScope.transitionOutOfAod(
+        assertMidTransition: () -> Unit = {},
+    ) {
+        keyguardTransitionRepository.sendTransitionStep(
+            TransitionStep(
+                from = KeyguardState.AOD,
+                to = KeyguardState.LOCKSCREEN,
+                value = 0f,
+                transitionState = TransitionState.STARTED,
+            )
+        )
+        runCurrent()
+
+        keyguardTransitionRepository.sendTransitionStep(
+            TransitionStep(
+                from = KeyguardState.AOD,
+                to = KeyguardState.LOCKSCREEN,
+                value = 0.5f,
+                transitionState = TransitionState.RUNNING,
+            )
+        )
+        runCurrent()
+        assertMidTransition()
+
+        keyguardTransitionRepository.sendTransitionStep(
+            TransitionStep(
+                from = KeyguardState.AOD,
+                to = KeyguardState.LOCKSCREEN,
+                value = 1f,
+                transitionState = TransitionState.FINISHED,
+            )
+        )
+        runCurrent()
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index 594543b..605e5c0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -46,11 +46,13 @@
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
 import com.android.systemui.power.domain.interactor.PowerInteractorFactory
+import com.android.systemui.scene.domain.interactor.sceneContainerOcclusionInteractor
 import com.android.systemui.scene.domain.interactor.sceneInteractor
 import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags
 import com.android.systemui.scene.shared.model.Scenes
 import com.android.systemui.scene.shared.model.fakeSceneDataSource
 import com.android.systemui.statusbar.NotificationShadeWindowController
+import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
 import com.android.systemui.statusbar.notification.data.repository.FakeHeadsUpRowRepository
 import com.android.systemui.statusbar.notification.data.repository.HeadsUpRowRepository
 import com.android.systemui.statusbar.notification.stack.data.repository.headsUpNotificationRepository
@@ -128,6 +130,7 @@
                 deviceProvisioningInteractor = kosmos.deviceProvisioningInteractor,
                 centralSurfaces = centralSurfaces,
                 headsUpInteractor = kosmos.headsUpNotificationInteractor,
+                occlusionInteractor = kosmos.sceneContainerOcclusionInteractor,
             )
     }
 
@@ -204,6 +207,28 @@
         }
 
     @Test
+    fun hydrateVisibility_basedOnOcclusion() =
+        testScope.runTest {
+            val isVisible by collectLastValue(sceneInteractor.isVisible)
+            prepareState(
+                isDeviceUnlocked = true,
+                initialSceneKey = Scenes.Lockscreen,
+            )
+
+            underTest.start()
+            assertThat(isVisible).isTrue()
+
+            kosmos.keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop(
+                true,
+                mock()
+            )
+            assertThat(isVisible).isFalse()
+
+            kosmos.keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop(false)
+            assertThat(isVisible).isTrue()
+        }
+
+    @Test
     fun startsInLockscreenScene() =
         testScope.runTest {
             val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt
index 5358a6d..fa79e7f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt
@@ -201,11 +201,38 @@
     }
 
     @Test
-    fun alarmStream_isNotMutable() {
+    fun streamNotAffectedByMute_isNotMutable() {
         with(kosmos) {
-            val isMutable = underTest.isMutable(AudioStream(AudioManager.STREAM_ALARM))
+            testScope.runTest {
+                audioRepository.setIsAffectedByMute(audioStream, false)
+                val isMutable = underTest.isAffectedByMute(audioStream)
 
-            assertThat(isMutable).isFalse()
+                assertThat(isMutable).isFalse()
+            }
+        }
+    }
+
+    @Test
+    fun muteRingerStream_ringerMode_vibrate() {
+        with(kosmos) {
+            testScope.runTest {
+                val ringerMode by collectLastValue(audioRepository.ringerMode)
+                underTest.setMuted(AudioStream(AudioManager.STREAM_RING), true)
+
+                assertThat(ringerMode).isEqualTo(RingerMode(AudioManager.RINGER_MODE_VIBRATE))
+            }
+        }
+    }
+
+    @Test
+    fun unMuteRingerStream_ringerMode_normal() {
+        with(kosmos) {
+            testScope.runTest {
+                val ringerMode by collectLastValue(audioRepository.ringerMode)
+                underTest.setMuted(AudioStream(AudioManager.STREAM_RING), false)
+
+                assertThat(ringerMode).isEqualTo(RingerMode(AudioManager.RINGER_MODE_NORMAL))
+            }
         }
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModelTest.kt
index 8e92557..2cc1ad3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModelTest.kt
@@ -84,8 +84,17 @@
 
                 runCurrent()
 
-                verify(activityStarter).startActivity(capture(intentCaptor), eq(true),
-                        capture(activityStartedCaptor))
+                verify(activityStarter)
+                    .startActivityDismissingKeyguard(
+                        /* intent = */ capture(intentCaptor),
+                        /* onlyProvisioned = */ eq(false),
+                        /* dismissShade = */ eq(true),
+                        /* disallowEnterPictureInPictureWhileLaunching = */ eq(false),
+                        /* callback = */ capture(activityStartedCaptor),
+                        /* flags = */ eq(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT),
+                        /* animationController = */ eq(null),
+                        /* userHandle = */ eq(null),
+                    )
                 assertThat(intentCaptor.value.action).isEqualTo(Settings.ACTION_SOUND_SETTINGS)
 
                 activityStartedCaptor.value.onActivityStarted(ActivityManager.START_SUCCESS)
diff --git a/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceDataPlugin.java b/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceDataPlugin.java
index c99cb39..83658d3 100644
--- a/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceDataPlugin.java
+++ b/packages/SystemUI/plugin/bcsmartspace/src/com/android/systemui/plugins/BcSmartspaceDataPlugin.java
@@ -129,6 +129,11 @@
         void setDozeAmount(float amount);
 
         /**
+         * Set if the screen is on.
+         */
+        default void setScreenOn(boolean screenOn) {}
+
+        /**
          * Set if dozing is true or false
          */
         default void setDozing(boolean dozing) {}
diff --git a/packages/SystemUI/res/drawable/accessibility_fullscreen_magnification_border_background.xml b/packages/SystemUI/res/drawable/accessibility_fullscreen_magnification_border_background.xml
new file mode 100644
index 0000000..edfbe7b
--- /dev/null
+++ b/packages/SystemUI/res/drawable/accessibility_fullscreen_magnification_border_background.xml
@@ -0,0 +1,25 @@
+<?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.
+  -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+       android:shape="rectangle">
+    <corners android:radius="@*android:dimen/rounded_corner_radius" />
+    <!-- Since the device corners are not perfectly rounded, we create the stroke with offset
+         to fill up the space between border and device corner -->
+    <stroke
+        android:color="@color/magnification_border_color"
+        android:width="@dimen/magnifier_border_width_fullscreen_with_offset"/>
+</shape>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/qs_hearing_devices_icon.xml b/packages/SystemUI/res/drawable/qs_hearing_devices_icon.xml
new file mode 100644
index 0000000..c1573a3
--- /dev/null
+++ b/packages/SystemUI/res/drawable/qs_hearing_devices_icon.xml
@@ -0,0 +1,25 @@
+<!--
+    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.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="18dp"
+    android:height="18dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="#ffffff"
+        android:pathData="M17,20c-0.29,0 -0.56,-0.06 -0.76,-0.15 -0.71,-0.37 -1.21,-0.88 -1.71,-2.38 -0.51,-1.56 -1.47,-2.29 -2.39,-3 -0.79,-0.61 -1.61,-1.24 -2.32,-2.53C9.29,10.98 9,9.93 9,9c0,-2.8 2.2,-5 5,-5s5,2.2 5,5h2c0,-3.93 -3.07,-7 -7,-7S7,5.07 7,9c0,1.26 0.38,2.65 1.07,3.9 0.91,1.65 1.98,2.48 2.85,3.15 0.81,0.62 1.39,1.07 1.71,2.05 0.6,1.82 1.37,2.84 2.73,3.55 0.51,0.23 1.07,0.35 1.64,0.35 2.21,0 4,-1.79 4,-4h-2c0,1.1 -0.9,2 -2,2zM7.64,2.64L6.22,1.22C4.23,3.21 3,5.96 3,9s1.23,5.79 3.22,7.78l1.41,-1.41C6.01,13.74 5,11.49 5,9s1.01,-4.74 2.64,-6.36zM11.5,9c0,1.38 1.12,2.5 2.5,2.5s2.5,-1.12 2.5,-2.5 -1.12,-2.5 -2.5,-2.5 -2.5,1.12 -2.5,2.5z"/>
+</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/fullscreen_magnification_border.xml b/packages/SystemUI/res/layout/fullscreen_magnification_border.xml
new file mode 100644
index 0000000..5f738c0
--- /dev/null
+++ b/packages/SystemUI/res/layout/fullscreen_magnification_border.xml
@@ -0,0 +1,22 @@
+<?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.
+  -->
+<View xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/magnification_fullscreen_border"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:focusable="false"
+    android:background="@drawable/accessibility_fullscreen_magnification_border_background"/>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/global_actions_grid_lite.xml b/packages/SystemUI/res/layout/global_actions_grid_lite.xml
index a64c9ae..9a7108a 100644
--- a/packages/SystemUI/res/layout/global_actions_grid_lite.xml
+++ b/packages/SystemUI/res/layout/global_actions_grid_lite.xml
@@ -19,6 +19,7 @@
     android:id="@+id/global_actions_container"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
+    android:clipChildren="false"
     android:gravity="center"
     android:layout_gravity="center">
   <com.android.systemui.globalactions.GlobalActionsLayoutLite
diff --git a/packages/SystemUI/res/layout/screenshot_shelf.xml b/packages/SystemUI/res/layout/screenshot_shelf.xml
index ef1a21f..c988b4a 100644
--- a/packages/SystemUI/res/layout/screenshot_shelf.xml
+++ b/packages/SystemUI/res/layout/screenshot_shelf.xml
@@ -28,7 +28,7 @@
         android:elevation="4dp"
         android:background="@drawable/action_chip_container_background"
         android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal"
-        android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom"
+        android:layout_marginBottom="@dimen/screenshot_shelf_vertical_margin"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toTopOf="@+id/actions_container"
         app:layout_constraintEnd_toEndOf="@+id/actions_container"
@@ -38,14 +38,14 @@
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_marginEnd="@dimen/overlay_action_container_margin_horizontal"
-        android:paddingEnd="@dimen/overlay_action_container_padding_end"
+        android:paddingHorizontal="@dimen/overlay_action_container_padding_end"
         android:paddingVertical="@dimen/overlay_action_container_padding_vertical"
         android:elevation="4dp"
         android:scrollbars="none"
         app:layout_constraintHorizontal_bias="0"
         app:layout_constraintWidth_percent="1.0"
         app:layout_constraintWidth_max="wrap"
-        app:layout_constraintStart_toEndOf="@+id/screenshot_preview_border"
+        app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintBottom_toBottomOf="@id/actions_container_background">
         <LinearLayout
@@ -65,16 +65,16 @@
         android:id="@+id/screenshot_preview_border"
         android:layout_width="0dp"
         android:layout_height="0dp"
-        android:layout_marginStart="16dp"
+        android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal"
         android:layout_marginTop="@dimen/overlay_border_width_neg"
         android:layout_marginEnd="@dimen/overlay_border_width_neg"
-        android:layout_marginBottom="14dp"
+        android:layout_marginBottom="@dimen/screenshot_shelf_vertical_margin"
         android:elevation="8dp"
         android:background="@drawable/overlay_border"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toTopOf="@id/screenshot_preview"
         app:layout_constraintEnd_toEndOf="@id/screenshot_preview"
-        app:layout_constraintBottom_toBottomOf="parent"/>
+        app:layout_constraintBottom_toTopOf="@id/actions_container"/>
     <ImageView
         android:id="@+id/screenshot_preview"
         android:layout_width="@dimen/overlay_x_scale"
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 35f6a08..a6f6d4d 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -101,7 +101,7 @@
 
     <!-- Tiles native to System UI. Order should match "quick_settings_tiles_default" -->
     <string name="quick_settings_tiles_stock" translatable="false">
-        internet,bt,flashlight,dnd,alarm,airplane,controls,wallet,rotation,battery,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue
+        internet,bt,flashlight,dnd,alarm,airplane,controls,wallet,rotation,battery,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices
     </string>
 
     <!-- The tiles to display in QuickSettings -->
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index e004ee9..d2efccd 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -448,6 +448,7 @@
     <dimen name="overlay_action_container_padding_end">8dp</dimen>
     <dimen name="overlay_dismiss_button_tappable_size">48dp</dimen>
     <dimen name="overlay_dismiss_button_margin">8dp</dimen>
+    <dimen name="screenshot_shelf_vertical_margin">8dp</dimen>
     <!-- must be kept aligned with overlay_border_width_neg, below;
          overlay_border_width = overlay_border_width_neg * -1 -->
     <dimen name="overlay_border_width">4dp</dimen>
@@ -1266,6 +1267,8 @@
     <dimen name="magnifier_corner_radius">28dp</dimen>
     <dimen name="magnifier_edit_corner_radius">16dp</dimen>
     <dimen name="magnifier_edit_outer_corner_radius">18dp</dimen>
+    <dimen name="magnifier_border_width_fullscreen_with_offset">12dp</dimen>
+    <dimen name="magnifier_border_width_fullscreen">6dp</dimen>
     <dimen name="magnifier_border_width">8dp</dimen>
     <dimen name="magnifier_stroke_width">2dp</dimen>
     <dimen name="magnifier_edit_dash_gap">20dp</dimen>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 2195849..b0d98e7 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -899,6 +899,9 @@
     <!-- QuickSettings: Contrast tile description: high [CHAR LIMIT=NONE] -->
     <string name="quick_settings_contrast_high">High</string>
 
+    <!-- QuickSettings: Hearing devices [CHAR LIMIT=NONE] -->
+    <string name="quick_settings_hearing_devices_label">Hearing devices</string>
+
     <!--- Title of dialog triggered if the microphone is disabled but an app tried to access it. [CHAR LIMIT=150] -->
     <string name="sensor_privacy_start_use_mic_dialog_title">Unblock device microphone?</string>
     <!--- Title of dialog triggered if the camera is disabled but an app tried to access it. [CHAR LIMIT=150] -->
diff --git a/packages/SystemUI/res/values/tiles_states_strings.xml b/packages/SystemUI/res/values/tiles_states_strings.xml
index 9036a35..ad09b46 100644
--- a/packages/SystemUI/res/values/tiles_states_strings.xml
+++ b/packages/SystemUI/res/values/tiles_states_strings.xml
@@ -338,4 +338,14 @@
         <item>Off</item>
         <item>On</item>
     </string-array>
+
+    <!-- State names for hearing devices tile: unavailable, off, on.
+         This subtitle is shown when the tile is in that particular state but does not set its own
+         subtitle, so some of these may never appear on screen. They should still be translated as
+         if they could appear. [CHAR LIMIT=32] -->
+    <string-array name="tile_states_hearing_devices">
+        <item>Unavailable</item>
+        <item>Off</item>
+        <item>On</item>
+    </string-array>
 </resources>
\ No newline at end of file
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/Utilities.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/Utilities.java
index 751a3f8..68d2eb3 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/Utilities.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/utilities/Utilities.java
@@ -22,6 +22,7 @@
 
 import android.annotation.TargetApi;
 import android.content.Context;
+import android.content.res.Resources;
 import android.graphics.Color;
 import android.graphics.Rect;
 import android.inputmethodservice.InputMethodService;
@@ -138,11 +139,15 @@
     /** @return whether or not {@param context} represents that of a large screen device or not */
     @TargetApi(Build.VERSION_CODES.R)
     public static boolean isLargeScreen(Context context) {
-        final WindowManager windowManager = context.getSystemService(WindowManager.class);
+        return isLargeScreen(context.getSystemService(WindowManager.class), context.getResources());
+    }
+
+    /** @return whether or not {@param context} represents that of a large screen device or not */
+    public static boolean isLargeScreen(WindowManager windowManager, Resources resources) {
         final Rect bounds = windowManager.getCurrentWindowMetrics().getBounds();
 
         float smallestWidth = dpiFromPx(Math.min(bounds.width(), bounds.height()),
-                context.getResources().getConfiguration().densityDpi);
+                resources.getConfiguration().densityDpi);
         return smallestWidth >= TABLET_MIN_DPS;
     }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
index 8f1a5f7..985f6c8 100644
--- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java
@@ -454,6 +454,7 @@
         final float scaleFactor = mAuthController.getScaleFactor();
         final int scaledPadding = (int) (mDefaultPaddingPx * scaleFactor);
         if (KeyguardBottomAreaRefactor.isEnabled() || MigrateClocksToBlueprint.isEnabled()) {
+            // positioning in this case is handled by [DefaultDeviceEntrySection]
             mView.getLockIcon().setPadding(scaledPadding, scaledPadding, scaledPadding,
                     scaledPadding);
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
index 1a9b01f..7e94804 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
@@ -29,8 +29,8 @@
 import com.android.systemui.util.InitializationChecker;
 import com.android.wm.shell.dagger.WMShellConcurrencyModule;
 import com.android.wm.shell.keyguard.KeyguardTransitions;
+import com.android.wm.shell.shared.ShellTransitions;
 import com.android.wm.shell.sysui.ShellInterface;
-import com.android.wm.shell.transition.ShellTransitions;
 
 import java.util.Optional;
 import java.util.concurrent.ExecutionException;
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java
new file mode 100644
index 0000000..af8149f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java
@@ -0,0 +1,140 @@
+/*
+ * 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.accessibility;
+
+import static android.view.WindowManager.LayoutParams;
+
+import android.annotation.UiContext;
+import android.content.Context;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.view.AttachedSurfaceControl;
+import android.view.LayoutInflater;
+import android.view.SurfaceControl;
+import android.view.SurfaceControlViewHost;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
+
+import androidx.annotation.UiThread;
+
+import com.android.systemui.res.R;
+
+import java.util.function.Supplier;
+
+class FullscreenMagnificationController {
+
+    private final Context mContext;
+    private final AccessibilityManager mAccessibilityManager;
+    private final WindowManager mWindowManager;
+    private Supplier<SurfaceControlViewHost> mScvhSupplier;
+    private SurfaceControlViewHost mSurfaceControlViewHost;
+    private Rect mWindowBounds;
+    private SurfaceControl.Transaction mTransaction;
+    private View mFullscreenBorder = null;
+    private int mBorderOffset;
+    private final int mDisplayId;
+    private static final Region sEmptyRegion = new Region();
+
+    FullscreenMagnificationController(
+            @UiContext Context context,
+            AccessibilityManager accessibilityManager,
+            WindowManager windowManager,
+            Supplier<SurfaceControlViewHost> scvhSupplier) {
+        mContext = context;
+        mAccessibilityManager = accessibilityManager;
+        mWindowManager = windowManager;
+        mWindowBounds = mWindowManager.getCurrentWindowMetrics().getBounds();
+        mTransaction = new SurfaceControl.Transaction();
+        mScvhSupplier = scvhSupplier;
+        mBorderOffset = mContext.getResources().getDimensionPixelSize(
+                R.dimen.magnifier_border_width_fullscreen_with_offset)
+                - mContext.getResources().getDimensionPixelSize(
+                R.dimen.magnifier_border_width_fullscreen);
+        mDisplayId = mContext.getDisplayId();
+    }
+
+    @UiThread
+    void onFullscreenMagnificationActivationChanged(boolean activated) {
+        if (activated) {
+            createFullscreenMagnificationBorder();
+        } else {
+            removeFullscreenMagnificationBorder();
+        }
+    }
+
+    @UiThread
+    private void removeFullscreenMagnificationBorder() {
+        if (mSurfaceControlViewHost != null) {
+            mSurfaceControlViewHost.release();
+            mSurfaceControlViewHost = null;
+        }
+
+        if (mFullscreenBorder != null) {
+            mFullscreenBorder = null;
+        }
+    }
+
+    /**
+     * Since the device corners are not perfectly rounded, we would like to create a thick stroke,
+     * and set negative offset to the border view to fill up the spaces between the border and the
+     * device corners.
+     */
+    @UiThread
+    private void createFullscreenMagnificationBorder() {
+        mFullscreenBorder = LayoutInflater.from(mContext)
+                .inflate(R.layout.fullscreen_magnification_border, null);
+        mSurfaceControlViewHost = mScvhSupplier.get();
+        mSurfaceControlViewHost.setView(mFullscreenBorder, getBorderLayoutParams());
+
+        SurfaceControl surfaceControl = mSurfaceControlViewHost
+                .getSurfacePackage().getSurfaceControl();
+
+        mTransaction
+                .setPosition(surfaceControl, -mBorderOffset, -mBorderOffset)
+                .setLayer(surfaceControl, Integer.MAX_VALUE)
+                .show(surfaceControl)
+                .apply();
+
+        mAccessibilityManager.attachAccessibilityOverlayToDisplay(mDisplayId, surfaceControl);
+
+        applyTouchableRegion();
+    }
+
+    private LayoutParams getBorderLayoutParams() {
+        LayoutParams params =  new LayoutParams(
+                mWindowBounds.width() + 2 * mBorderOffset,
+                mWindowBounds.height() + 2 * mBorderOffset,
+                LayoutParams.TYPE_ACCESSIBILITY_OVERLAY,
+                LayoutParams.FLAG_NOT_TOUCH_MODAL | LayoutParams.FLAG_NOT_FOCUSABLE,
+                PixelFormat.TRANSPARENT);
+        params.setTrustedOverlay();
+        return params;
+    }
+
+    private void applyTouchableRegion() {
+        // Sometimes this can get posted and run after deleteWindowMagnification() is called.
+        if (mFullscreenBorder == null) return;
+
+        AttachedSurfaceControl surfaceControl = mSurfaceControlViewHost.getRootSurfaceControl();
+
+        // The touchable region of the mFullscreenBorder will be empty since we are going to allow
+        // all touch events to go through this view.
+        surfaceControl.setTouchableRegion(sEmptyRegion);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java b/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java
index 88fa2de..70165f3 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java
@@ -34,6 +34,7 @@
 import android.view.Display;
 import android.view.SurfaceControl;
 import android.view.SurfaceControlViewHost;
+import android.view.WindowManager;
 import android.view.WindowManagerGlobal;
 import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.IMagnificationConnection;
@@ -134,6 +135,37 @@
     @VisibleForTesting
     DisplayIdIndexSupplier<WindowMagnificationController> mWindowMagnificationControllerSupplier;
 
+    private static class FullscreenMagnificationControllerSupplier extends
+            DisplayIdIndexSupplier<FullscreenMagnificationController> {
+
+        private final Context mContext;
+
+        FullscreenMagnificationControllerSupplier(Context context, Handler handler,
+                DisplayManager displayManager, SysUiState sysUiState,
+                SecureSettings secureSettings) {
+            super(displayManager);
+            mContext = context;
+        }
+
+        @Override
+        protected FullscreenMagnificationController createInstance(Display display) {
+            final Context windowContext = mContext.createWindowContext(display,
+                    TYPE_ACCESSIBILITY_OVERLAY, /* options */ null);
+            Supplier<SurfaceControlViewHost> scvhSupplier = () -> new SurfaceControlViewHost(
+                    mContext, mContext.getDisplay(), new InputTransferToken(), TAG);
+            windowContext.setTheme(com.android.systemui.res.R.style.Theme_SystemUI);
+            return new FullscreenMagnificationController(
+                    windowContext,
+                    windowContext.getSystemService(AccessibilityManager.class),
+                    windowContext.getSystemService(WindowManager.class),
+                    scvhSupplier);
+        }
+    }
+
+    @VisibleForTesting
+    DisplayIdIndexSupplier<FullscreenMagnificationController>
+            mFullscreenMagnificationControllerSupplier;
+
     private static class SettingsSupplier extends
             DisplayIdIndexSupplier<MagnificationSettingsController> {
 
@@ -185,6 +217,8 @@
         mWindowMagnificationControllerSupplier = new WindowMagnificationControllerSupplier(context,
                 mHandler, mWindowMagnifierCallback,
                 displayManager, sysUiState, secureSettings);
+        mFullscreenMagnificationControllerSupplier = new FullscreenMagnificationControllerSupplier(
+                context, mHandler, displayManager, sysUiState, secureSettings);
         mMagnificationSettingsSupplier = new SettingsSupplier(context,
                 mMagnificationSettingsControllerCallback, displayManager, secureSettings);
 
@@ -273,8 +307,13 @@
         }
     }
 
+    @MainThread
     void onFullscreenMagnificationActivationChanged(int displayId, boolean activated) {
-        // Do nothing
+        final FullscreenMagnificationController fullscreenMagnificationController =
+                mFullscreenMagnificationControllerSupplier.get(displayId);
+        if (fullscreenMagnificationController != null) {
+            fullscreenMagnificationController.onFullscreenMagnificationActivationChanged(activated);
+        }
     }
 
     @MainThread
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java
index 35fe6b1..c464c82 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuMessageView.java
@@ -22,7 +22,6 @@
 import android.annotation.IntDef;
 import android.content.ComponentCallbacks;
 import android.content.Context;
-import android.content.res.ColorStateList;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Rect;
@@ -37,7 +36,6 @@
 
 import androidx.annotation.NonNull;
 
-import com.android.settingslib.Utils;
 import com.android.systemui.res.R;
 
 import java.lang.annotation.Retention;
@@ -171,9 +169,9 @@
         mTextView.setTextColor(textColor);
         mTextView.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL);
 
-        final ColorStateList colorAccent = Utils.getColorAccent(getContext());
         mUndoButton.setText(res.getString(R.string.accessibility_floating_button_undo));
         mUndoButton.setTextSize(COMPLEX_UNIT_PX, textSize);
-        mUndoButton.setTextColor(colorAccent);
+        mUndoButton.setTextColor(textColor);
+        mUndoButton.setAllCaps(true);
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt b/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt
index 4047623..7cb028a 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/qs/QSAccessibilityModule.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.qs.tiles.ColorInversionTile
 import com.android.systemui.qs.tiles.DreamTile
 import com.android.systemui.qs.tiles.FontScalingTile
+import com.android.systemui.qs.tiles.HearingDevicesTile
 import com.android.systemui.qs.tiles.NightDisplayTile
 import com.android.systemui.qs.tiles.OneHandedModeTile
 import com.android.systemui.qs.tiles.ReduceBrightColorsTile
@@ -94,6 +95,12 @@
     @StringKey(FontScalingTile.TILE_SPEC)
     fun bindFontScalingTile(fontScalingTile: FontScalingTile): QSTileImpl<*>
 
+    /** Inject HearingDevicesTile into tileMap in QSModule */
+    @Binds
+    @IntoMap
+    @StringKey(HearingDevicesTile.TILE_SPEC)
+    fun bindHearingDevicesTile(hearingDevicesTile: HearingDevicesTile): QSTileImpl<*>
+
     companion object {
         const val COLOR_CORRECTION_TILE_SPEC = "color_correction"
         const val COLOR_INVERSION_TILE_SPEC = "inversion"
diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
index ef686f9..4d328d6 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
@@ -72,7 +72,7 @@
             .filterNotNull()
             // TODO(b/322787129): Also set a custom transition animation here to avoid the regular
             // slide-in animation when setting the scene programmatically
-            .onEach { nextScene -> communalInteractor.onSceneChanged(nextScene) }
+            .onEach { nextScene -> communalInteractor.changeScene(nextScene) }
             .launchIn(applicationScope)
 
         // TODO(b/322787129): re-enable once custom animations are in place
@@ -129,7 +129,7 @@
                 .sample(keyguardInteractor.isDreaming, ::Pair)
                 .collect { (shouldTimeout, isDreaming) ->
                     if (isDreaming && shouldTimeout) {
-                        communalInteractor.onSceneChanged(CommunalScenes.Blank)
+                        communalInteractor.changeScene(CommunalScenes.Blank)
                     }
                 }
         }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/Communal.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/Communal.kt
new file mode 100644
index 0000000..5e41a1b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/Communal.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.communal.dagger
+
+import javax.inject.Qualifier
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Communal
diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
index 82d9437..72dcb26 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt
@@ -23,11 +23,19 @@
 import com.android.systemui.communal.data.repository.CommunalSettingsRepositoryModule
 import com.android.systemui.communal.data.repository.CommunalTutorialRepositoryModule
 import com.android.systemui.communal.data.repository.CommunalWidgetRepositoryModule
+import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.communal.widgets.CommunalWidgetModule
 import com.android.systemui.communal.widgets.EditWidgetsActivityStarter
 import com.android.systemui.communal.widgets.EditWidgetsActivityStarterImpl
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.scene.shared.model.SceneContainerConfig
+import com.android.systemui.scene.shared.model.SceneDataSource
+import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
 import dagger.Binds
 import dagger.Module
+import dagger.Provides
+import kotlinx.coroutines.CoroutineScope
 
 @Module(
     includes =
@@ -47,4 +55,24 @@
     fun bindEditWidgetsActivityStarter(
         starter: EditWidgetsActivityStarterImpl
     ): EditWidgetsActivityStarter
+
+    @Binds
+    @Communal
+    fun bindCommunalSceneDataSource(@Communal delegator: SceneDataSourceDelegator): SceneDataSource
+
+    companion object {
+        @Provides
+        @Communal
+        @SysUISingleton
+        fun providesCommunalSceneDataSourceDelegator(
+            @Application applicationScope: CoroutineScope
+        ): SceneDataSourceDelegator {
+            val config =
+                SceneContainerConfig(
+                    sceneKeys = listOf(CommunalScenes.Blank, CommunalScenes.Communal),
+                    initialSceneKey = CommunalScenes.Blank
+                )
+            return SceneDataSourceDelegator(applicationScope, config)
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt
index 201ce83..8bfd8d9 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt
@@ -18,11 +18,12 @@
 
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.TransitionKey
+import com.android.systemui.communal.dagger.Communal
 import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.scene.data.repository.SceneContainerRepository
-import com.android.systemui.scene.shared.flag.SceneContainerFlags
+import com.android.systemui.scene.shared.model.SceneDataSource
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -30,7 +31,6 @@
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.stateIn
@@ -38,16 +38,15 @@
 /** Encapsulates the state of communal mode. */
 interface CommunalRepository {
     /**
-     * Target scene as requested by the underlying [SceneTransitionLayout] or through
-     * [setDesiredScene].
+     * Target scene as requested by the underlying [SceneTransitionLayout] or through [changeScene].
      */
-    val desiredScene: StateFlow<SceneKey>
+    val currentScene: StateFlow<SceneKey>
 
     /** Exposes the transition state of the communal [SceneTransitionLayout]. */
     val transitionState: StateFlow<ObservableTransitionState>
 
     /** Updates the requested scene. */
-    fun setDesiredScene(desiredScene: SceneKey)
+    fun changeScene(toScene: SceneKey, transitionKey: TransitionKey? = null)
 
     /**
      * Updates the transition state of the hub [SceneTransitionLayout].
@@ -63,12 +62,10 @@
 @Inject
 constructor(
     @Background backgroundScope: CoroutineScope,
-    sceneContainerFlags: SceneContainerFlags,
-    sceneContainerRepository: SceneContainerRepository,
+    @Communal private val sceneDataSource: SceneDataSource,
 ) : CommunalRepository {
 
-    private val _desiredScene: MutableStateFlow<SceneKey> = MutableStateFlow(CommunalScenes.Default)
-    override val desiredScene: StateFlow<SceneKey> = _desiredScene.asStateFlow()
+    override val currentScene: StateFlow<SceneKey> = sceneDataSource.currentScene
 
     private val defaultTransitionState = ObservableTransitionState.Idle(CommunalScenes.Default)
     private val _transitionState = MutableStateFlow<Flow<ObservableTransitionState>?>(null)
@@ -81,8 +78,8 @@
                 initialValue = defaultTransitionState,
             )
 
-    override fun setDesiredScene(desiredScene: SceneKey) {
-        _desiredScene.value = desiredScene
+    override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) {
+        sceneDataSource.changeScene(toScene, transitionKey)
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
index ada984d..86b254b 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
@@ -25,6 +25,7 @@
 import android.provider.Settings
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.TransitionKey
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.communal.data.repository.CommunalMediaRepository
 import com.android.systemui.communal.data.repository.CommunalPrefsRepository
@@ -142,13 +143,12 @@
             )
 
     /**
-     * Target scene as requested by the underlying [SceneTransitionLayout] or through
-     * [onSceneChanged].
+     * Target scene as requested by the underlying [SceneTransitionLayout] or through [changeScene].
      *
      * If [isCommunalAvailable] is false, will return [CommunalScenes.Blank]
      */
     val desiredScene: Flow<SceneKey> =
-        communalRepository.desiredScene.combine(isCommunalAvailable) { scene, available ->
+        communalRepository.currentScene.combine(isCommunalAvailable) { scene, available ->
             if (available) scene else CommunalScenes.Blank
         }
 
@@ -254,9 +254,12 @@
             !(it is ObservableTransitionState.Idle && it.scene == CommunalScenes.Blank)
         }
 
-    /** Callback received whenever the [SceneTransitionLayout] finishes a scene transition. */
-    fun onSceneChanged(newScene: SceneKey) {
-        communalRepository.setDesiredScene(newScene)
+    /**
+     * Asks for an asynchronous scene witch to [newScene], which will use the corresponding
+     * installed transition or the one specified by [transitionKey], if provided.
+     */
+    fun changeScene(newScene: SceneKey, transitionKey: TransitionKey? = null) {
+        communalRepository.changeScene(newScene, transitionKey)
     }
 
     fun setEditModeOpen(isOpen: Boolean) {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
index 531f1987..095222a 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt
@@ -49,8 +49,8 @@
         communalInteractor.signalUserInteraction()
     }
 
-    fun onSceneChanged(scene: SceneKey) {
-        communalInteractor.onSceneChanged(scene)
+    fun changeScene(scene: SceneKey) {
+        communalInteractor.changeScene(scene)
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
index 902133d..5f4b394 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
@@ -31,7 +31,6 @@
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.ui.Modifier
 import androidx.lifecycle.lifecycleScope
-import com.android.app.tracing.coroutines.launch
 import com.android.compose.theme.LocalAndroidColorScheme
 import com.android.compose.theme.PlatformTheme
 import com.android.internal.logging.UiEventLogger
@@ -150,7 +149,7 @@
 
     private fun onEditDone() {
         try {
-            communalViewModel.onSceneChanged(CommunalScenes.Communal)
+            communalViewModel.changeScene(CommunalScenes.Communal)
             checkNotNull(windowManagerService).lockNow(/* options */ null)
             finish()
         } catch (e: RemoteException) {
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
index 3b0c281..e104166 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
@@ -36,11 +36,11 @@
 import com.android.wm.shell.onehanded.OneHanded;
 import com.android.wm.shell.pip.Pip;
 import com.android.wm.shell.recents.RecentTasks;
+import com.android.wm.shell.shared.ShellTransitions;
 import com.android.wm.shell.splitscreen.SplitScreen;
 import com.android.wm.shell.startingsurface.StartingSurface;
 import com.android.wm.shell.sysui.ShellInterface;
 import com.android.wm.shell.taskview.TaskViewFactory;
-import com.android.wm.shell.transition.ShellTransitions;
 
 import dagger.BindsInstance;
 import dagger.Subcomponent;
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 19af371..1ed4b50 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -49,6 +49,7 @@
 import com.android.systemui.communal.dagger.CommunalModule;
 import com.android.systemui.complication.dagger.ComplicationComponent;
 import com.android.systemui.controls.dagger.ControlsModule;
+import com.android.systemui.dagger.qualifiers.Application;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dagger.qualifiers.SystemUser;
 import com.android.systemui.dagger.qualifiers.UiBackground;
@@ -89,6 +90,7 @@
 import com.android.systemui.recents.Recents;
 import com.android.systemui.recordissue.RecordIssueModule;
 import com.android.systemui.retail.dagger.RetailModeModule;
+import com.android.systemui.scene.shared.model.SceneContainerConfig;
 import com.android.systemui.scene.shared.model.SceneDataSource;
 import com.android.systemui.scene.shared.model.SceneDataSourceDelegator;
 import com.android.systemui.scene.ui.view.WindowRootViewComponent;
@@ -165,6 +167,8 @@
 
 import javax.inject.Named;
 
+import kotlinx.coroutines.CoroutineScope;
+
 /**
  * A dagger module for injecting components of System UI that are required by System UI.
  *
@@ -402,6 +406,13 @@
     @ClassKey(SystemUISecondaryUserService.class)
     abstract Service bindsSystemUISecondaryUserService(SystemUISecondaryUserService service);
 
+    @Provides
+    @SysUISingleton
+    static SceneDataSourceDelegator providesSceneDataSourceDelegator(
+            @Application CoroutineScope applicationScope, SceneContainerConfig config) {
+        return new SceneDataSourceDelegator(applicationScope, config);
+    }
+
     @Binds
     abstract SceneDataSource bindSceneDataSource(SceneDataSourceDelegator delegator);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java
index dfec771..e04a0e5 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java
@@ -23,7 +23,6 @@
 import com.android.systemui.SystemUIInitializer;
 import com.android.wm.shell.back.BackAnimation;
 import com.android.wm.shell.bubbles.Bubbles;
-import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.dagger.WMShellModule;
 import com.android.wm.shell.dagger.WMSingleton;
 import com.android.wm.shell.desktopmode.DesktopMode;
@@ -32,11 +31,12 @@
 import com.android.wm.shell.onehanded.OneHanded;
 import com.android.wm.shell.pip.Pip;
 import com.android.wm.shell.recents.RecentTasks;
+import com.android.wm.shell.shared.ShellTransitions;
+import com.android.wm.shell.shared.annotations.ShellMainThread;
 import com.android.wm.shell.splitscreen.SplitScreen;
 import com.android.wm.shell.startingsurface.StartingSurface;
 import com.android.wm.shell.sysui.ShellInterface;
 import com.android.wm.shell.taskview.TaskViewFactory;
-import com.android.wm.shell.transition.ShellTransitions;
 
 import dagger.BindsInstance;
 import dagger.Subcomponent;
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt
index 1b832d4..037c23b 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/ui/viewmodel/DreamViewModel.kt
@@ -57,7 +57,7 @@
                 !keyguardUpdateMonitor.isEncryptedOrLockdown(userTracker.userId)
         if (showGlanceableHub) {
             toGlanceableHubTransitionViewModel.startTransition()
-            communalInteractor.onSceneChanged(CommunalScenes.Communal)
+            communalInteractor.changeScene(CommunalScenes.Communal)
         } else {
             toLockscreenTransitionViewModel.startTransition()
         }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 19a44cc..d33e7ff 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -394,10 +394,6 @@
     // TODO(b/251205791): Tracking Bug
     @JvmField val SCREENSHOT_APP_CLIPS = releasedFlag("screenshot_app_clips")
 
-    /** TODO(b/295143676): Tracking bug. When enable, captures a screenshot for each display. */
-    @JvmField
-    val MULTI_DISPLAY_SCREENSHOT = releasedFlag("multi_display_screenshot")
-
     // 1400 - columbus
     // TODO(b/254512756): Tracking Bug
     val QUICK_TAP_IN_PCC = releasedFlag("quick_tap_in_pcc")
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
index 3134e35..6b53f4e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -84,8 +84,8 @@
 import com.android.systemui.power.shared.model.ScreenPowerState;
 import com.android.systemui.settings.DisplayTracker;
 import com.android.wm.shell.shared.CounterRotator;
+import com.android.wm.shell.shared.ShellTransitions;
 import com.android.wm.shell.shared.TransitionUtil;
-import com.android.wm.shell.transition.ShellTransitions;
 import com.android.wm.shell.transition.Transitions;
 
 import java.util.ArrayList;
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
index 53c81e5..cf83582 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt
@@ -20,7 +20,7 @@
 import android.animation.AnimatorListenerAdapter
 import android.animation.ValueAnimator
 import android.app.WallpaperManager
-import android.content.Context
+import android.content.res.Resources
 import android.graphics.Matrix
 import android.graphics.Rect
 import android.os.DeadObjectException
@@ -32,16 +32,18 @@
 import android.view.SurfaceControl
 import android.view.SyncRtSurfaceTransactionApplier
 import android.view.View
+import android.view.WindowManager
 import androidx.annotation.VisibleForTesting
 import androidx.core.math.MathUtils
 import com.android.app.animation.Interpolators
 import com.android.internal.R
 import com.android.keyguard.KeyguardClockSwitchController
 import com.android.keyguard.KeyguardViewController
+import com.android.systemui.Flags.fastUnlockTransition
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
-import com.android.systemui.Flags.fastUnlockTransition
 import com.android.systemui.plugins.BcSmartspaceDataPlugin
 import com.android.systemui.shared.recents.utilities.Utilities
 import com.android.systemui.shared.system.ActivityManagerWrapper
@@ -145,17 +147,18 @@
  */
 @SysUISingleton
 class KeyguardUnlockAnimationController @Inject constructor(
-    private val context: Context,
-    private val keyguardStateController: KeyguardStateController,
-    private val
+        private val windowManager: WindowManager,
+        @Main private val resources: Resources,
+        private val keyguardStateController: KeyguardStateController,
+        private val
     keyguardViewMediator: Lazy<KeyguardViewMediator>,
-    private val keyguardViewController: KeyguardViewController,
-    private val featureFlags: FeatureFlags,
-    private val biometricUnlockControllerLazy: Lazy<BiometricUnlockController>,
-    private val statusBarStateController: SysuiStatusBarStateController,
-    private val notificationShadeWindowController: NotificationShadeWindowController,
-    private val powerManager: PowerManager,
-    private val wallpaperManager: WallpaperManager
+        private val keyguardViewController: KeyguardViewController,
+        private val featureFlags: FeatureFlags,
+        private val biometricUnlockControllerLazy: Lazy<BiometricUnlockController>,
+        private val statusBarStateController: SysuiStatusBarStateController,
+        private val notificationShadeWindowController: NotificationShadeWindowController,
+        private val powerManager: PowerManager,
+        private val wallpaperManager: WallpaperManager
 ) : KeyguardStateController.Callback, ISysuiUnlockAnimationController.Stub() {
 
     interface KeyguardUnlockAnimationListener {
@@ -399,7 +402,7 @@
         keyguardStateController.addCallback(this)
 
         roundedCornerRadius =
-            context.resources.getDimensionPixelSize(R.dimen.rounded_corner_radius).toFloat()
+            resources.getDimensionPixelSize(R.dimen.rounded_corner_radius).toFloat()
     }
 
     /**
@@ -438,7 +441,7 @@
         Log.wtf(TAG, "  !notificationShadeWindowController.isLaunchingActivity: " +
                 "${!notificationShadeWindowController.isLaunchingActivity}")
         Log.wtf(TAG, "  launcherUnlockController != null: ${launcherUnlockController != null}")
-        Log.wtf(TAG, "  !isFoldable(context): ${!isFoldable(context)}")
+        Log.wtf(TAG, "  !isFoldable(context): ${!isFoldable(resources)}")
     }
 
     /**
@@ -1100,7 +1103,7 @@
 
         // We don't do the shared element on large screens because the smartspace has to fly across
         // large distances, which is distracting.
-        if (Utilities.isLargeScreen(context)) {
+        if (Utilities.isLargeScreen(windowManager, resources)) {
             return false
         }
 
@@ -1180,8 +1183,8 @@
 
     companion object {
 
-        fun isFoldable(context: Context): Boolean {
-            return context.resources.getIntArray(R.array.config_foldedDeviceStates).isNotEmpty()
+        fun isFoldable(resources: Resources): Boolean {
+            return resources.getIntArray(R.array.config_foldedDeviceStates).isNotEmpty()
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index fa845c7..3da3e2f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -29,6 +29,9 @@
 import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
 import androidx.constraintlayout.widget.ConstraintSet.START
 import androidx.constraintlayout.widget.ConstraintSet.TOP
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.SceneTransitionLayout
+import com.android.compose.animation.scene.transitions
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.keyguard.KeyguardStatusView
 import com.android.keyguard.KeyguardStatusViewController
@@ -109,6 +112,7 @@
 
     private var rootViewHandle: DisposableHandle? = null
     private var indicationAreaHandle: DisposableHandle? = null
+    private val sceneKey = SceneKey("root-view-scene-key")
 
     var keyguardStatusViewController: KeyguardStatusViewController? = null
         get() {
@@ -219,12 +223,25 @@
             blueprints.mapNotNull { it as? ComposableLockscreenSceneBlueprint }.toSet()
         return ComposeView(context).apply {
             setContent {
-                LockscreenContent(
-                        viewModel = viewModel,
-                        blueprints = sceneBlueprints,
-                        clockInteractor = clockInteractor
-                    )
-                    .Content(modifier = Modifier.fillMaxSize())
+                // STL is used solely to provide a SceneScope to enable us to invoke SceneScope
+                // composables.
+                SceneTransitionLayout(
+                    currentScene = sceneKey,
+                    onChangeScene = {},
+                    transitions = transitions {},
+                ) {
+                    scene(sceneKey) {
+                        with(
+                            LockscreenContent(
+                                viewModel = viewModel,
+                                blueprints = sceneBlueprints,
+                                clockInteractor = clockInteractor
+                            )
+                        ) {
+                            Content(modifier = Modifier.fillMaxSize())
+                        }
+                    }
+                }
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/backup/KeyguardQuickAffordanceBackupHelper.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/backup/KeyguardQuickAffordanceBackupHelper.kt
index fa6efa5..f763e62 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/backup/KeyguardQuickAffordanceBackupHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/backup/KeyguardQuickAffordanceBackupHelper.kt
@@ -17,14 +17,17 @@
 
 package com.android.systemui.keyguard.domain.backup
 
+import android.app.backup.BackupDataInputStream
 import android.app.backup.SharedPreferencesBackupHelper
 import android.content.Context
+import android.util.Log
+import com.android.app.tracing.traceSection
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager
 import com.android.systemui.settings.UserFileManagerImpl
 
 /** Handles backup & restore for keyguard quick affordances. */
 class KeyguardQuickAffordanceBackupHelper(
-    context: Context,
+    private val context: Context,
     userId: Int,
 ) :
     SharedPreferencesBackupHelper(
@@ -34,4 +37,17 @@
                 fileName = KeyguardQuickAffordanceSelectionManager.FILE_NAME,
             )
             .getPath()
-    )
+    ) {
+
+    override fun restoreEntity(data: BackupDataInputStream?) {
+        Log.d(TAG, "Starting restore for ${data?.key} for user ${context.userId}")
+        traceSection("$TAG File restore: ${data?.key}") {
+            super.restoreEntity(data)
+        }
+        Log.d(TAG, "Finished restore for ${data?.key} for user ${context.userId}")
+    }
+
+    companion object {
+        private const val TAG = "KeyguardQuickAffordanceBackupHelper"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractor.kt
index 9aa2202..03ed567 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardOcclusionInteractor.kt
@@ -19,13 +19,17 @@
 import android.app.ActivityManager.RunningTaskInfo
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor
 import com.android.systemui.keyguard.data.repository.KeyguardOcclusionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.util.kotlin.sample
+import dagger.Lazy
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.map
@@ -45,11 +49,12 @@
 class KeyguardOcclusionInteractor
 @Inject
 constructor(
-    @Application scope: CoroutineScope,
-    val repository: KeyguardOcclusionRepository,
-    val powerInteractor: PowerInteractor,
-    val transitionInteractor: KeyguardTransitionInteractor,
-    val keyguardInteractor: KeyguardInteractor,
+    @Application applicationScope: CoroutineScope,
+    private val repository: KeyguardOcclusionRepository,
+    private val powerInteractor: PowerInteractor,
+    private val transitionInteractor: KeyguardTransitionInteractor,
+    keyguardInteractor: KeyguardInteractor,
+    deviceUnlockedInteractor: Lazy<DeviceUnlockedInteractor>,
 ) {
     val showWhenLockedActivityInfo = repository.showWhenLockedActivityInfo.asStateFlow()
 
@@ -94,14 +99,19 @@
                 // Emit false once that activity goes away.
                 isShowWhenLockedActivityOnTop.filter { !it }.map { false }
             )
-            .stateIn(scope, SharingStarted.Eagerly, false)
+            .stateIn(applicationScope, SharingStarted.Eagerly, false)
 
     /**
      * Whether launching an occluding activity will automatically dismiss keyguard. This happens if
      * the keyguard is dismissable.
      */
-    val occludingActivityWillDismissKeyguard =
-        keyguardInteractor.isKeyguardDismissible.stateIn(scope, SharingStarted.Eagerly, false)
+    val occludingActivityWillDismissKeyguard: StateFlow<Boolean> =
+        if (SceneContainerFlag.isEnabled) {
+                deviceUnlockedInteractor.get().isDeviceUnlocked
+            } else {
+                keyguardInteractor.isKeyguardDismissible
+            }
+            .stateIn(scope = applicationScope, SharingStarted.Eagerly, false)
 
     /**
      * Called to let System UI know that WM says a SHOW_WHEN_LOCKED activity is on top (or no longer
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt
index d9f12c3..5906cfd 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardPreviewClockViewBinder.kt
@@ -18,6 +18,7 @@
 package com.android.systemui.keyguard.ui.binder
 
 import android.content.Context
+import android.util.DisplayMetrics
 import android.view.View
 import android.view.View.INVISIBLE
 import android.view.View.VISIBLE
@@ -109,7 +110,7 @@
     private fun applyClockDefaultConstraints(context: Context, constraints: ConstraintSet) {
         constraints.apply {
             constrainWidth(R.id.lockscreen_clock_view_large, ConstraintSet.WRAP_CONTENT)
-            constrainHeight(R.id.lockscreen_clock_view_large, ConstraintSet.WRAP_CONTENT)
+            constrainHeight(R.id.lockscreen_clock_view_large, ConstraintSet.MATCH_CONSTRAINT)
             val largeClockTopMargin =
                 context.resources.getDimensionPixelSize(R.dimen.status_bar_height) +
                     context.resources.getDimensionPixelSize(
@@ -129,7 +130,29 @@
                 ConstraintSet.END
             )
 
-            connect(R.id.lockscreen_clock_view_large, BOTTOM, R.id.lock_icon_view, TOP)
+            // In preview, we'll show UDFPS icon for UDFPS devices
+            // and nothing for non-UDFPS devices,
+            // but we need position of device entry icon to constrain clock
+            if (getConstraint(R.id.lock_icon_view) != null) {
+                connect(R.id.lockscreen_clock_view_large, BOTTOM, R.id.lock_icon_view, TOP)
+            } else {
+                // Copied calculation codes from applyConstraints in DefaultDeviceEntrySection
+                val bottomPaddingPx =
+                    context.resources.getDimensionPixelSize(R.dimen.lock_icon_margin_bottom)
+                val defaultDensity =
+                    DisplayMetrics.DENSITY_DEVICE_STABLE.toFloat() /
+                        DisplayMetrics.DENSITY_DEFAULT.toFloat()
+                val lockIconRadiusPx = (defaultDensity * 36).toInt()
+                val clockBottomMargin = bottomPaddingPx + 2 * lockIconRadiusPx
+                connect(
+                    R.id.lockscreen_clock_view_large,
+                    BOTTOM,
+                    PARENT_ID,
+                    BOTTOM,
+                    clockBottomMargin
+                )
+            }
+
             constrainWidth(R.id.lockscreen_clock_view, WRAP_CONTENT)
             constrainHeight(
                 R.id.lockscreen_clock_view,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
index 881467f..4a09939 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
@@ -25,6 +25,7 @@
 import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
 import androidx.constraintlayout.widget.ConstraintSet.END
 import androidx.constraintlayout.widget.ConstraintSet.GONE
+import androidx.constraintlayout.widget.ConstraintSet.MATCH_CONSTRAINT
 import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
 import androidx.constraintlayout.widget.ConstraintSet.START
 import androidx.constraintlayout.widget.ConstraintSet.TOP
@@ -160,7 +161,7 @@
         constraints.apply {
             connect(R.id.lockscreen_clock_view_large, START, PARENT_ID, START)
             connect(R.id.lockscreen_clock_view_large, END, guideline, END)
-            connect(R.id.lockscreen_clock_view_large, BOTTOM, R.id.lock_icon_view, TOP)
+            connect(R.id.lockscreen_clock_view_large, BOTTOM, R.id.device_entry_icon_view, TOP)
             var largeClockTopMargin =
                 context.resources.getDimensionPixelSize(R.dimen.status_bar_height) +
                     context.resources.getDimensionPixelSize(
@@ -172,7 +173,7 @@
 
             connect(R.id.lockscreen_clock_view_large, TOP, PARENT_ID, TOP, largeClockTopMargin)
             constrainWidth(R.id.lockscreen_clock_view_large, WRAP_CONTENT)
-            constrainHeight(R.id.lockscreen_clock_view_large, WRAP_CONTENT)
+            constrainHeight(R.id.lockscreen_clock_view_large, MATCH_CONSTRAINT)
             constrainWidth(R.id.lockscreen_clock_view, WRAP_CONTENT)
             constrainHeight(
                 R.id.lockscreen_clock_view,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
index 4c846e4..29041d1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt
@@ -34,6 +34,7 @@
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
+import com.android.systemui.keyguard.MigrateClocksToBlueprint
 import com.android.systemui.keyguard.shared.model.KeyguardSection
 import com.android.systemui.keyguard.ui.binder.DeviceEntryIconViewBinder
 import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
@@ -72,7 +73,7 @@
     override fun addViews(constraintLayout: ConstraintLayout) {
         if (
             !KeyguardBottomAreaRefactor.isEnabled &&
-                !DeviceEntryUdfpsRefactor.isEnabled &&
+                !MigrateClocksToBlueprint.isEnabled &&
                 !DeviceEntryUdfpsRefactor.isEnabled
         ) {
             return
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt
index ac2713d..8c6be98 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.keyguard.shared.model.ScrimAlpha
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import com.android.systemui.statusbar.SysuiStatusBarStateController
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -40,6 +41,7 @@
 constructor(
     bouncerToGoneFlows: BouncerToGoneFlows,
     animationFlow: KeyguardTransitionAnimationFlow,
+    private val statusBarStateController: SysuiStatusBarStateController,
 ) : DeviceEntryIconTransition {
     private val transitionAnimation =
         animationFlow.setup(
@@ -59,6 +61,30 @@
         )
     }
 
+    fun notificationAlpha(viewState: ViewStateAccessor): Flow<Float> {
+        var startAlpha = 1f
+        var leaveShadeOpen = false
+
+        return transitionAnimation.sharedFlow(
+            duration = 200.milliseconds,
+            onStart = {
+                leaveShadeOpen = statusBarStateController.leaveOpenOnKeyguardHide()
+                startAlpha = viewState.alpha()
+            },
+            onStep = {
+                if (leaveShadeOpen) {
+                    1f
+                } else {
+                    MathUtils.lerp(startAlpha, 0f, it)
+                }
+            },
+        )
+    }
+
+    /** See [BouncerToGoneFlows#showAllNotifications] */
+    val showAllNotifications: Flow<Boolean> =
+        bouncerToGoneFlows.showAllNotifications(TO_GONE_DURATION, ALTERNATE_BOUNCER)
+
     /** Scrim alpha values */
     val scrimAlpha: Flow<ScrimAlpha> =
         bouncerToGoneFlows.scrimAlpha(TO_GONE_DURATION, ALTERNATE_BOUNCER)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlows.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlows.kt
index 924fc5d..fe88b81 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlows.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlows.kt
@@ -32,6 +32,7 @@
 import kotlin.time.Duration
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.map
 
@@ -63,6 +64,31 @@
         }
     }
 
+    /**
+     * When the shade is expanded, make sure that all notifications can be seen immediately during a
+     * transition to GONE. This matters especially when the user has chosen to not show
+     * notifications on the lockscreen and then pulls down the shade, which presents them with an
+     * immediate auth prompt, followed by a notification animation.
+     */
+    fun showAllNotifications(duration: Duration, from: KeyguardState): Flow<Boolean> {
+        var leaveShadeOpen = false
+        return animationFlow
+            .setup(
+                duration = duration,
+                from = from,
+                to = GONE,
+            )
+            .sharedFlow(
+                duration = duration,
+                onStart = { leaveShadeOpen = statusBarStateController.leaveOpenOnKeyguardHide() },
+                onStep = { if (leaveShadeOpen) 1f else 0f },
+                onFinish = { 0f },
+                onCancel = { 0f },
+            )
+            .map { it == 1f }
+            .distinctUntilChanged()
+    }
+
     private fun createScrimAlphaFlow(
         duration: Duration,
         fromState: KeyguardState,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
index 1f80441..36896f9 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt
@@ -18,8 +18,10 @@
 
 import android.content.res.Resources
 import com.android.keyguard.KeyguardClockSwitch
+import com.android.keyguard.KeyguardClockSwitch.SMALL
 import com.android.systemui.biometrics.AuthController
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
 import com.android.systemui.res.R
@@ -29,6 +31,7 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
@@ -42,6 +45,7 @@
     private val authController: AuthController,
     val longPress: KeyguardLongPressViewModel,
     val shadeInteractor: ShadeInteractor,
+    @Application private val applicationScope: CoroutineScope,
 ) {
     private val clockSize = clockInteractor.clockSize
 
@@ -50,11 +54,26 @@
     val isLargeClockVisible: Boolean
         get() = clockSize.value == KeyguardClockSwitch.LARGE
 
-    val areNotificationsVisible: Boolean
-        get() = !isLargeClockVisible || shouldUseSplitNotificationShade
+    val shouldUseSplitNotificationShade: StateFlow<Boolean> =
+        shadeInteractor.shadeMode
+            .map { it == ShadeMode.Split }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = false,
+            )
 
-    val shouldUseSplitNotificationShade: Boolean
-        get() = shadeInteractor.shadeMode.value == ShadeMode.Split
+    val areNotificationsVisible: StateFlow<Boolean> =
+        combine(clockSize, shouldUseSplitNotificationShade) {
+                clockSize,
+                shouldUseSplitNotificationShade ->
+                clockSize == SMALL || shouldUseSplitNotificationShade
+            }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = false,
+            )
 
     fun getSmartSpacePaddingTop(resources: Resources): Int {
         return if (isLargeClockVisible) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt
index 4e6aa03..f03625e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModel.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
 import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow.FlowBuilder
 import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition
+import com.android.systemui.statusbar.SysuiStatusBarStateController
 import javax.inject.Inject
 import kotlin.time.Duration.Companion.milliseconds
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -37,6 +38,7 @@
 @Inject
 constructor(
     animationFlow: KeyguardTransitionAnimationFlow,
+    private val statusBarStateController: SysuiStatusBarStateController,
 ) : DeviceEntryIconTransition {
 
     private val transitionAnimation: FlowBuilder =
@@ -54,6 +56,26 @@
             onCancel = { 1f },
         )
 
+    fun notificationAlpha(viewState: ViewStateAccessor): Flow<Float> {
+        var startAlpha = 1f
+        var leaveShadeOpen = false
+
+        return transitionAnimation.sharedFlow(
+            duration = 200.milliseconds,
+            onStart = {
+                leaveShadeOpen = statusBarStateController.leaveOpenOnKeyguardHide()
+                startAlpha = viewState.alpha()
+            },
+            onStep = {
+                if (leaveShadeOpen) {
+                    1f
+                } else {
+                    MathUtils.lerp(startAlpha, 0f, it)
+                }
+            },
+        )
+    }
+
     fun lockscreenAlpha(viewState: ViewStateAccessor): Flow<Float> {
         var startAlpha = 1f
         return transitionAnimation.sharedFlow(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt
index 53f4488..0587826 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModel.kt
@@ -60,6 +60,10 @@
     private var leaveShadeOpen: Boolean = false
     private var willRunDismissFromKeyguard: Boolean = false
 
+    /** See [BouncerToGoneFlows#showAllNotifications] */
+    val showAllNotifications: Flow<Boolean> =
+        bouncerToGoneFlows.showAllNotifications(TO_GONE_DURATION, PRIMARY_BOUNCER)
+
     val notificationAlpha: Flow<Float> =
         transitionAnimation.sharedFlow(
             duration = 200.milliseconds,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java
index 1aef920..8d3500a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java
@@ -29,6 +29,7 @@
 import com.android.systemui.qs.QSHost;
 import com.android.systemui.qs.ReduceBrightColorsController;
 import com.android.systemui.qs.external.QSExternalModule;
+import com.android.systemui.qs.panels.dagger.PanelsModule;
 import com.android.systemui.qs.pipeline.dagger.QSPipelineModule;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
 import com.android.systemui.qs.tiles.di.QSTilesModule;
@@ -44,21 +45,22 @@
 import com.android.systemui.statusbar.policy.WalletController;
 import com.android.systemui.util.settings.SecureSettings;
 
-import java.util.Map;
-
-import javax.inject.Named;
-
 import dagger.Binds;
 import dagger.Module;
 import dagger.Provides;
 import dagger.multibindings.Multibinds;
 
+import java.util.Map;
+
+import javax.inject.Named;
+
 /**
  * Module for QS dependencies
  */
 @Module(subcomponents = {QSFragmentComponent.class, QSSceneComponent.class},
         includes = {
                 MediaModule.class,
+                PanelsModule.class,
                 QSExternalModule.class,
                 QSFlagsModule.class,
                 QSHostModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.kt
new file mode 100644
index 0000000..1307296
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/dagger/PanelsModule.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.systemui.qs.panels.dagger
+
+import com.android.systemui.qs.panels.data.repository.IconTilesRepository
+import com.android.systemui.qs.panels.data.repository.IconTilesRepositoryImpl
+import dagger.Binds
+import dagger.Module
+
+@Module
+interface PanelsModule {
+    @Binds fun bindIconTilesRepository(impl: IconTilesRepositoryImpl): IconTilesRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/IconTilesRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/IconTilesRepository.kt
new file mode 100644
index 0000000..92f87e7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/data/repository/IconTilesRepository.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.qs.panels.data.repository
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+
+/** Repository for retrieving the list of [TileSpec] to be displayed as icons. */
+interface IconTilesRepository {
+    val iconTilesSpecs: Flow<Set<TileSpec>>
+}
+
+@SysUISingleton
+class IconTilesRepositoryImpl @Inject constructor() : IconTilesRepository {
+
+    /** Set of toggleable tiles that are suitable for being shown as an icon. */
+    override val iconTilesSpecs: Flow<Set<TileSpec>> =
+        flowOf(
+            setOf(
+                TileSpec.create("airplane"),
+                TileSpec.create("battery"),
+                TileSpec.create("cameratoggle"),
+                TileSpec.create("cast"),
+                TileSpec.create("color_correction"),
+                TileSpec.create("inversion"),
+                TileSpec.create("saver"),
+                TileSpec.create("dnd"),
+                TileSpec.create("flashlight"),
+                TileSpec.create("location"),
+                TileSpec.create("mictoggle"),
+                TileSpec.create("nfc"),
+                TileSpec.create("night"),
+                TileSpec.create("rotation")
+            )
+        )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt
new file mode 100644
index 0000000..367c670
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/panels/domain/interactor/IconTilesInteractor.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.panels.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qs.panels.data.repository.IconTilesRepository
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+/** Interactor for retrieving the list of [TileSpec] to be displayed as icons. */
+@SysUISingleton
+class IconTilesInteractor @Inject constructor(private val repo: IconTilesRepository) {
+    val iconTilesSpecs: Flow<Set<TileSpec>> = repo.iconTilesSpecs
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt
index 15b8cfb..f34389e 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/SubtitleArrayMapping.kt
@@ -52,6 +52,7 @@
         subtitleIdsMap["color_correction"] = R.array.tile_states_color_correction
         subtitleIdsMap["dream"] = R.array.tile_states_dream
         subtitleIdsMap["font_scaling"] = R.array.tile_states_font_scaling
+        subtitleIdsMap["hearing_devices"] = R.array.tile_states_hearing_devices
     }
 
     /** Get the subtitle resource id of the given tile */
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/HearingDevicesTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/HearingDevicesTile.java
new file mode 100644
index 0000000..1fb701e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/HearingDevicesTile.java
@@ -0,0 +1,95 @@
+/*
+ * 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.qs.tiles;
+
+import android.content.Intent;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.systemui.Flags;
+import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.plugins.FalsingManager;
+import com.android.systemui.plugins.qs.QSTile.State;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.qs.QSHost;
+import com.android.systemui.qs.QsEventLogger;
+import com.android.systemui.qs.logging.QSLogger;
+import com.android.systemui.qs.tileimpl.QSTileImpl;
+import com.android.systemui.res.R;
+
+import javax.inject.Inject;
+
+/** Quick settings tile: Hearing Devices **/
+public class HearingDevicesTile extends QSTileImpl<State> {
+
+    public static final String TILE_SPEC = "hearing_devices";
+
+    @Inject
+    public HearingDevicesTile(
+            QSHost host,
+            QsEventLogger uiEventLogger,
+            @Background Looper backgroundLooper,
+            @Main Handler mainHandler,
+            FalsingManager falsingManager,
+            MetricsLogger metricsLogger,
+            StatusBarStateController statusBarStateController,
+            ActivityStarter activityStarter,
+            QSLogger qsLogger
+    ) {
+        super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger,
+                statusBarStateController, activityStarter, qsLogger);
+    }
+
+    @Override
+    public State newTileState() {
+        return new State();
+    }
+
+    @Override
+    protected void handleClick(@Nullable View view) {
+
+    }
+
+    @Override
+    protected void handleUpdateState(State state, Object arg) {
+        state.label = mContext.getString(R.string.quick_settings_hearing_devices_label);
+        state.icon = ResourceIcon.get(R.drawable.qs_hearing_devices_icon);
+    }
+
+    @Nullable
+    @Override
+    public Intent getLongClickIntent() {
+        return new Intent(Settings.ACTION_HEARING_DEVICES_SETTINGS);
+    }
+
+    @Override
+    public CharSequence getTileLabel() {
+        return mContext.getString(R.string.quick_settings_hearing_devices_label);
+    }
+
+    @Override
+    public boolean isAvailable() {
+        return Flags.hearingAidsQsTileDialog();
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt
new file mode 100644
index 0000000..a823916
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.scene.domain.interactor
+
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.SceneKey
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.KeyguardOcclusionInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.scene.shared.model.Scenes
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+
+/** Encapsulates logic regarding the occlusion state of the scene container. */
+@SysUISingleton
+class SceneContainerOcclusionInteractor
+@Inject
+constructor(
+    keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
+    sceneInteractor: SceneInteractor,
+    keyguardTransitionInteractor: KeyguardTransitionInteractor,
+) {
+    /**
+     * Whether the scene container should become invisible due to "occlusion" by an in-foreground
+     * "show when locked" activity.
+     */
+    val invisibleDueToOcclusion: Flow<Boolean> =
+        combine(
+                keyguardOcclusionInteractor.isShowWhenLockedActivityOnTop,
+                sceneInteractor.transitionState,
+                keyguardTransitionInteractor
+                    .transitionValue(KeyguardState.AOD)
+                    .onStart { emit(0f) }
+                    .map { it > 0 }
+                    .distinctUntilChanged(),
+            ) { isOccludingActivityShown, sceneTransitionState, isAodFullyOrPartiallyShown ->
+                isOccludingActivityShown &&
+                    !isAodFullyOrPartiallyShown &&
+                    sceneTransitionState.canBeOccluded
+            }
+            .distinctUntilChanged()
+
+    private val ObservableTransitionState.canBeOccluded: Boolean
+        get() =
+            when (this) {
+                is ObservableTransitionState.Idle -> scene.canBeOccluded
+                is ObservableTransitionState.Transition ->
+                    fromScene.canBeOccluded && toScene.canBeOccluded
+            }
+
+    /**
+     * Whether the scene can be occluded by a "show when locked" activity. Some scenes should, on
+     * principle not be occlude-able because they render as if they are expanding on top of the
+     * occluding activity.
+     */
+    private val SceneKey.canBeOccluded: Boolean
+        get() =
+            when (this) {
+                Scenes.Bouncer -> true
+                Scenes.Communal -> true
+                Scenes.Gone -> true
+                Scenes.Lockscreen -> true
+                Scenes.QuickSettings -> false
+                Scenes.Shade -> false
+                else -> error("SceneKey \"$this\" doesn't have a mapping for canBeOccluded!")
+            }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index 42b41f8..0e4049b 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
@@ -40,6 +40,7 @@
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.plugins.FalsingManager.FalsingBeliefListener
 import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.scene.domain.interactor.SceneContainerOcclusionInteractor
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.flag.SceneContainerFlags
 import com.android.systemui.scene.shared.logger.SceneLogger
@@ -94,6 +95,7 @@
     private val deviceProvisioningInteractor: DeviceProvisioningInteractor,
     private val centralSurfaces: CentralSurfaces,
     private val headsUpInteractor: HeadsUpNotificationInteractor,
+    private val occlusionInteractor: SceneContainerOcclusionInteractor,
 ) : CoreStartable {
 
     override fun start() {
@@ -130,32 +132,36 @@
                 .distinctUntilChanged()
                 .flatMapLatest { isAllowedToBeVisible ->
                     if (isAllowedToBeVisible) {
-                        sceneInteractor.transitionState
-                            .mapNotNull { state ->
-                                when (state) {
-                                    is ObservableTransitionState.Idle -> {
-                                        if (state.scene != Scenes.Gone) {
-                                            true to "scene is not Gone"
-                                        } else {
-                                            false to "scene is Gone"
+                        combine(
+                                sceneInteractor.transitionState.mapNotNull { state ->
+                                    when (state) {
+                                        is ObservableTransitionState.Idle -> {
+                                            if (state.scene != Scenes.Gone) {
+                                                true to "scene is not Gone"
+                                            } else {
+                                                false to "scene is Gone"
+                                            }
+                                        }
+                                        is ObservableTransitionState.Transition -> {
+                                            if (state.fromScene == Scenes.Gone) {
+                                                true to "scene transitioning away from Gone"
+                                            } else {
+                                                null
+                                            }
                                         }
                                     }
-                                    is ObservableTransitionState.Transition -> {
-                                        if (state.fromScene == Scenes.Gone) {
-                                            true to "scene transitioning away from Gone"
-                                        } else {
-                                            null
-                                        }
-                                    }
-                                }
-                            }
-                            .combine(headsUpInteractor.isHeadsUpOrAnimatingAway) {
+                                },
+                                headsUpInteractor.isHeadsUpOrAnimatingAway,
+                                occlusionInteractor.invisibleDueToOcclusion,
+                            ) {
                                 visibilityForTransitionState,
-                                isHeadsUpOrAnimatingAway ->
-                                if (isHeadsUpOrAnimatingAway) {
-                                    true to "showing a HUN"
-                                } else {
-                                    visibilityForTransitionState
+                                isHeadsUpOrAnimatingAway,
+                                invisibleDueToOcclusion,
+                                ->
+                                when {
+                                    isHeadsUpOrAnimatingAway -> true to "showing a HUN"
+                                    invisibleDueToOcclusion -> false to "invisible due to occlusion"
+                                    else -> visibilityForTransitionState
                                 }
                             }
                             .distinctUntilChanged()
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt
index 69dce83..2fbcba9 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/SceneDataSourceDelegator.kt
@@ -20,9 +20,6 @@
 
 import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.TransitionKey
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -36,14 +33,10 @@
  * Delegates calls to a runtime-provided [SceneDataSource] or to a no-op implementation if a
  * delegate isn't set.
  */
-@SysUISingleton
-class SceneDataSourceDelegator
-@Inject
-constructor(
-    @Application private val applicationScope: CoroutineScope,
+class SceneDataSourceDelegator(
+    applicationScope: CoroutineScope,
     config: SceneContainerConfig,
 ) : SceneDataSource {
-
     private val noOpDelegate = NoOpSceneDataSource(config.initialSceneKey)
     private val delegateMutable = MutableStateFlow<SceneDataSource>(noOpDelegate)
 
@@ -82,6 +75,7 @@
     ) : SceneDataSource {
         override val currentScene: StateFlow<SceneKey> =
             MutableStateFlow(initialSceneKey).asStateFlow()
+
         override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) = Unit
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt
index 3081f89..922997d 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/RequestProcessor.kt
@@ -18,32 +18,11 @@
 
 import android.util.Log
 import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.app.tracing.coroutines.launch
-import kotlinx.coroutines.CoroutineScope
-import java.util.function.Consumer
-import javax.inject.Inject
-
-/** Processes a screenshot request sent from [ScreenshotHelper]. */
-interface ScreenshotRequestProcessor {
-    /**
-     * Inspects the incoming ScreenshotData, potentially modifying it based upon policy.
-     *
-     * @param screenshot the screenshot to process
-     */
-    suspend fun process(screenshot: ScreenshotData): ScreenshotData
-}
 
 /** Implementation of [ScreenshotRequestProcessor] */
-@SysUISingleton
-class RequestProcessor
-@Inject
-constructor(
+class RequestProcessor(
     private val capture: ImageCapture,
     private val policy: ScreenshotPolicy,
-    /** For the Java Async version, to invoke the callback. */
-    @Application private val mainScope: CoroutineScope
 ) : ScreenshotRequestProcessor {
 
     override suspend fun process(screenshot: ScreenshotData): ScreenshotData {
@@ -78,24 +57,6 @@
 
         return result
     }
-
-    /**
-     * Note: This is for compatibility with existing Java. Prefer the suspending function when
-     * calling from a Coroutine context.
-     *
-     * @param screenshot the screenshot to process
-     * @param callback the callback to provide the processed screenshot, invoked from the main
-     *                 thread
-     */
-    fun processAsync(screenshot: ScreenshotData, callback: Consumer<ScreenshotData>) {
-        mainScope.launch({ "$TAG#processAsync" }) {
-            val result = process(screenshot)
-            callback.accept(result)
-        }
-    }
 }
 
 private const val TAG = "RequestProcessor"
-
-/** Exception thrown by [RequestProcessor] if something goes wrong. */
-class RequestProcessorException(message: String) : IllegalStateException(message)
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index b796a20..047ecb4 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -29,8 +29,6 @@
 import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER;
 import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_INTERACTION_TIMEOUT;
 
-import static java.util.Objects.requireNonNull;
-
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.annotation.MainThread;
@@ -41,7 +39,6 @@
 import android.app.ExitTransitionCoordinator;
 import android.app.ICompatCameraControlCallback;
 import android.app.Notification;
-import android.app.assist.AssistContent;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -73,7 +70,6 @@
 import android.view.WindowInsets;
 import android.view.WindowManager;
 import android.view.WindowManagerGlobal;
-import android.view.accessibility.AccessibilityManager;
 import android.widget.Toast;
 import android.window.WindowContext;
 
@@ -81,11 +77,11 @@
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.policy.PhoneWindow;
 import com.android.settingslib.applications.InterestingConfigChanges;
+import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.broadcast.BroadcastSender;
 import com.android.systemui.clipboardoverlay.ClipboardOverlayController;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.res.R;
 import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback;
 import com.android.systemui.screenshot.scroll.LongScreenshotActivity;
@@ -218,17 +214,10 @@
     // ScreenshotNotificationSmartActionsProvider.
     static final String EXTRA_ACTION_TYPE = "android:screenshot_action_type";
     static final String EXTRA_ID = "android:screenshot_id";
-    static final String ACTION_TYPE_DELETE = "Delete";
-    static final String ACTION_TYPE_SHARE = "Share";
-    static final String ACTION_TYPE_EDIT = "Edit";
     static final String EXTRA_SMART_ACTIONS_ENABLED = "android:smart_actions_enabled";
-    static final String EXTRA_OVERRIDE_TRANSITION = "android:screenshot_override_transition";
     static final String EXTRA_ACTION_INTENT = "android:screenshot_action_intent";
     static final String EXTRA_ACTION_INTENT_FILLIN = "android:screenshot_action_intent_fillin";
 
-    static final String SCREENSHOT_URI_ID = "android:screenshot_uri_id";
-    static final String EXTRA_CANCEL_NOTIFICATION = "android:screenshot_cancel_notification";
-    static final String EXTRA_DISALLOW_ENTER_PIP = "android:screenshot_disallow_enter_pip";
 
     // From WizardManagerHelper.java
     private static final String SETTINGS_SECURE_USER_SETUP_COMPLETE = "user_setup_complete";
@@ -247,10 +236,10 @@
     private final Executor mMainExecutor;
     private final ExecutorService mBgExecutor;
     private final BroadcastSender mBroadcastSender;
+    private final BroadcastDispatcher mBroadcastDispatcher;
 
     private final WindowManager mWindowManager;
     private final WindowManager.LayoutParams mWindowLayoutParams;
-    private final AccessibilityManager mAccessibilityManager;
     @Nullable
     private final ScreenshotSoundController mScreenshotSoundController;
     private final ScrollCaptureClient mScrollCaptureClient;
@@ -278,7 +267,7 @@
     private Animator mScreenshotAnimation;
     private RequestCallback mCurrentRequestCallback;
     private String mPackageName = "";
-    private BroadcastReceiver mCopyBroadcastReceiver;
+    private final BroadcastReceiver mCopyBroadcastReceiver;
 
     // When false, the screenshot is taken without showing the ui. Note that this only applies to
     // external displays, as on the default one the UI should **always** be shown.
@@ -300,6 +289,8 @@
     @AssistedInject
     ScreenshotController(
             Context context,
+            DisplayManager displayManager,
+            WindowManager windowManager,
             FeatureFlags flags,
             ScreenshotViewProxy.Factory viewProxyFactory,
             ScreenshotActionsProvider.Factory actionsProviderFactory,
@@ -315,6 +306,7 @@
             ActivityManager activityManager,
             TimeoutHandler timeoutHandler,
             BroadcastSender broadcastSender,
+            BroadcastDispatcher broadcastDispatcher,
             ScreenshotNotificationSmartActionsProvider screenshotNotificationSmartActionsProvider,
             ActionIntentExecutor actionExecutor,
             UserManager userManager,
@@ -337,16 +329,17 @@
         mScreenshotNotificationSmartActionsProvider = screenshotNotificationSmartActionsProvider;
         mBgExecutor = Executors.newSingleThreadExecutor();
         mBroadcastSender = broadcastSender;
+        mBroadcastDispatcher = broadcastDispatcher;
 
         mScreenshotHandler = timeoutHandler;
         mScreenshotHandler.setDefaultTimeoutMillis(SCREENSHOT_CORNER_DEFAULT_TIMEOUT_MILLIS);
 
 
         mDisplayId = displayId;
-        mDisplayManager = requireNonNull(context.getSystemService(DisplayManager.class));
+        mDisplayManager = displayManager;
+        mWindowManager = windowManager;
         final Context displayContext = context.createDisplayContext(getDisplay());
         mContext = (WindowContext) displayContext.createWindowContext(TYPE_SCREENSHOT, null);
-        mWindowManager = mContext.getSystemService(WindowManager.class);
         mFlags = flags;
         mActionExecutor = actionExecutor;
         mUserManager = userManager;
@@ -363,8 +356,6 @@
             mViewProxy.requestDismissal(SCREENSHOT_INTERACTION_TIMEOUT);
         });
 
-        mAccessibilityManager = AccessibilityManager.getInstance(mContext);
-
         // Setup the window that we are going to use
         mWindowLayoutParams = FloatingWindowUtil.getFloatingWindowParams();
         mWindowLayoutParams.setTitle("ScreenshotAnimation");
@@ -390,9 +381,9 @@
                 }
             }
         };
-        mContext.registerReceiver(mCopyBroadcastReceiver, new IntentFilter(
-                        ClipboardOverlayController.COPY_OVERLAY_ACTION),
-                ClipboardOverlayController.SELF_PERMISSION, null, Context.RECEIVER_NOT_EXPORTED);
+        mBroadcastDispatcher.registerReceiver(mCopyBroadcastReceiver, new IntentFilter(
+                        ClipboardOverlayController.COPY_OVERLAY_ACTION), null, null,
+                Context.RECEIVER_NOT_EXPORTED, ClipboardOverlayController.SELF_PERMISSION);
         mShowUIOnExternalDisplay = showUIOnExternalDisplay;
     }
 
@@ -442,16 +433,6 @@
 
         prepareViewForNewScreenshot(screenshot, oldPackageName);
 
-        if (mFlags.isEnabled(Flags.SCREENSHOT_METADATA) && screenshot.getTaskId() >= 0) {
-            mAssistContentRequester.requestAssistContent(screenshot.getTaskId(),
-                    new AssistContentRequester.Callback() {
-                        @Override
-                        public void onAssistContentAvailable(AssistContent assistContent) {
-                            screenshot.setContextUrl(assistContent.getWebUri());
-                        }
-                    });
-        }
-
         if (!shouldShowUi()) {
             saveScreenshotInWorkerThread(
                     screenshot.getUserHandle(), finisher, this::logSuccessOnActionsReady,
@@ -567,7 +548,7 @@
      * Release the constructed window context.
      */
     private void releaseContext() {
-        mContext.unregisterReceiver(mCopyBroadcastReceiver);
+        mBroadcastDispatcher.unregisterReceiver(mCopyBroadcastReceiver);
         mContext.release();
     }
 
@@ -615,7 +596,7 @@
         if (DEBUG_WINDOW) {
             Log.d(TAG, "setContentView: " + mViewProxy.getView());
         }
-        setContentView(mViewProxy.getView());
+        mWindow.setContentView(mViewProxy.getView());
     }
 
     private void enqueueScrollCaptureRequest(UserHandle owner) {
@@ -697,10 +678,8 @@
 
             final ScrollCaptureResponse response = mLastScrollCaptureResponse;
             mViewProxy.showScrollChip(response.getPackageName(), /* onClick */ () -> {
-                DisplayMetrics displayMetrics = new DisplayMetrics();
-                getDisplay().getRealMetrics(displayMetrics);
-                Bitmap newScreenshot = mImageCapture.captureDisplay(mDisplayId,
-                        new Rect(0, 0, displayMetrics.widthPixels, displayMetrics.heightPixels));
+                Bitmap newScreenshot =
+                        mImageCapture.captureDisplay(mDisplayId, getFullScreenRect());
 
                 if (newScreenshot != null) {
                     // delay starting scroll capture to make sure scrim is up before the app moves
@@ -797,10 +776,6 @@
         }
     }
 
-    private void setContentView(View contentView) {
-        mWindow.setContentView(contentView);
-    }
-
     @MainThread
     private void attachWindow() {
         View decorView = mWindow.getDecorView();
@@ -912,12 +887,10 @@
                     public void onFinish() {
                     }
                 };
-        Pair<ActivityOptions, ExitTransitionCoordinator> transition =
-                ActivityOptions.startSharedElementAnimation(mWindow, callbacks, null,
-                        Pair.create(mViewProxy.getScreenshotPreview(),
-                                ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME));
 
-        return transition;
+        return ActivityOptions.startSharedElementAnimation(mWindow, callbacks, null,
+                Pair.create(mViewProxy.getScreenshotPreview(),
+                        ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME));
     }
 
     /** Reset screenshot view and then call onCompleteRunnable */
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationsController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationsController.kt
index d874eb6..4079abe 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotNotificationsController.kt
@@ -105,7 +105,7 @@
 
     /** Factory for [ScreenshotNotificationsController]. */
     @AssistedFactory
-    interface Factory {
-        fun create(displayId: Int = Display.DEFAULT_DISPLAY): ScreenshotNotificationsController
+    fun interface Factory {
+        fun create(displayId: Int): ScreenshotNotificationsController
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt
index d5ab306..c8067df 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotProxyService.kt
@@ -30,7 +30,7 @@
 import kotlinx.coroutines.withContext
 
 /** Provides state from the main SystemUI process on behalf of the Screenshot process. */
-internal class ScreenshotProxyService
+class ScreenshotProxyService
 @Inject
 constructor(
     private val mExpansionMgr: ShadeExpansionStateManager,
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotRequestProcessor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotRequestProcessor.kt
new file mode 100644
index 0000000..796457d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotRequestProcessor.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot
+
+/** Processes a screenshot request sent from [ScreenshotHelper]. */
+interface ScreenshotRequestProcessor {
+    /**
+     * Inspects the incoming ScreenshotData, potentially modifying it based upon policy.
+     *
+     * @param screenshot the screenshot to process
+     */
+    suspend fun process(screenshot: ScreenshotData): ScreenshotData
+}
+
+/** Exception thrown by [RequestProcessor] if something goes wrong. */
+class RequestProcessorException(message: String) : IllegalStateException(message)
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
index cb2dba0..65e8457 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotView.java
@@ -90,7 +90,6 @@
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.logging.UiEventLogger;
 import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.flags.Flags;
 import com.android.systemui.res.R;
 import com.android.systemui.screenshot.scroll.ScrollCaptureController;
 import com.android.systemui.shared.system.InputChannelCompat;
@@ -789,15 +788,8 @@
             mUiEventLogger.log(ScreenshotEvent.SCREENSHOT_SHARE_TAPPED, 0, mPackageName);
             prepareSharedTransition();
 
-            Intent shareIntent;
-            if (mFlags.isEnabled(Flags.SCREENSHOT_METADATA) && mScreenshotData != null
-                    && mScreenshotData.getContextUrl() != null) {
-                shareIntent = ActionIntentCreator.INSTANCE.createShareWithText(
-                        imageData.uri, mScreenshotData.getContextUrl().toString());
-            } else {
-                shareIntent = ActionIntentCreator.INSTANCE.createShareWithSubject(
-                        imageData.uri, imageData.subject);
-            }
+            Intent shareIntent = ActionIntentCreator.INSTANCE.createShareWithSubject(
+                    imageData.uri, imageData.subject);
             mCallbacks.onAction(shareIntent, imageData.owner, false);
 
         });
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
index bc33755..92d3e55 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
@@ -5,7 +5,6 @@
 import android.util.Log
 import android.view.Display
 import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
-import com.android.app.tracing.coroutines.launch
 import com.android.internal.logging.UiEventLogger
 import com.android.internal.util.ScreenshotRequest
 import com.android.systemui.dagger.SysUISingleton
@@ -18,6 +17,23 @@
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+
+interface TakeScreenshotExecutor {
+    suspend fun executeScreenshots(
+        screenshotRequest: ScreenshotRequest,
+        onSaved: (Uri) -> Unit,
+        requestCallback: RequestCallback
+    )
+    fun onCloseSystemDialogsReceived()
+    fun removeWindows()
+    fun onDestroy()
+    fun executeScreenshotsAsync(
+        screenshotRequest: ScreenshotRequest,
+        onSaved: Consumer<Uri>,
+        requestCallback: RequestCallback
+    )
+}
 
 /**
  * Receives the signal to take a screenshot from [TakeScreenshotService], and calls back with the
@@ -26,7 +42,7 @@
  * Captures a screenshot for each [Display] available.
  */
 @SysUISingleton
-class TakeScreenshotExecutor
+class TakeScreenshotExecutorImpl
 @Inject
 constructor(
     private val screenshotControllerFactory: ScreenshotController.Factory,
@@ -35,7 +51,7 @@
     private val screenshotRequestProcessor: ScreenshotRequestProcessor,
     private val uiEventLogger: UiEventLogger,
     private val screenshotNotificationControllerFactory: ScreenshotNotificationsController.Factory,
-) {
+) : TakeScreenshotExecutor {
 
     private val displays = displayRepository.displays
     private val screenshotControllers = mutableMapOf<Int, ScreenshotController>()
@@ -47,7 +63,7 @@
      * [onSaved] is invoked only on the default display result. [RequestCallback.onFinish] is
      * invoked only when both screenshot UIs are removed.
      */
-    suspend fun executeScreenshots(
+    override suspend fun executeScreenshots(
         screenshotRequest: ScreenshotRequest,
         onSaved: (Uri) -> Unit,
         requestCallback: RequestCallback
@@ -128,12 +144,8 @@
         }
     }
 
-    /**
-     * Propagates the close system dialog signal to all controllers.
-     *
-     * TODO(b/295143676): Move the receiver in this class once the flag is flipped.
-     */
-    fun onCloseSystemDialogsReceived() {
+    /** Propagates the close system dialog signal to all controllers. */
+    override fun onCloseSystemDialogsReceived() {
         screenshotControllers.forEach { (_, screenshotController) ->
             if (!screenshotController.isPendingSharedTransition) {
                 screenshotController.requestDismissal(ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER)
@@ -142,7 +154,7 @@
     }
 
     /** Removes all screenshot related windows. */
-    fun removeWindows() {
+    override fun removeWindows() {
         screenshotControllers.forEach { (_, screenshotController) ->
             screenshotController.removeWindow()
         }
@@ -151,7 +163,7 @@
     /**
      * Destroys the executor. Afterwards, this class is not expected to work as intended anymore.
      */
-    fun onDestroy() {
+    override fun onDestroy() {
         screenshotControllers.forEach { (_, screenshotController) ->
             screenshotController.onDestroy()
         }
@@ -171,12 +183,12 @@
     }
 
     /** For java compatibility only. see [executeScreenshots] */
-    fun executeScreenshotsAsync(
+    override fun executeScreenshotsAsync(
         screenshotRequest: ScreenshotRequest,
         onSaved: Consumer<Uri>,
         requestCallback: RequestCallback
     ) {
-        mainScope.launch("TakeScreenshotService#executeScreenshotsAsync") {
+        mainScope.launch {
             executeScreenshots(screenshotRequest, { uri -> onSaved.accept(uri) }, requestCallback)
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
index 9cf347b..2187b51 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotService.java
@@ -21,13 +21,11 @@
 
 import static com.android.internal.util.ScreenshotHelper.SCREENSHOT_MSG_PROCESS_COMPLETE;
 import static com.android.internal.util.ScreenshotHelper.SCREENSHOT_MSG_URI;
-import static com.android.systemui.flags.Flags.MULTI_DISPLAY_SCREENSHOT;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_CALLBACK;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS;
 import static com.android.systemui.screenshot.LogConfig.DEBUG_SERVICE;
 import static com.android.systemui.screenshot.LogConfig.logTag;
 import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_CAPTURE_FAILED;
-import static com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER;
 
 import android.annotation.MainThread;
 import android.app.Service;
@@ -50,24 +48,19 @@
 import android.view.Display;
 import android.widget.Toast;
 
-import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.util.ScreenshotRequest;
 import com.android.systemui.dagger.qualifiers.Background;
-import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.res.R;
 
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 
 import javax.inject.Inject;
-import javax.inject.Provider;
 
 public class TakeScreenshotService extends Service {
     private static final String TAG = logTag(TakeScreenshotService.class);
 
-    private final ScreenshotController mScreenshot;
-
     private final UserManager mUserManager;
     private final DevicePolicyManager mDevicePolicyManager;
     private final UiEventLogger mUiEventLogger;
@@ -75,27 +68,20 @@
     private final Handler mHandler;
     private final Context mContext;
     private final @Background Executor mBgExecutor;
-    private final RequestProcessor mProcessor;
-    private final FeatureFlags mFeatureFlags;
+    private final TakeScreenshotExecutor mTakeScreenshotExecutor;
 
+    @SuppressWarnings("deprecation")
     private final BroadcastReceiver mCloseSystemDialogs = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
-            if (ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction()) && mScreenshot != null) {
+            if (ACTION_CLOSE_SYSTEM_DIALOGS.equals(intent.getAction())) {
                 if (DEBUG_DISMISS) {
                     Log.d(TAG, "Received ACTION_CLOSE_SYSTEM_DIALOGS");
                 }
-                if (mFeatureFlags.isEnabled(MULTI_DISPLAY_SCREENSHOT)) {
-                    // TODO(b/295143676): move receiver inside executor when the flag is enabled.
-                    mTakeScreenshotExecutor.get().onCloseSystemDialogsReceived();
-                } else if (!mScreenshot.isPendingSharedTransition()) {
-                    mScreenshot.requestDismissal(SCREENSHOT_DISMISSED_OTHER);
-                }
+                mTakeScreenshotExecutor.onCloseSystemDialogsReceived();
             }
         }
     };
-    private final Provider<TakeScreenshotExecutor> mTakeScreenshotExecutor;
-
 
     /** Informs about coarse grained state of the Controller. */
     public interface RequestCallback {
@@ -111,12 +97,14 @@
     }
 
     @Inject
-    public TakeScreenshotService(ScreenshotController.Factory screenshotControllerFactory,
-            UserManager userManager, DevicePolicyManager devicePolicyManager,
+    public TakeScreenshotService(
+            UserManager userManager,
+            DevicePolicyManager devicePolicyManager,
             UiEventLogger uiEventLogger,
             ScreenshotNotificationsController.Factory notificationsControllerFactory,
-            Context context, @Background Executor bgExecutor, FeatureFlags featureFlags,
-            RequestProcessor processor, Provider<TakeScreenshotExecutor> takeScreenshotExecutor) {
+            Context context,
+            @Background Executor bgExecutor,
+            TakeScreenshotExecutor takeScreenshotExecutor) {
         if (DEBUG_SERVICE) {
             Log.d(TAG, "new " + this);
         }
@@ -127,15 +115,7 @@
         mNotificationsController = notificationsControllerFactory.create(Display.DEFAULT_DISPLAY);
         mContext = context;
         mBgExecutor = bgExecutor;
-        mFeatureFlags = featureFlags;
-        mProcessor = processor;
         mTakeScreenshotExecutor = takeScreenshotExecutor;
-        if (mFeatureFlags.isEnabled(MULTI_DISPLAY_SCREENSHOT)) {
-            mScreenshot = null;
-        } else {
-            mScreenshot = screenshotControllerFactory.create(
-                    Display.DEFAULT_DISPLAY, /* showUIOnExternalDisplay= */ false);
-        }
     }
 
     @Override
@@ -161,11 +141,7 @@
         if (DEBUG_SERVICE) {
             Log.d(TAG, "onUnbind");
         }
-        if (mFeatureFlags.isEnabled(MULTI_DISPLAY_SCREENSHOT)) {
-            mTakeScreenshotExecutor.get().removeWindows();
-        } else {
-            mScreenshot.removeWindow();
-        }
+        mTakeScreenshotExecutor.removeWindows();
         unregisterReceiver(mCloseSystemDialogs);
         return false;
     }
@@ -173,11 +149,7 @@
     @Override
     public void onDestroy() {
         super.onDestroy();
-        if (mFeatureFlags.isEnabled(MULTI_DISPLAY_SCREENSHOT)) {
-            mTakeScreenshotExecutor.get().onDestroy();
-        } else {
-            mScreenshot.onDestroy();
-        }
+        mTakeScreenshotExecutor.onDestroy();
         if (DEBUG_SERVICE) {
             Log.d(TAG, "onDestroy");
         }
@@ -190,6 +162,7 @@
             mReplyTo = replyTo;
         }
 
+        @Override
         public void reportError() {
             reportUri(mReplyTo, null);
             sendComplete(mReplyTo);
@@ -214,7 +187,6 @@
     }
 
     @MainThread
-    @VisibleForTesting
     void handleRequest(ScreenshotRequest request, Consumer<Uri> onSaved,
             RequestCallback callback) {
         // If the storage for this user is locked, we have no place to store
@@ -245,36 +217,9 @@
         }
 
         Log.d(TAG, "Processing screenshot data");
-
-
-        if (mFeatureFlags.isEnabled(MULTI_DISPLAY_SCREENSHOT)) {
-            mTakeScreenshotExecutor.get().executeScreenshotsAsync(request, onSaved, callback);
-            return;
-        }
-        // TODO(b/295143676): Delete the following after the flag is released.
-        try {
-            ScreenshotData screenshotData = ScreenshotData.fromRequest(
-                    request, Display.DEFAULT_DISPLAY);
-            mProcessor.processAsync(screenshotData, (data) ->
-                    dispatchToController(data, onSaved, callback));
-
-        } catch (IllegalStateException e) {
-            Log.e(TAG, "Failed to process screenshot request!", e);
-            logFailedRequest(request);
-            mNotificationsController.notifyScreenshotError(
-                    R.string.screenshot_failed_to_capture_text);
-            callback.reportError();
-        }
+        mTakeScreenshotExecutor.executeScreenshotsAsync(request, onSaved, callback);
     }
 
-    // TODO(b/295143676): Delete this.
-    private void dispatchToController(ScreenshotData screenshot,
-            Consumer<Uri> uriConsumer, RequestCallback callback) {
-        mUiEventLogger.log(ScreenshotEvent.getScreenshotSource(screenshot.getSource()), 0,
-                screenshot.getPackageNameString());
-        Log.d(TAG, "Screenshot request: " + screenshot);
-        mScreenshot.handleScreenshot(screenshot, uriConsumer, callback);
-    }
 
     private void logFailedRequest(ScreenshotRequest request) {
         ComponentName topComponent = request.getTopComponent();
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
index 2ce6d83..6ff0fda 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
@@ -26,21 +26,22 @@
 import com.android.systemui.screenshot.ImageCapture;
 import com.android.systemui.screenshot.ImageCaptureImpl;
 import com.android.systemui.screenshot.LegacyScreenshotViewProxy;
-import com.android.systemui.screenshot.RequestProcessor;
 import com.android.systemui.screenshot.ScreenshotActionsProvider;
 import com.android.systemui.screenshot.ScreenshotPolicy;
 import com.android.systemui.screenshot.ScreenshotPolicyImpl;
-import com.android.systemui.screenshot.ScreenshotProxyService;
-import com.android.systemui.screenshot.ScreenshotRequestProcessor;
 import com.android.systemui.screenshot.ScreenshotShelfViewProxy;
 import com.android.systemui.screenshot.ScreenshotSoundController;
 import com.android.systemui.screenshot.ScreenshotSoundControllerImpl;
 import com.android.systemui.screenshot.ScreenshotSoundProvider;
 import com.android.systemui.screenshot.ScreenshotSoundProviderImpl;
 import com.android.systemui.screenshot.ScreenshotViewProxy;
+import com.android.systemui.screenshot.TakeScreenshotExecutor;
+import com.android.systemui.screenshot.TakeScreenshotExecutorImpl;
 import com.android.systemui.screenshot.TakeScreenshotService;
 import com.android.systemui.screenshot.appclips.AppClipsScreenshotHelperService;
 import com.android.systemui.screenshot.appclips.AppClipsService;
+import com.android.systemui.screenshot.policy.ScreenshotPolicyModule;
+import com.android.systemui.screenshot.proxy.SystemUiProxyModule;
 import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel;
 
 import dagger.Binds;
@@ -52,7 +53,7 @@
 /**
  * Defines injectable resources for Screenshots
  */
-@Module
+@Module(includes = {ScreenshotPolicyModule.class, SystemUiProxyModule.class})
 public abstract class ScreenshotModule {
 
     @Binds
@@ -61,9 +62,9 @@
     abstract Service bindTakeScreenshotService(TakeScreenshotService service);
 
     @Binds
-    @IntoMap
-    @ClassKey(ScreenshotProxyService.class)
-    abstract Service bindScreenshotProxyService(ScreenshotProxyService service);
+    @SysUISingleton
+    abstract TakeScreenshotExecutor bindTakeScreenshotExecutor(
+            TakeScreenshotExecutorImpl impl);
 
     @Binds
     abstract ScreenshotPolicy bindScreenshotPolicyImpl(ScreenshotPolicyImpl impl);
@@ -82,10 +83,6 @@
     abstract Service bindAppClipsService(AppClipsService service);
 
     @Binds
-    abstract ScreenshotRequestProcessor bindScreenshotRequestProcessor(
-            RequestProcessor requestProcessor);
-
-    @Binds
     abstract ScreenshotSoundProvider bindScreenshotSoundProvider(
             ScreenshotSoundProviderImpl screenshotSoundProviderImpl);
 
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/data/model/DisplayContentModel.kt b/packages/SystemUI/src/com/android/systemui/screenshot/data/model/DisplayContentModel.kt
new file mode 100644
index 0000000..837a661
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/data/model/DisplayContentModel.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.data.model
+
+import android.app.ActivityTaskManager.RootTaskInfo
+
+/** Information about the tasks on a display. */
+data class DisplayContentModel(
+    /** The id of the display. */
+    val displayId: Int,
+    /** Information about the current System UI state which can affect capture. */
+    val systemUiState: SystemUiState,
+    /** A list of root tasks on the display, ordered from bottom to top along the z-axis */
+    val rootTasks: List<RootTaskInfo>,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/data/model/ProfileType.kt b/packages/SystemUI/src/com/android/systemui/screenshot/data/model/ProfileType.kt
new file mode 100644
index 0000000..38016ad
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/data/model/ProfileType.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.data.model
+
+/** The profile type of a user. */
+enum class ProfileType {
+    /** The user is not a profile. */
+    NONE,
+    /** Private space user */
+    PRIVATE,
+    /** Managed (work) profile. */
+    WORK,
+    /** Cloned apps user */
+    CLONE,
+    /** Communal profile */
+    COMMUNAL,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/data/model/SystemUiState.kt b/packages/SystemUI/src/com/android/systemui/screenshot/data/model/SystemUiState.kt
new file mode 100644
index 0000000..78be6bd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/data/model/SystemUiState.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.data.model
+
+/** Information about SystemUI state relevant to screenshot policy. */
+data class SystemUiState(val shadeExpanded: Boolean)
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/data/repository/DisplayContentRepository.kt b/packages/SystemUI/src/com/android/systemui/screenshot/data/repository/DisplayContentRepository.kt
new file mode 100644
index 0000000..9c81b32
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/data/repository/DisplayContentRepository.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.screenshot.data.repository
+
+import com.android.systemui.screenshot.data.model.DisplayContentModel
+
+/** Provides information about tasks related to a display. */
+interface DisplayContentRepository {
+    /** Provides information about the tasks and content presented on a given display. */
+    suspend fun getDisplayContent(displayId: Int): DisplayContentModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/data/repository/DisplayContentRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/screenshot/data/repository/DisplayContentRepositoryImpl.kt
new file mode 100644
index 0000000..e9599dc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/data/repository/DisplayContentRepositoryImpl.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.data.repository
+
+import android.annotation.SuppressLint
+import android.app.ActivityTaskManager
+import android.app.IActivityTaskManager
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.screenshot.data.model.DisplayContentModel
+import com.android.systemui.screenshot.data.model.SystemUiState
+import com.android.systemui.screenshot.proxy.SystemUiProxy
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+/**
+ * Implements DisplayTaskRepository using [IActivityTaskManager], along with [ProfileTypeRepository]
+ * and [SystemUiProxy].
+ */
+@SuppressLint("MissingPermission")
+class DisplayContentRepositoryImpl
+@Inject
+constructor(
+    private val atmService: IActivityTaskManager,
+    private val systemUiProxy: SystemUiProxy,
+    @Background private val background: CoroutineDispatcher,
+) : DisplayContentRepository {
+
+    override suspend fun getDisplayContent(displayId: Int): DisplayContentModel {
+        return withContext(background) {
+            val rootTasks = atmService.getAllRootTaskInfosOnDisplay(displayId)
+            toDisplayTasksModel(displayId, rootTasks)
+        }
+    }
+
+    private suspend fun toDisplayTasksModel(
+        displayId: Int,
+        rootTasks: List<ActivityTaskManager.RootTaskInfo>,
+    ): DisplayContentModel {
+        return DisplayContentModel(
+            displayId,
+            SystemUiState(systemUiProxy.isNotificationShadeExpanded()),
+            rootTasks
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/data/repository/ProfileTypeRepository.kt b/packages/SystemUI/src/com/android/systemui/screenshot/data/repository/ProfileTypeRepository.kt
new file mode 100644
index 0000000..2c6e4fe
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/data/repository/ProfileTypeRepository.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.data.repository
+
+import android.annotation.UserIdInt
+import com.android.systemui.screenshot.data.model.ProfileType
+
+/** A facility for checking user profile types. */
+fun interface ProfileTypeRepository {
+    /**
+     * Returns the profile type when [userId] refers to a profile user. If the profile type is
+     * unknown or not a profile user, [ProfileType.NONE] is returned.
+     */
+    suspend fun getProfileType(@UserIdInt userId: Int): ProfileType
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/data/repository/ProfileTypeRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/screenshot/data/repository/ProfileTypeRepositoryImpl.kt
new file mode 100644
index 0000000..42ad21bd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/data/repository/ProfileTypeRepositoryImpl.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+@file:SuppressLint("MissingPermission")
+
+package com.android.systemui.screenshot.data.repository
+
+import android.annotation.SuppressLint
+import android.annotation.UserIdInt
+import android.os.UserManager
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.screenshot.data.model.ProfileType
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withContext
+
+/** Fetches profile types from [UserManager] as needed, caching results for a given user. */
+class ProfileTypeRepositoryImpl
+@Inject
+constructor(
+    private val userManager: UserManager,
+    @Background private val background: CoroutineDispatcher
+) : ProfileTypeRepository {
+    /** Cache to avoid repeated requests to IActivityTaskManager for the same userId */
+    private val cache = mutableMapOf<Int, ProfileType>()
+    private val mutex = Mutex()
+
+    override suspend fun getProfileType(@UserIdInt userId: Int): ProfileType {
+        return mutex.withLock {
+            cache[userId]
+                ?: withContext(background) {
+                        val userType = userManager.getUserInfo(userId).userType
+                        when (userType) {
+                            UserManager.USER_TYPE_PROFILE_MANAGED -> ProfileType.WORK
+                            UserManager.USER_TYPE_PROFILE_PRIVATE -> ProfileType.PRIVATE
+                            UserManager.USER_TYPE_PROFILE_CLONE -> ProfileType.CLONE
+                            UserManager.USER_TYPE_PROFILE_COMMUNAL -> ProfileType.COMMUNAL
+                            else -> ProfileType.NONE
+                        }
+                    }
+                    .also { cache[userId] = it }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/policy/ScreenshotPolicyModule.kt b/packages/SystemUI/src/com/android/systemui/screenshot/policy/ScreenshotPolicyModule.kt
new file mode 100644
index 0000000..bc71ab7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/policy/ScreenshotPolicyModule.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.policy
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.screenshot.ImageCapture
+import com.android.systemui.screenshot.RequestProcessor
+import com.android.systemui.screenshot.ScreenshotPolicy
+import com.android.systemui.screenshot.ScreenshotRequestProcessor
+import com.android.systemui.screenshot.data.repository.DisplayContentRepository
+import com.android.systemui.screenshot.data.repository.DisplayContentRepositoryImpl
+import com.android.systemui.screenshot.data.repository.ProfileTypeRepository
+import com.android.systemui.screenshot.data.repository.ProfileTypeRepositoryImpl
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import javax.inject.Provider
+
+@Module
+interface ScreenshotPolicyModule {
+
+    @Binds
+    @SysUISingleton
+    fun bindProfileTypeRepository(impl: ProfileTypeRepositoryImpl): ProfileTypeRepository
+
+    companion object {
+        @Provides
+        @SysUISingleton
+        fun bindScreenshotRequestProcessor(
+            imageCapture: ImageCapture,
+            policyProvider: Provider<ScreenshotPolicy>,
+        ): ScreenshotRequestProcessor {
+            return RequestProcessor(imageCapture, policyProvider.get())
+        }
+    }
+
+    @Binds
+    @SysUISingleton
+    fun bindDisplayContentRepository(impl: DisplayContentRepositoryImpl): DisplayContentRepository
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/proxy/SystemUiProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/proxy/SystemUiProxy.kt
new file mode 100644
index 0000000..e3eb3c4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/proxy/SystemUiProxy.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.proxy
+
+/**
+ * Provides a mechanism to interact with the main SystemUI process.
+ *
+ * ScreenshotService runs in an isolated process. Because of this, interactions with an outside
+ * component with shared state must be accessed through this proxy to reach the correct instance.
+ *
+ * TODO: Rename and relocate 'ScreenshotProxyService' to this package and remove duplicate clients.
+ */
+interface SystemUiProxy {
+    /** Indicate if the notification shade is "open"... (not in the fully collapsed position) */
+    suspend fun isNotificationShadeExpanded(): Boolean
+
+    /**
+     * Request keyguard dismissal, raising keyguard credential entry if required and waits for
+     * completion.
+     */
+    suspend fun dismissKeyguard()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/proxy/SystemUiProxyClient.kt b/packages/SystemUI/src/com/android/systemui/screenshot/proxy/SystemUiProxyClient.kt
new file mode 100644
index 0000000..dcf58bd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/proxy/SystemUiProxyClient.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.proxy
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+import com.android.internal.infra.ServiceConnector
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.screenshot.IOnDoneCallback
+import com.android.systemui.screenshot.IScreenshotProxy
+import com.android.systemui.screenshot.ScreenshotProxyService
+import javax.inject.Inject
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+import kotlinx.coroutines.CompletableDeferred
+
+private const val TAG = "SystemUiProxy"
+
+/** An implementation of [SystemUiProxy] using [ScreenshotProxyService]. */
+class SystemUiProxyClient @Inject constructor(@Application context: Context) : SystemUiProxy {
+    @SuppressLint("ImplicitSamInstance")
+    private val proxyConnector: ServiceConnector<IScreenshotProxy> =
+        ServiceConnector.Impl(
+            context,
+            Intent(context, ScreenshotProxyService::class.java),
+            Context.BIND_AUTO_CREATE or Context.BIND_WAIVE_PRIORITY or Context.BIND_NOT_VISIBLE,
+            context.userId,
+            IScreenshotProxy.Stub::asInterface
+        )
+
+    override suspend fun isNotificationShadeExpanded(): Boolean = suspendCoroutine { k ->
+        proxyConnector
+            .postForResult { it.isNotificationShadeExpanded }
+            .whenComplete { expanded, error ->
+                error?.also { Log.wtf(TAG, "isNotificationShadeExpanded", it) }
+                k.resume(expanded ?: false)
+            }
+    }
+
+    override suspend fun dismissKeyguard() {
+        val completion = CompletableDeferred<Unit>()
+        val onDoneBinder =
+            object : IOnDoneCallback.Stub() {
+                override fun onDone(success: Boolean) {
+                    completion.complete(Unit)
+                }
+            }
+        if (proxyConnector.run { it.dismissKeyguard(onDoneBinder) }) {
+            completion.await()
+        } else {
+            Log.wtf(TAG, "Keyguard dismissal request failed")
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/proxy/SystemUiProxyModule.kt b/packages/SystemUI/src/com/android/systemui/screenshot/proxy/SystemUiProxyModule.kt
new file mode 100644
index 0000000..4dd5cc4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/proxy/SystemUiProxyModule.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.proxy
+
+import android.app.Service
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.screenshot.ScreenshotProxyService
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+
+@Module
+interface SystemUiProxyModule {
+
+    @Binds
+    @IntoMap
+    @ClassKey(ScreenshotProxyService::class)
+    fun bindScreenshotProxyService(service: ScreenshotProxyService): Service
+
+    @Binds
+    @SysUISingleton
+    fun bindSystemUiProxy(systemUiProxyClient: SystemUiProxyClient): SystemUiProxy
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
index 3169e9c..33cf9ba 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
@@ -26,12 +26,14 @@
 import androidx.compose.ui.platform.ComposeView
 import com.android.compose.theme.PlatformTheme
 import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.communal.dagger.Communal
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.communal.ui.compose.CommunalContainer
 import com.android.systemui.communal.ui.viewmodel.CommunalViewModel
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.res.R
+import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.phone.SystemUIDialogFactory
 import com.android.systemui.util.kotlin.collectFlow
@@ -52,6 +54,7 @@
     private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
     private val shadeInteractor: ShadeInteractor,
     private val powerManager: PowerManager,
+    @Communal private val dataSourceDelegator: SceneDataSourceDelegator,
 ) {
     /** The container view for the hub. This will not be initialized until [initView] is called. */
     private var communalContainerView: View? = null
@@ -125,6 +128,7 @@
                     PlatformTheme {
                         CommunalContainer(
                             viewModel = communalViewModel,
+                            dataSourceDelegator = dataSourceDelegator,
                             dialogFactory = dialogFactory,
                         )
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 1d3e874..0ddea30 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -4977,6 +4977,12 @@
                 return false;
             }
 
+            if (DeviceEntryUdfpsRefactor.isEnabled()
+                    && mAlternateBouncerInteractor.isVisibleState()) {
+                // never send touches to shade if the alternate bouncer is showing
+                return false;
+            }
+
             if (event.getAction() == MotionEvent.ACTION_DOWN) {
                 if (event.getDownTime() == mLastTouchDownTime) {
                     // An issue can occur when swiping down after unlock, where multiple down
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java
index 037dc4d..07836e4 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java
@@ -127,7 +127,9 @@
     @Override
     public void animateCollapseShade(int flags, boolean force, boolean delayed,
             float speedUpFactor) {
-        if (!force && mStatusBarStateController.getState() != StatusBarState.SHADE) {
+        int statusBarState = mStatusBarStateController.getState();
+        if (!force && statusBarState != StatusBarState.SHADE
+                && statusBarState != StatusBarState.SHADE_LOCKED) {
             runPostCollapseActions();
             return;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/HeadsUpStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/HeadsUpStatusBarView.java
index 0715dfc..8d9fab1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/HeadsUpStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/HeadsUpStatusBarView.java
@@ -117,9 +117,9 @@
         mShowingEntry = entry;
 
         if (mShowingEntry != null) {
-            CharSequence text = entry.headsUpStatusBarText;
-            if (entry.isSensitive()) {
-                text = entry.headsUpStatusBarTextPublic;
+            CharSequence text = entry.getHeadsUpStatusBarText().getValue();
+            if (entry.isSensitive().getValue()) {
+                text = entry.getHeadsUpStatusBarTextPublic().getValue();
             }
             mTextView.setText(text);
             mShowingEntry.addOnSensitivityChangedListener(mOnSensitivityChangedListener);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
index fc1dc62..519d719 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt
@@ -396,7 +396,7 @@
             }
             if (view is ExpandableNotificationRow) {
                 // Only drag down on sensitive views, otherwise the ExpandHelper will take this
-                return view.entry.isSensitive
+                return view.entry.isSensitive.value
             }
         }
         return false
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
index 5f0b298..307e702 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java
@@ -41,7 +41,6 @@
 import android.view.ViewParent;
 import android.widget.RemoteViews;
 import android.widget.RemoteViews.InteractionHandler;
-import android.widget.TextView;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -473,25 +472,7 @@
             // if we still didn't find a view that is attached, let's abort.
             return false;
         }
-        int width = view.getWidth();
-        if (view instanceof TextView) {
-            // Center the reveal on the text which might be off-center from the TextView
-            TextView tv = (TextView) view;
-            if (tv.getLayout() != null) {
-                int innerWidth = (int) tv.getLayout().getLineWidth(0);
-                innerWidth += tv.getCompoundPaddingLeft() + tv.getCompoundPaddingRight();
-                width = Math.min(width, innerWidth);
-            }
-        }
-        int cx = view.getLeft() + width / 2;
-        int cy = view.getTop() + view.getHeight() / 2;
-        int w = riv.getWidth();
-        int h = riv.getHeight();
-        int r = Math.max(
-                Math.max(cx + cy, cx + (h - cy)),
-                Math.max((w - cx) + cy, (w - cx) + (h - cy)));
 
-        riv.getController().setRevealParams(new RemoteInputView.RevealParams(cx, cy, r));
         riv.getController().setPendingIntent(pendingIntent);
         riv.getController().setRemoteInput(input);
         riv.getController().setRemoteInputs(inputs);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
index 0fd0555..c29a64e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
@@ -36,6 +36,7 @@
 import android.view.ContextThemeWrapper
 import android.view.View
 import android.view.ViewGroup
+import androidx.annotation.VisibleForTesting
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.settingslib.Utils
 import com.android.systemui.Dumpable
@@ -45,6 +46,7 @@
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.BcSmartspaceConfigPlugin
 import com.android.systemui.plugins.BcSmartspaceDataPlugin
@@ -95,6 +97,7 @@
         private val deviceProvisionedController: DeviceProvisionedController,
         private val bypassController: KeyguardBypassController,
         private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+        private val wakefulnessLifecycle: WakefulnessLifecycle,
         private val dumpManager: DumpManager,
         private val execution: Execution,
         @Main private val uiExecutor: Executor,
@@ -123,7 +126,7 @@
     private val recentSmartspaceData: Deque<List<SmartspaceTarget>> = LinkedList()
 
     // Smartspace can be used on multiple displays, such as when the user casts their screen
-    private var smartspaceViews = mutableSetOf<SmartspaceView>()
+    @VisibleForTesting var smartspaceViews = mutableSetOf<SmartspaceView>()
     private var regionSamplers =
             mutableMapOf<SmartspaceView, RegionSampler>()
 
@@ -272,6 +275,18 @@
             }
         }
 
+    // TODO(b/331451011): Refactor to viewmodel and use interactor pattern.
+    private val wakefulnessLifecycleObserver =
+        object : WakefulnessLifecycle.Observer {
+            override fun onStartedWakingUp() {
+                smartspaceViews.forEach { it.setScreenOn(true) }
+            }
+
+            override fun onFinishedGoingToSleep() {
+                smartspaceViews.forEach { it.setScreenOn(false) }
+            }
+        }
+
     init {
         deviceProvisionedController.addCallback(deviceProvisionedListener)
         dumpManager.registerDumpable(this)
@@ -451,6 +466,7 @@
         configurationController.addCallback(configChangeListener)
         statusBarStateController.addCallback(statusBarStateListener)
         bypassController.registerOnBypassStateChangedListener(bypassStateChangedListener)
+        wakefulnessLifecycle.addObserver(wakefulnessLifecycleObserver)
 
         datePlugin?.registerSmartspaceEventNotifier { e -> session?.notifySmartspaceEvent(e) }
         weatherPlugin?.registerSmartspaceEventNotifier { e -> session?.notifySmartspaceEvent(e) }
@@ -493,6 +509,7 @@
         configurationController.removeCallback(configChangeListener)
         statusBarStateController.removeCallback(statusBarStateListener)
         bypassController.unregisterOnBypassStateChangedListener(bypassStateChangedListener)
+        wakefulnessLifecycle.removeObserver(wakefulnessLifecycleObserver)
         session = null
 
         datePlugin?.registerSmartspaceEventNotifier(null)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index c1dd992..9ce38db 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -76,6 +76,10 @@
 import java.util.List;
 import java.util.Objects;
 
+import kotlinx.coroutines.flow.MutableStateFlow;
+import kotlinx.coroutines.flow.StateFlow;
+import kotlinx.coroutines.flow.StateFlowKt;
+
 /**
  * Represents a notification that the system UI knows about
  *
@@ -150,8 +154,11 @@
     public CharSequence remoteInputTextWhenReset;
     public long lastRemoteInputSent = NOT_LAUNCHED_YET;
     public final ArraySet<Integer> mActiveAppOps = new ArraySet<>(3);
-    public CharSequence headsUpStatusBarText;
-    public CharSequence headsUpStatusBarTextPublic;
+
+    private final MutableStateFlow<CharSequence> mHeadsUpStatusBarText =
+            StateFlowKt.MutableStateFlow(null);
+    private final MutableStateFlow<CharSequence> mHeadsUpStatusBarTextPublic =
+            StateFlowKt.MutableStateFlow(null);
 
     // indicates when this entry's view was first attached to a window
     // this value will reset when the view is completely removed from the shade (ie: filtered out)
@@ -162,8 +169,8 @@
      */
     private boolean hasSentReply;
 
-    private boolean mSensitive = true;
-    private ListenerSet<OnSensitivityChangedListener> mOnSensitivityChangedListeners =
+    private final MutableStateFlow<Boolean> mSensitive = StateFlowKt.MutableStateFlow(true);
+    private final ListenerSet<OnSensitivityChangedListener> mOnSensitivityChangedListeners =
             new ListenerSet<>();
 
     private boolean mPulseSupressed;
@@ -934,6 +941,11 @@
         return Objects.equals(n.category, category);
     }
 
+    /** @see #setSensitive(boolean, boolean)  */
+    public StateFlow<Boolean> isSensitive() {
+        return mSensitive;
+    }
+
     /**
      * Set this notification to be sensitive.
      *
@@ -942,8 +954,8 @@
      */
     public void setSensitive(boolean sensitive, boolean deviceSensitive) {
         getRow().setSensitive(sensitive, deviceSensitive);
-        if (sensitive != mSensitive) {
-            mSensitive = sensitive;
+        if (sensitive != mSensitive.getValue()) {
+            mSensitive.setValue(sensitive);
             for (NotificationEntry.OnSensitivityChangedListener listener :
                     mOnSensitivityChangedListeners) {
                 listener.onSensitivityChanged(this);
@@ -951,10 +963,6 @@
         }
     }
 
-    public boolean isSensitive() {
-        return mSensitive;
-    }
-
     /** Add a listener to be notified when the entry's sensitivity changes. */
     public void addOnSensitivityChangedListener(OnSensitivityChangedListener listener) {
         mOnSensitivityChangedListeners.addIfAbsent(listener);
@@ -965,6 +973,32 @@
         mOnSensitivityChangedListeners.remove(listener);
     }
 
+    /** @see #setHeadsUpStatusBarText(CharSequence) */
+    public StateFlow<CharSequence> getHeadsUpStatusBarText() {
+        return mHeadsUpStatusBarText;
+    }
+
+    /**
+     * Sets the text to be displayed on the StatusBar, when this notification is the top pinned
+     * heads up.
+     */
+    public void setHeadsUpStatusBarText(CharSequence headsUpStatusBarText) {
+        this.mHeadsUpStatusBarText.setValue(headsUpStatusBarText);
+    }
+
+    /** @see #setHeadsUpStatusBarTextPublic(CharSequence) */
+    public StateFlow<CharSequence> getHeadsUpStatusBarTextPublic() {
+        return mHeadsUpStatusBarTextPublic;
+    }
+
+    /**
+     * Sets the text to be displayed on the StatusBar, when this notification is the top pinned
+     * heads up, and its content is sensitive right now.
+     */
+    public void setHeadsUpStatusBarTextPublic(CharSequence headsUpStatusBarTextPublic) {
+        this.mHeadsUpStatusBarTextPublic.setValue(headsUpStatusBarTextPublic);
+    }
+
     public boolean isPulseSuppressed() {
         return mPulseSupressed;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
index a900e45..4ebb699 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt
@@ -39,13 +39,13 @@
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
-import java.util.concurrent.ConcurrentHashMap
-import javax.inject.Inject
-import kotlin.coroutines.CoroutineContext
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
+import java.util.concurrent.ConcurrentHashMap
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
 
 /**
  * Inflates and updates icons associated with notifications
@@ -206,7 +206,7 @@
     private fun getIconDescriptors(entry: NotificationEntry): Pair<StatusBarIcon, StatusBarIcon> {
         val iconDescriptor = getIconDescriptor(entry, redact = false)
         val sensitiveDescriptor =
-            if (entry.isSensitive) {
+            if (entry.isSensitive.value) {
                 getIconDescriptor(entry, redact = true)
             } else {
                 iconDescriptor
@@ -376,7 +376,7 @@
         val isSmallIcon = iconDescriptor.icon.equals(entry.sbn.notification.smallIcon)
         return isImportantConversation(entry) &&
             !isSmallIcon &&
-            (!usedInSensitiveContext || !entry.isSensitive)
+            (!usedInSensitiveContext || !entry.isSensitive.value)
     }
 
     private fun isImportantConversation(entry: NotificationEntry): Boolean {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
index ded635c..31e69c9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
@@ -988,8 +988,8 @@
             }
         }
 
-        entry.headsUpStatusBarText = result.headsUpStatusBarText;
-        entry.headsUpStatusBarTextPublic = result.headsUpStatusBarTextPublic;
+        entry.setHeadsUpStatusBarText(result.headsUpStatusBarText);
+        entry.setHeadsUpStatusBarTextPublic(result.headsUpStatusBarTextPublic);
         Trace.endAsyncSection(APPLY_TRACE_METHOD, System.identityHashCode(row));
         if (endListener != null) {
             endListener.onAsyncInflationFinished(entry);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
index 5a7433d..5ab5857 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationListViewModel.kt
@@ -123,7 +123,10 @@
             // When the shade is closed, the footer is still present in the list, but not visible.
             // This prevents the footer from being shown when a HUN is present, while still allowing
             // the footer to be counted as part of the shade for measurements.
-            shadeInteractor.shadeExpansion.map { it == 0f }.distinctUntilChanged()
+            shadeInteractor.shadeExpansion
+                .map { it == 0f }
+                .flowOn(bgDispatcher)
+                .distinctUntilChanged()
         }
     }
 
@@ -274,5 +277,6 @@
     // TODO(b/325936094) use it for the text displayed in the StatusBar
     fun headsUpRow(key: HeadsUpRowKey): HeadsUpRowViewModel =
         HeadsUpRowViewModel(headsUpNotificationInteractor.headsUpRow(key))
+
     fun elementKeyFor(key: HeadsUpRowKey): Any = headsUpNotificationInteractor.elementKeyFor(key)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index 9df6f93..9f57606 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -445,7 +445,7 @@
         // All transition view models are mututally exclusive, and safe to merge
         val alphaTransitions =
             merge(
-                alternateBouncerToGoneTransitionViewModel.lockscreenAlpha(viewState),
+                alternateBouncerToGoneTransitionViewModel.notificationAlpha(viewState),
                 aodToLockscreenTransitionViewModel.notificationAlpha,
                 aodToOccludedTransitionViewModel.lockscreenAlpha(viewState),
                 dozingToLockscreenTransitionViewModel.lockscreenAlpha,
@@ -455,7 +455,7 @@
                 goneToDreamingTransitionViewModel.lockscreenAlpha,
                 goneToDozingTransitionViewModel.lockscreenAlpha,
                 lockscreenToDreamingTransitionViewModel.lockscreenAlpha,
-                lockscreenToGoneTransitionViewModel.lockscreenAlpha(viewState),
+                lockscreenToGoneTransitionViewModel.notificationAlpha(viewState),
                 lockscreenToOccludedTransitionViewModel.lockscreenAlpha,
                 lockscreenToPrimaryBouncerTransitionViewModel.lockscreenAlpha,
                 occludedToAodTransitionViewModel.lockscreenAlpha,
@@ -589,8 +589,13 @@
             combine(
                 isOnLockscreen,
                 keyguardInteractor.statusBarState,
-            ) { isOnLockscreen, statusBarState ->
-                statusBarState == SHADE_LOCKED || !isOnLockscreen
+                merge(
+                        primaryBouncerToGoneTransitionViewModel.showAllNotifications,
+                        alternateBouncerToGoneTransitionViewModel.showAllNotifications,
+                    )
+                    .onStart { emit(false) }
+            ) { isOnLockscreen, statusBarState, showAllNotifications ->
+                statusBarState == SHADE_LOCKED || !isOnLockscreen || showAllNotifications
             }
 
         return combineTransform(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
index c193783..79ea59c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
@@ -247,7 +247,7 @@
         if (nowExpanded) {
             if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) {
                 mShadeTransitionController.goToLockedShade(clickedEntry.getRow());
-            } else if (clickedEntry.isSensitive()
+            } else if (clickedEntry.isSensitive().getValue()
                     && mDynamicPrivacyController.isInLockedDownShade()) {
                 mStatusBarStateController.setLeaveOpenOnKeyguardHide(true);
                 mActivityStarter.dismissKeyguardThenExecute(() -> false /* dismissAction */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
index 579d43b..1fc7bf4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java
@@ -45,7 +45,6 @@
 import android.view.MotionEvent;
 import android.view.OnReceiveContentListener;
 import android.view.View;
-import android.view.ViewAnimationUtils;
 import android.view.ViewGroup;
 import android.view.ViewRootImpl;
 import android.view.WindowInsets;
@@ -85,9 +84,7 @@
 import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
-import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
 import com.android.systemui.statusbar.phone.LightBarController;
-import com.android.wm.shell.animation.Interpolators;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -132,8 +129,6 @@
     private boolean mColorized;
     private int mLastBackgroundColor;
     private boolean mResetting;
-    @Nullable
-    private RevealParams mRevealParams;
     private Rect mContentBackgroundBounds;
     private boolean mIsAnimatingAppearance = false;
 
@@ -465,20 +460,6 @@
                 if (actionsContainer != null) actionsContainer.setAlpha(0f);
                 animator.start();
 
-            } else if (animate && mRevealParams != null && mRevealParams.radius > 0) {
-                android.animation.Animator reveal = mRevealParams.createCircularHideAnimator(this);
-                reveal.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
-                reveal.setDuration(StackStateAnimator.ANIMATION_DURATION_CLOSE_REMOTE_INPUT);
-                reveal.addListener(new android.animation.AnimatorListenerAdapter() {
-                    @Override
-                    public void onAnimationEnd(android.animation.Animator animation) {
-                        setVisibility(GONE);
-                        if (mWrapper != null) {
-                            mWrapper.setRemoteInputVisible(false);
-                        }
-                    }
-                });
-                reveal.start();
             } else {
                 setVisibility(GONE);
                 if (doAfterDefocus != null) doAfterDefocus.run();
@@ -720,10 +701,6 @@
         mRemoved = true;
     }
 
-    public void setRevealParameters(@Nullable RevealParams revealParams) {
-        mRevealParams = revealParams;
-    }
-
     @Override
     public void dispatchStartTemporaryDetach() {
         super.dispatchStartTemporaryDetach();
@@ -1178,24 +1155,4 @@
         }
 
     }
-
-    public static class RevealParams {
-        final int centerX;
-        final int centerY;
-        final int radius;
-
-        public RevealParams(int centerX, int centerY, int radius) {
-            this.centerX = centerX;
-            this.centerY = centerY;
-            this.radius = radius;
-        }
-
-        android.animation.Animator createCircularHideAnimator(View view) {
-            return ViewAnimationUtils.createCircularReveal(view, centerX, centerY, radius, 0);
-        }
-
-        android.animation.Animator createCircularRevealAnimator(View view) {
-            return ViewAnimationUtils.createCircularReveal(view, centerX, centerY, 0, radius);
-        }
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputViewController.kt
index bfee9ad..f619369 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputViewController.kt
@@ -30,14 +30,13 @@
 import android.util.Log
 import android.view.View
 import com.android.internal.logging.UiEventLogger
-import com.android.systemui.res.R
 import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.res.R
 import com.android.systemui.statusbar.NotificationRemoteInputManager
 import com.android.systemui.statusbar.RemoteInputController
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.collection.NotificationEntry.EditedSuggestionInfo
 import com.android.systemui.statusbar.policy.RemoteInputView.NotificationRemoteInputEvent
-import com.android.systemui.statusbar.policy.RemoteInputView.RevealParams
 import com.android.systemui.statusbar.policy.dagger.RemoteInputViewScope
 import javax.inject.Inject
 
@@ -61,8 +60,6 @@
     /** Other [RemoteInput]s from the notification associated with this Controller. */
     var remoteInputs: Array<RemoteInput>?
 
-    var revealParams: RevealParams?
-
     /**
      * Sets the smart reply that should be inserted in the remote input, or `null` if the user is
      * not editing a smart reply.
@@ -91,7 +88,6 @@
         other.close()
         remoteInput = other.remoteInput
         remoteInputs = other.remoteInputs
-        revealParams = other.revealParams
         pendingIntent = other.pendingIntent
         focus()
     }
@@ -142,14 +138,6 @@
     override var pendingIntent: PendingIntent? = null
     override var remoteInputs: Array<RemoteInput>? = null
 
-    override var revealParams: RevealParams? = null
-        set(value) {
-            field = value
-            if (isBound) {
-                view.setRevealParameters(value)
-            }
-        }
-
     override val isActive: Boolean get() = view.isActive
 
     override fun bind() {
@@ -161,7 +149,6 @@
             view.setHintText(it.label)
             view.setSupportedMimeTypes(it.allowedDataTypes)
         }
-        view.setRevealParameters(revealParams)
 
         view.addOnEditTextFocusChangedListener(onFocusChangeListener)
         view.addOnSendRemoteInputListener(onSendRemoteInputListener)
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt
index dc004f3..d66fe891 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserActionsUtil.kt
@@ -24,50 +24,53 @@
 /** Utilities related to user management actions. */
 object UserActionsUtil {
 
-    /** Returns `true` if it's possible to add a guest user to the device; `false` otherwise. */
+    /**
+     * Returns `true` if it's possible for the given user to add a guest user to the device; `false`
+     * otherwise.
+     */
     fun canCreateGuest(
         manager: UserManager,
         repository: UserRepository,
         isUserSwitcherEnabled: Boolean,
-        isAddUsersFromLockScreenEnabled: Boolean,
+        canAddUsersWhenLockedOrDeviceUnlocked: Boolean,
     ): Boolean {
-        if (!isUserSwitcherEnabled) {
-            return false
-        }
-
-        return currentUserCanCreateUsers(manager, repository) ||
-            anyoneCanCreateUsers(manager, isAddUsersFromLockScreenEnabled)
+        return canAddMoreUsers(
+            manager,
+            repository,
+            isUserSwitcherEnabled,
+            canAddUsersWhenLockedOrDeviceUnlocked,
+            UserManager.USER_TYPE_FULL_GUEST
+        )
     }
 
-    /** Returns `true` if it's possible to add a user to the device; `false` otherwise. */
+    /**
+     * Returns `true` if it's possible for the given user to add a user to the device; `false`
+     * otherwise.
+     */
     fun canCreateUser(
         manager: UserManager,
         repository: UserRepository,
         isUserSwitcherEnabled: Boolean,
-        isAddUsersFromLockScreenEnabled: Boolean,
+        canAddUsersWhenLockedOrDeviceUnlocked: Boolean,
     ): Boolean {
-        if (!isUserSwitcherEnabled) {
-            return false
-        }
-
-        if (
-            !currentUserCanCreateUsers(manager, repository) &&
-                !anyoneCanCreateUsers(manager, isAddUsersFromLockScreenEnabled)
-        ) {
-            return false
-        }
-
-        return manager.canAddMoreUsers(UserManager.USER_TYPE_FULL_SECONDARY)
+        return canAddMoreUsers(
+            manager,
+            repository,
+            isUserSwitcherEnabled,
+            canAddUsersWhenLockedOrDeviceUnlocked,
+            UserManager.USER_TYPE_FULL_SECONDARY
+        )
     }
 
     /**
-     * Returns `true` if it's possible to add a supervised user to the device; `false` otherwise.
+     * Returns `true` if it's possible to add a supervised user to the device given the current
+     * user; false` otherwise.
      */
     fun canCreateSupervisedUser(
         manager: UserManager,
         repository: UserRepository,
         isUserSwitcherEnabled: Boolean,
-        isAddUsersFromLockScreenEnabled: Boolean,
+        canAddUsersWhenLockedOrDeviceUnlocked: Boolean,
         supervisedUserPackageName: String?
     ): Boolean {
         if (supervisedUserPackageName.isNullOrEmpty()) {
@@ -78,17 +81,30 @@
             manager,
             repository,
             isUserSwitcherEnabled,
-            isAddUsersFromLockScreenEnabled
+            canAddUsersWhenLockedOrDeviceUnlocked
         )
     }
 
-    fun canManageUsers(
+    fun canManageUsers(repository: UserRepository, isUserSwitcherEnabled: Boolean): Boolean {
+        return isUserSwitcherEnabled && repository.getSelectedUserInfo().isAdmin
+    }
+
+    /**
+     * Returns `true` if it's possible to add a user to the device for the given user type; false
+     * otherwise.
+     */
+    private fun canAddMoreUsers(
+        manager: UserManager,
         repository: UserRepository,
         isUserSwitcherEnabled: Boolean,
-        isAddUsersFromLockScreenEnabled: Boolean,
+        canAddUsersWhenLockedOrDeviceUnlocked: Boolean,
+        userType: String
     ): Boolean {
-        return isUserSwitcherEnabled &&
-            (repository.getSelectedUserInfo().isAdmin || isAddUsersFromLockScreenEnabled)
+        if (!isUserSwitcherEnabled || !canAddUsersWhenLockedOrDeviceUnlocked) {
+            return false
+        }
+
+        return currentUserCanCreateUsers(manager, repository) && manager.canAddMoreUsers(userType)
     }
 
     /**
@@ -96,28 +112,15 @@
      */
     private fun currentUserCanCreateUsers(
         manager: UserManager,
-        repository: UserRepository,
+        repository: UserRepository
     ): Boolean {
         val currentUser = repository.getSelectedUserInfo()
         if (!currentUser.isAdmin && currentUser.id != UserHandle.USER_SYSTEM) {
             return false
         }
-
-        return systemCanCreateUsers(manager)
-    }
-
-    /** Returns `true` if the system can add users to the device; `false` otherwise. */
-    private fun systemCanCreateUsers(
-        manager: UserManager,
-    ): Boolean {
-        return !manager.hasBaseUserRestriction(UserManager.DISALLOW_ADD_USER, UserHandle.SYSTEM)
-    }
-
-    /** Returns `true` if it's allowed to add users to the device at all; `false` otherwise. */
-    private fun anyoneCanCreateUsers(
-        manager: UserManager,
-        isAddUsersFromLockScreenEnabled: Boolean,
-    ): Boolean {
-        return systemCanCreateUsers(manager) && isAddUsersFromLockScreenEnabled
+        return !manager.hasUserRestrictionForUser(
+            UserManager.DISALLOW_ADD_USER,
+            UserHandle.of(currentUser.id)
+        ) && !manager.hasUserRestrictionForUser(UserManager.DISALLOW_ADD_USER, UserHandle.SYSTEM)
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt
index a122311..382bc03 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractor.kt
@@ -170,7 +170,8 @@
                 keyguardInteractor.isKeyguardShowing,
             ) { _, userInfos, settings, isDeviceLocked ->
                 buildList {
-                    if (!isDeviceLocked || settings.isAddUsersFromLockscreen) {
+                    val canAccessUserSwitcher = !isDeviceLocked || settings.isAddUsersFromLockscreen
+                    if (canAccessUserSwitcher) {
                         // The device is locked and our setting to allow actions that add users
                         // from the lock-screen is not enabled. We can finish building the list
                         // here.
@@ -194,7 +195,10 @@
                             when (it) {
                                 UserActionModel.ENTER_GUEST_MODE -> {
                                     val hasGuestUser = userInfos.any { it.isGuest }
-                                    if (!hasGuestUser && canCreateGuestUser(settings)) {
+                                    if (
+                                        !hasGuestUser &&
+                                            canCreateGuestUser(settings, canAccessUserSwitcher)
+                                    ) {
                                         add(UserActionModel.ENTER_GUEST_MODE)
                                     }
                                 }
@@ -204,7 +208,7 @@
                                             manager,
                                             repository,
                                             settings.isUserSwitcherEnabled,
-                                            settings.isAddUsersFromLockscreen,
+                                            canAccessUserSwitcher
                                         )
 
                                     if (canCreateUsers) {
@@ -217,7 +221,7 @@
                                             manager,
                                             repository,
                                             settings.isUserSwitcherEnabled,
-                                            settings.isAddUsersFromLockscreen,
+                                            canAccessUserSwitcher,
                                             supervisedUserPackageName,
                                         )
                                     ) {
@@ -229,11 +233,7 @@
                         }
                     }
                     if (
-                        UserActionsUtil.canManageUsers(
-                            repository,
-                            settings.isUserSwitcherEnabled,
-                            settings.isAddUsersFromLockscreen,
-                        )
+                        UserActionsUtil.canManageUsers(repository, settings.isUserSwitcherEnabled)
                     ) {
                         add(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT)
                     }
@@ -820,13 +820,16 @@
         )
     }
 
-    private fun canCreateGuestUser(settings: UserSwitcherSettingsModel): Boolean {
+    private fun canCreateGuestUser(
+        settings: UserSwitcherSettingsModel,
+        canAccessUserSwitcher: Boolean
+    ): Boolean {
         return guestUserInteractor.isGuestUserAutoCreated ||
             UserActionsUtil.canCreateGuest(
                 manager,
                 repository,
                 settings.isUserSwitcherEnabled,
-                settings.isAddUsersFromLockscreen,
+                canAccessUserSwitcher,
             )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModel.kt
index 0207d6e..04d7b1f 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModel.kt
@@ -36,10 +36,15 @@
     }
 
     fun onSettingsClicked() {
-        activityStarter.startActivity(
-            Intent(Settings.ACTION_SOUND_SETTINGS)
-                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT),
-            true,
-        ) { volumePanelViewModel.dismissPanel() }
+        activityStarter.startActivityDismissingKeyguard(
+            /* intent = */ Intent(Settings.ACTION_SOUND_SETTINGS),
+            /* onlyProvisioned = */ false,
+            /* dismissShade = */ true,
+            /* disallowEnterPictureInPictureWhileLaunching = */ false,
+            /* callback = */ { volumePanelViewModel.dismissPanel() },
+            /* flags = */ Intent.FLAG_ACTIVITY_REORDER_TO_FRONT,
+            /* animationController = */ null,
+            /* userHandle = */ null,
+        )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
index 3242c28..57b5d57 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
@@ -98,7 +98,7 @@
         }
     }
 
-    private fun AudioStreamModel.toState(
+    private suspend fun AudioStreamModel.toState(
         isEnabled: Boolean,
         ringerMode: RingerMode,
     ): State {
@@ -116,7 +116,7 @@
             isEnabled = isEnabled,
             a11yStep = volumeRange.step,
             audioStreamModel = this,
-            isMutable = audioVolumeInteractor.isMutable(audioStream),
+            isMutable = audioVolumeInteractor.isAffectedByMute(audioStream),
         )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java
new file mode 100644
index 0000000..12f334b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/FullscreenMagnificationControllerTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.accessibility;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.SurfaceControlViewHost;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
+import android.window.InputTransferToken;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.function.Supplier;
+
+@SmallTest
+@TestableLooper.RunWithLooper
+@RunWith(AndroidTestingRunner.class)
+public class FullscreenMagnificationControllerTest extends SysuiTestCase {
+
+    private FullscreenMagnificationController mFullscreenMagnificationController;
+    private SurfaceControlViewHost mSurfaceControlViewHost;
+
+    @Before
+    public void setUp() {
+        getInstrumentation().runOnMainSync(() -> mSurfaceControlViewHost =
+                new SurfaceControlViewHost(mContext, mContext.getDisplay(),
+                        new InputTransferToken(), "FullscreenMagnification"));
+
+        Supplier<SurfaceControlViewHost> scvhSupplier = () -> mSurfaceControlViewHost;
+
+        mFullscreenMagnificationController = new FullscreenMagnificationController(
+                mContext,
+                mContext.getSystemService(AccessibilityManager.class),
+                mContext.getSystemService(WindowManager.class),
+                scvhSupplier);
+    }
+
+    @After
+    public void tearDown() {
+        getInstrumentation().runOnMainSync(
+                () -> mFullscreenMagnificationController
+                        .onFullscreenMagnificationActivationChanged(false));
+    }
+
+    @Test
+    public void onFullscreenMagnificationActivationChange_activated_visibleBorder() {
+        getInstrumentation().runOnMainSync(
+                () -> mFullscreenMagnificationController
+                        .onFullscreenMagnificationActivationChanged(true)
+        );
+
+        // Wait for Rects updated.
+        waitForIdleSync();
+        assertThat(mSurfaceControlViewHost.getView().isVisibleToUser()).isTrue();
+    }
+
+    @Test
+    public void onFullscreenMagnificationActivationChange_deactivated_invisibleBorder() {
+        getInstrumentation().runOnMainSync(
+                () -> {
+                    mFullscreenMagnificationController
+                            .onFullscreenMagnificationActivationChanged(true);
+                    mFullscreenMagnificationController
+                            .onFullscreenMagnificationActivationChanged(false);
+                }
+        );
+
+        assertThat(mSurfaceControlViewHost.getView()).isNull();
+    }
+
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java
index bd49927..41d5d5d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/IMagnificationConnectionTest.java
@@ -71,6 +71,8 @@
     @Mock
     private WindowMagnificationController mWindowMagnificationController;
     @Mock
+    private FullscreenMagnificationController mFullscreenMagnificationController;
+    @Mock
     private MagnificationSettingsController mMagnificationSettingsController;
     @Mock
     private ModeSwitchesController mModeSwitchesController;
@@ -105,6 +107,9 @@
         mMagnification.mWindowMagnificationControllerSupplier =
                 new FakeWindowMagnificationControllerSupplier(
                         mContext.getSystemService(DisplayManager.class));
+        mMagnification.mFullscreenMagnificationControllerSupplier =
+                new FakeFullscreenMagnificationControllerSupplier(
+                        mContext.getSystemService(DisplayManager.class));
         mMagnification.mMagnificationSettingsSupplier = new FakeSettingsSupplier(
                 mContext.getSystemService(DisplayManager.class));
 
@@ -124,6 +129,15 @@
     }
 
     @Test
+    public void onFullscreenMagnificationActivationChanged_passThrough() throws RemoteException {
+        mIMagnificationConnection.onFullscreenMagnificationActivationChanged(TEST_DISPLAY, true);
+        waitForIdleSync();
+
+        verify(mFullscreenMagnificationController)
+                .onFullscreenMagnificationActivationChanged(eq(true));
+    }
+
+    @Test
     public void disableWindowMagnification_deleteWindowMagnification() throws RemoteException {
         mIMagnificationConnection.disableWindowMagnification(TEST_DISPLAY,
                 mAnimationCallback);
@@ -215,6 +229,20 @@
         }
     }
 
+
+    private class FakeFullscreenMagnificationControllerSupplier extends
+            DisplayIdIndexSupplier<FullscreenMagnificationController> {
+
+        FakeFullscreenMagnificationControllerSupplier(DisplayManager displayManager) {
+            super(displayManager);
+        }
+
+        @Override
+        protected FullscreenMagnificationController createInstance(Display display) {
+            return mFullscreenMagnificationController;
+        }
+    }
+
     private class FakeSettingsSupplier extends
             DisplayIdIndexSupplier<MagnificationSettingsController> {
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackAnimationSpecTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackAnimationSpecTest.kt
index 58011eb..190babd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackAnimationSpecTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/back/BackAnimationSpecTest.kt
@@ -27,7 +27,7 @@
         val maxY = 14.0f
         val minScale = 0.9f
 
-        val backAnimationSpec = BackAnimationSpec.floatingSystemSurfacesForSysUi(displayMetrics)
+        val backAnimationSpec = BackAnimationSpec.floatingSystemSurfacesForSysUi { displayMetrics }
 
         assertBackTransformation(
             backAnimationSpec = backAnimationSpec,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtensionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtensionTest.kt
index f5c9bef..314abda 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtensionTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/animation/back/OnBackAnimationCallbackExtensionTest.kt
@@ -30,7 +30,7 @@
 
     private val onBackAnimationCallback =
         onBackAnimationCallbackFrom(
-            backAnimationSpec = BackAnimationSpec.floatingSystemSurfacesForSysUi(displayMetrics),
+            backAnimationSpec = BackAnimationSpec.floatingSystemSurfacesForSysUi { displayMetrics },
             displayMetrics = displayMetrics,
             onBackProgressed = onBackProgress,
             onBackStarted = onBackStart,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt
index 51828c9..6ebda4d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardUnlockAnimationControllerTest.kt
@@ -14,6 +14,7 @@
 import android.view.SyncRtSurfaceTransactionApplier
 import android.view.View
 import android.view.ViewRootImpl
+import android.view.WindowManager
 import androidx.test.filters.SmallTest
 import com.android.keyguard.KeyguardViewController
 import com.android.systemui.Flags
@@ -52,6 +53,8 @@
     private lateinit var keyguardUnlockAnimationController: KeyguardUnlockAnimationController
 
     @Mock
+    private lateinit var windowManager: WindowManager
+    @Mock
     private lateinit var keyguardViewMediator: KeyguardViewMediator
     @Mock
     private lateinit var keyguardStateController: KeyguardStateController
@@ -99,7 +102,8 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
         keyguardUnlockAnimationController = KeyguardUnlockAnimationController(
-            context, keyguardStateController, { keyguardViewMediator }, keyguardViewController,
+            windowManager, context.resources,
+            keyguardStateController, { keyguardViewMediator }, keyguardViewController,
             featureFlags, { biometricUnlockController }, statusBarStateController,
             notificationShadeWindowController, powerManager, wallpaperManager
         )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt
index 7bef01a..3926f92 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractorTest.kt
@@ -48,6 +48,7 @@
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
 import com.android.systemui.power.domain.interactor.powerInteractor
@@ -73,13 +74,17 @@
         }
 
     private val testScope = kosmos.testScope
-    private val underTest = kosmos.fromAodTransitionInteractor
+    private lateinit var underTest: FromAodTransitionInteractor
 
-    private val powerInteractor = kosmos.powerInteractor
-    private val transitionRepository = kosmos.fakeKeyguardTransitionRepository
+    private lateinit var powerInteractor: PowerInteractor
+    private lateinit var transitionRepository: FakeKeyguardTransitionRepository
 
     @Before
     fun setup() {
+        powerInteractor = kosmos.powerInteractor
+        transitionRepository = kosmos.fakeKeyguardTransitionRepository
+        underTest = kosmos.fromAodTransitionInteractor
+
         underTest.start()
 
         // Transition to AOD and set the power interactor asleep.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
index 258dbf3..cded2a4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
@@ -51,6 +51,7 @@
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat
 import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.domain.interactor.PowerInteractor
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
 import com.android.systemui.power.domain.interactor.powerInteractor
@@ -77,13 +78,17 @@
         }
 
     private val testScope = kosmos.testScope
-    private val underTest = kosmos.fromDozingTransitionInteractor
+    private lateinit var underTest: FromDozingTransitionInteractor
 
-    private val powerInteractor = kosmos.powerInteractor
-    private val transitionRepository = kosmos.fakeKeyguardTransitionRepository
+    private lateinit var powerInteractor: PowerInteractor
+    private lateinit var transitionRepository: FakeKeyguardTransitionRepository
 
     @Before
     fun setup() {
+        powerInteractor = kosmos.powerInteractor
+        transitionRepository = kosmos.fakeKeyguardTransitionRepository
+        underTest = kosmos.fromDozingTransitionInteractor
+
         underTest.start()
 
         // Transition to DOZING and set the power interactor asleep.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/repository/IconTilesRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/repository/IconTilesRepositoryImplTest.kt
new file mode 100644
index 0000000..8cc3a85
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/domain/repository/IconTilesRepositoryImplTest.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.qs.panels.domain.repository
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.qs.panels.data.repository.IconTilesRepositoryImpl
+import com.android.systemui.qs.pipeline.shared.TileSpec
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class IconTilesRepositoryImplTest : SysuiTestCase() {
+
+    private val underTest = IconTilesRepositoryImpl()
+
+    @Test
+    fun iconTilesSpecsIsValid() = runTest {
+        val tilesSpecs by collectLastValue(underTest.iconTilesSpecs)
+        assertThat(tilesSpecs).isEqualTo(ICON_ONLY_TILES_SPECS)
+    }
+
+    companion object {
+        private val ICON_ONLY_TILES_SPECS =
+            setOf(
+                TileSpec.create("airplane"),
+                TileSpec.create("battery"),
+                TileSpec.create("cameratoggle"),
+                TileSpec.create("cast"),
+                TileSpec.create("color_correction"),
+                TileSpec.create("inversion"),
+                TileSpec.create("saver"),
+                TileSpec.create("dnd"),
+                TileSpec.create("flashlight"),
+                TileSpec.create("location"),
+                TileSpec.create("mictoggle"),
+                TileSpec.create("nfc"),
+                TileSpec.create("night"),
+                TileSpec.create("rotation")
+            )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HearingDevicesTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HearingDevicesTileTest.java
new file mode 100644
index 0000000..326df5c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/HearingDevicesTileTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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.qs.tiles;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.verify;
+
+import android.content.Intent;
+import android.os.Handler;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.provider.Settings;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.systemui.Flags;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.classifier.FalsingManagerFake;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.qs.QSHost;
+import com.android.systemui.qs.QsEventLogger;
+import com.android.systemui.qs.logging.QSLogger;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+/** Tests for {@link HearingDevicesTile}. */
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@SmallTest
+public class HearingDevicesTileTest extends SysuiTestCase {
+
+    @Rule
+    public MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock
+    private QSHost mHost;
+    @Mock
+    private QsEventLogger mUiEventLogger;
+    @Mock
+    private MetricsLogger mMetricsLogger;
+    @Mock
+    private StatusBarStateController mStatusBarStateController;
+    @Mock
+    private ActivityStarter mActivityStarter;
+    @Mock
+    private QSLogger mQSLogger;
+
+    private TestableLooper mTestableLooper;
+    private HearingDevicesTile mTile;
+
+    @Before
+    public void setUp() throws Exception {
+        mTestableLooper = TestableLooper.get(this);
+
+        mTile = new HearingDevicesTile(
+                mHost,
+                mUiEventLogger,
+                mTestableLooper.getLooper(),
+                new Handler(mTestableLooper.getLooper()),
+                new FalsingManagerFake(),
+                mMetricsLogger,
+                mStatusBarStateController,
+                mActivityStarter,
+                mQSLogger);
+
+        mTile.initialize();
+        mTestableLooper.processAllMessages();
+    }
+
+    @After
+    public void tearDown() {
+        mTile.destroy();
+        mTestableLooper.processAllMessages();
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_HEARING_AIDS_QS_TILE_DIALOG)
+    public void isAvailable_flagEnabled_true() {
+        assertThat(mTile.isAvailable()).isTrue();
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_HEARING_AIDS_QS_TILE_DIALOG)
+    public void isAvailable_flagDisabled_false() {
+        assertThat(mTile.isAvailable()).isFalse();
+    }
+
+    @Test
+    public void longClick_expectedAction() {
+        mTile.longClick(null);
+        mTestableLooper.processAllMessages();
+
+        ArgumentCaptor<Intent> IntentCaptor = ArgumentCaptor.forClass(Intent.class);
+        verify(mActivityStarter).postStartActivityDismissingKeyguard(IntentCaptor.capture(),
+                anyInt(), any());
+        assertThat(IntentCaptor.getValue().getAction()).isEqualTo(
+                Settings.ACTION_HEARING_DEVICES_SETTINGS);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
index 7e41745..0847f01 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/RequestProcessorTest.kt
@@ -30,8 +30,6 @@
 import com.android.internal.util.ScreenshotRequest
 import com.android.systemui.screenshot.ScreenshotPolicy.DisplayContentInfo
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.runBlocking
 import org.junit.Assert
 import org.junit.Test
@@ -44,35 +42,8 @@
     private val component = ComponentName("android.test", "android.test.Component")
     private val bounds = Rect(25, 25, 75, 75)
 
-    private val scope = CoroutineScope(Dispatchers.Unconfined)
     private val policy = FakeScreenshotPolicy()
 
-    /** Tests the Java-compatible function wrapper, ensures callback is invoked. */
-    @Test
-    fun testProcessAsync_ScreenshotData() {
-        val request =
-            ScreenshotData.fromRequest(
-                ScreenshotRequest.Builder(TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_KEY_OTHER)
-                    .setBitmap(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888))
-                    .build()
-            )
-        val processor = RequestProcessor(imageCapture, policy, scope)
-
-        var result: ScreenshotData? = null
-        var callbackCount = 0
-        val callback: (ScreenshotData) -> Unit = { processedRequest: ScreenshotData ->
-            result = processedRequest
-            callbackCount++
-        }
-
-        // runs synchronously, using Unconfined Dispatcher
-        processor.processAsync(request, callback)
-
-        // Callback invoked once returning the same request (no changes)
-        assertThat(callbackCount).isEqualTo(1)
-        assertThat(result).isEqualTo(request)
-    }
-
     @Test
     fun testFullScreenshot() = runBlocking {
         // Indicate that the primary content belongs to a normal user
@@ -84,7 +55,7 @@
 
         val request =
             ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_OTHER).build()
-        val processor = RequestProcessor(imageCapture, policy, scope)
+        val processor = RequestProcessor(imageCapture, policy)
 
         val processedData = processor.process(ScreenshotData.fromRequest(request))
 
@@ -109,7 +80,7 @@
 
         val request =
             ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER).build()
-        val processor = RequestProcessor(imageCapture, policy, scope)
+        val processor = RequestProcessor(imageCapture, policy)
 
         val processedData = processor.process(ScreenshotData.fromRequest(request))
 
@@ -136,7 +107,7 @@
 
         val request =
             ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER).build()
-        val processor = RequestProcessor(imageCapture, policy, scope)
+        val processor = RequestProcessor(imageCapture, policy)
 
         Assert.assertThrows(IllegalStateException::class.java) {
             runBlocking { processor.process(ScreenshotData.fromRequest(request)) }
@@ -146,7 +117,7 @@
     @Test
     fun testProvidedImageScreenshot() = runBlocking {
         val bounds = Rect(50, 50, 150, 150)
-        val processor = RequestProcessor(imageCapture, policy, scope)
+        val processor = RequestProcessor(imageCapture, policy)
 
         policy.setManagedProfile(USER_ID, false)
 
@@ -171,7 +142,7 @@
     @Test
     fun testProvidedImageScreenshot_managedProfile() = runBlocking {
         val bounds = Rect(50, 50, 150, 150)
-        val processor = RequestProcessor(imageCapture, policy, scope)
+        val processor = RequestProcessor(imageCapture, policy)
 
         // Indicate that the screenshot belongs to a manged profile
         policy.setManagedProfile(USER_ID, true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
index 0baee5d..c900463 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
@@ -57,7 +57,7 @@
     private val eventLogger = UiEventLoggerFake()
 
     private val screenshotExecutor =
-        TakeScreenshotExecutor(
+        TakeScreenshotExecutorImpl(
             controllerFactory,
             fakeDisplayRepository,
             testScope,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt
index f3809aa..0776aa7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotServiceTest.kt
@@ -20,134 +20,97 @@
 import android.app.admin.DevicePolicyResources.Strings.SystemUi.SCREENSHOT_BLOCKED_BY_ADMIN
 import android.app.admin.DevicePolicyResourcesManager
 import android.content.ComponentName
+import android.net.Uri
 import android.os.UserHandle
 import android.os.UserManager
 import android.testing.AndroidTestingRunner
-import android.view.Display
 import android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_OTHER
 import android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN
-import androidx.test.filters.SmallTest
 import com.android.internal.logging.testing.UiEventLoggerFake
 import com.android.internal.util.ScreenshotRequest
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.flags.Flags.MULTI_DISPLAY_SCREENSHOT
 import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_CAPTURE_FAILED
 import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_REQUESTED_KEY_OTHER
 import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback
-import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.eq
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
 import java.util.function.Consumer
+import kotlinx.coroutines.runBlocking
 import org.junit.Assert.assertEquals
-import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.ArgumentMatchers.isNull
-import org.mockito.Mockito.clearInvocations
-import org.mockito.Mockito.doAnswer
-import org.mockito.Mockito.doThrow
-import org.mockito.Mockito.times
-import org.mockito.Mockito.verify
-import org.mockito.Mockito.verifyZeroInteractions
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
 
 @RunWith(AndroidTestingRunner::class)
-@SmallTest
 class TakeScreenshotServiceTest : SysuiTestCase() {
 
-    private val application = mock<Application>()
-    private val controller = mock<ScreenshotController>()
-    private val controllerFactory = mock<ScreenshotController.Factory>()
-    private val takeScreenshotExecutor = mock<TakeScreenshotExecutor>()
-    private val userManager = mock<UserManager>()
-    private val requestProcessor = mock<RequestProcessor>()
-    private val devicePolicyManager = mock<DevicePolicyManager>()
-    private val devicePolicyResourcesManager = mock<DevicePolicyResourcesManager>()
-    private val notificationsControllerFactory = mock<ScreenshotNotificationsController.Factory>()
+    private val userManager = mock<UserManager> { on { isUserUnlocked } doReturn (true) }
+
+    private val devicePolicyResourcesManager =
+        mock<DevicePolicyResourcesManager> {
+            on { getString(eq(SCREENSHOT_BLOCKED_BY_ADMIN), /* defaultStringLoader= */ any()) }
+                .doReturn("SCREENSHOT_BLOCKED_BY_ADMIN")
+        }
+
+    private val devicePolicyManager =
+        mock<DevicePolicyManager> {
+            on { resources } doReturn (devicePolicyResourcesManager)
+            on { getScreenCaptureDisabled(/* admin= */ isNull(), eq(UserHandle.USER_ALL)) }
+                .doReturn(false)
+        }
+
     private val notificationsController = mock<ScreenshotNotificationsController>()
-    private val callback = mock<RequestCallback>()
+    private val notificationsControllerFactory =
+        ScreenshotNotificationsController.Factory { notificationsController }
 
+    private val executor = FakeScreenshotExecutor()
+    private val callback = FakeRequestCallback()
     private val eventLogger = UiEventLoggerFake()
-    private val flags = FakeFeatureFlags()
     private val topComponent = ComponentName(mContext, TakeScreenshotServiceTest::class.java)
 
-    private lateinit var service: TakeScreenshotService
-
-    @Before
-    fun setUp() {
-        flags.set(MULTI_DISPLAY_SCREENSHOT, false)
-        whenever(devicePolicyManager.resources).thenReturn(devicePolicyResourcesManager)
-        whenever(
-                devicePolicyManager.getScreenCaptureDisabled(
-                    /* admin component (null: any admin) */ isNull(),
-                    eq(UserHandle.USER_ALL)
-                )
-            )
-            .thenReturn(false)
-        whenever(userManager.isUserUnlocked).thenReturn(true)
-        whenever(controllerFactory.create(any(), any())).thenReturn(controller)
-        whenever(notificationsControllerFactory.create(any())).thenReturn(notificationsController)
-
-        // Stub request processor as a synchronous no-op for tests with the flag enabled
-        doAnswer {
-                val request: ScreenshotData = it.getArgument(0) as ScreenshotData
-                val consumer: Consumer<ScreenshotData> = it.getArgument(1)
-                consumer.accept(request)
-            }
-            .whenever(requestProcessor)
-            .processAsync(/* screenshot= */ any(ScreenshotData::class.java), /* callback= */ any())
-
-        service = createService()
-    }
-
     @Test
     fun testServiceLifecycle() {
+        val service = createService()
         service.onCreate()
         service.onBind(null /* unused: Intent */)
+        assertThat(executor.windowsPresent).isTrue()
 
         service.onUnbind(null /* unused: Intent */)
-        verify(controller, times(1)).removeWindow()
+        assertThat(executor.windowsPresent).isFalse()
 
         service.onDestroy()
-        verify(controller, times(1)).onDestroy()
+        assertThat(executor.destroyed).isTrue()
     }
 
     @Test
     fun takeScreenshotFullscreen() {
+        val service = createService()
+
         val request =
             ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER)
                 .setTopComponent(topComponent)
                 .build()
 
         service.handleRequest(request, { /* onSaved */}, callback)
+        assertWithMessage("request received by executor").that(executor.requestReceived).isNotNull()
 
-        verify(controller, times(1))
-            .handleScreenshot(
-                eq(ScreenshotData.fromRequest(request, Display.DEFAULT_DISPLAY)),
-                /* onSavedListener = */ any(),
-                /* requestCallback = */ any()
-            )
-
-        assertEquals("Expected one UiEvent", eventLogger.numLogs(), 1)
-        val logEvent = eventLogger.get(0)
-
-        assertEquals(
-            "Expected SCREENSHOT_REQUESTED UiEvent",
-            logEvent.eventId,
-            SCREENSHOT_REQUESTED_KEY_OTHER.id
-        )
-        assertEquals(
-            "Expected supplied package name",
-            topComponent.packageName,
-            eventLogger.get(0).packageName
-        )
+        assertWithMessage("request received by executor")
+            .that(ScreenshotData.fromRequest(executor.requestReceived!!))
+            .isEqualTo(ScreenshotData.fromRequest(request))
     }
 
     @Test
     fun takeScreenshotFullscreen_userLocked() {
-        whenever(userManager.isUserUnlocked).thenReturn(false)
+        val service = createService()
+        whenever(userManager.isUserUnlocked).doReturn(false)
 
         val request =
             ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER)
@@ -157,47 +120,41 @@
         service.handleRequest(request, { /* onSaved */}, callback)
 
         verify(notificationsController, times(1)).notifyScreenshotError(anyInt())
-        verify(callback, times(1)).reportError()
-        verifyZeroInteractions(controller)
 
-        assertEquals("Expected two UiEvents", 2, eventLogger.numLogs())
+        assertWithMessage("callback errorReported").that(callback.errorReported).isTrue()
+
+        assertWithMessage("UiEvent count").that(eventLogger.numLogs()).isEqualTo(2)
+
         val requestEvent = eventLogger.get(0)
-        assertEquals(
-            "Expected SCREENSHOT_REQUESTED_* UiEvent",
-            SCREENSHOT_REQUESTED_KEY_OTHER.id,
-            requestEvent.eventId
-        )
-        assertEquals(
-            "Expected supplied package name",
-            topComponent.packageName,
-            requestEvent.packageName
-        )
+        assertWithMessage("request UiEvent id")
+            .that(requestEvent.eventId)
+            .isEqualTo(SCREENSHOT_REQUESTED_KEY_OTHER.id)
+
+        assertWithMessage("topComponent package name")
+            .that(requestEvent.packageName)
+            .isEqualTo(topComponent.packageName)
+
         val failureEvent = eventLogger.get(1)
-        assertEquals(
-            "Expected SCREENSHOT_CAPTURE_FAILED UiEvent",
-            SCREENSHOT_CAPTURE_FAILED.id,
-            failureEvent.eventId
-        )
-        assertEquals(
-            "Expected supplied package name",
-            topComponent.packageName,
-            failureEvent.packageName
-        )
+        assertWithMessage("failure UiEvent id")
+            .that(failureEvent.eventId)
+            .isEqualTo(SCREENSHOT_CAPTURE_FAILED.id)
+
+        assertWithMessage("Supplied package name")
+            .that(failureEvent.packageName)
+            .isEqualTo(topComponent.packageName)
     }
 
     @Test
     fun takeScreenshotFullscreen_screenCaptureDisabled_allUsers() {
-        whenever(devicePolicyManager.getScreenCaptureDisabled(isNull(), eq(UserHandle.USER_ALL)))
-            .thenReturn(true)
+        val service = createService()
 
         whenever(
-                devicePolicyResourcesManager.getString(
-                    eq(SCREENSHOT_BLOCKED_BY_ADMIN),
-                    /* Supplier<String> */
-                    any(),
+                devicePolicyManager.getScreenCaptureDisabled(
+                    /* admin= */ isNull(),
+                    eq(UserHandle.USER_ALL)
                 )
             )
-            .thenReturn("SCREENSHOT_BLOCKED_BY_ADMIN")
+            .doReturn(true)
 
         val request =
             ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER)
@@ -205,11 +162,9 @@
                 .build()
 
         service.handleRequest(request, { /* onSaved */}, callback)
+        assertThat(callback.errorReported).isTrue()
+        assertWithMessage("Expected two UiEvents").that(eventLogger.numLogs()).isEqualTo(2)
 
-        // error shown: Toast.makeText(...).show(), untestable
-        verify(callback, times(1)).reportError()
-        verifyZeroInteractions(controller)
-        assertEquals("Expected two UiEvents", 2, eventLogger.numLogs())
         val requestEvent = eventLogger.get(0)
         assertEquals(
             "Expected SCREENSHOT_REQUESTED_* UiEvent",
@@ -234,112 +189,70 @@
         )
     }
 
-    @Test
-    fun takeScreenshot_workProfile_nullBitmap() {
-        val request =
-            ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER)
-                .setTopComponent(topComponent)
-                .build()
-
-        doThrow(IllegalStateException::class.java)
-            .whenever(requestProcessor)
-            .processAsync(any(ScreenshotData::class.java), any())
-
-        service.handleRequest(request, { /* onSaved */}, callback)
-
-        verify(callback, times(1)).reportError()
-        verify(notificationsController, times(1)).notifyScreenshotError(anyInt())
-        verifyZeroInteractions(controller)
-        assertEquals("Expected two UiEvents", 2, eventLogger.numLogs())
-        val requestEvent = eventLogger.get(0)
-        assertEquals(
-            "Expected SCREENSHOT_REQUESTED_* UiEvent",
-            SCREENSHOT_REQUESTED_KEY_OTHER.id,
-            requestEvent.eventId
-        )
-        assertEquals(
-            "Expected supplied package name",
-            topComponent.packageName,
-            requestEvent.packageName
-        )
-        val failureEvent = eventLogger.get(1)
-        assertEquals(
-            "Expected SCREENSHOT_CAPTURE_FAILED UiEvent",
-            SCREENSHOT_CAPTURE_FAILED.id,
-            failureEvent.eventId
-        )
-        assertEquals(
-            "Expected supplied package name",
-            topComponent.packageName,
-            failureEvent.packageName
-        )
-    }
-
-    @Test
-    fun takeScreenshotFullScreen_multiDisplayFlagEnabled_takeScreenshotExecutor() {
-        flags.set(MULTI_DISPLAY_SCREENSHOT, true)
-        service = createService()
-
-        val request =
-            ScreenshotRequest.Builder(TAKE_SCREENSHOT_FULLSCREEN, SCREENSHOT_KEY_OTHER)
-                .setTopComponent(topComponent)
-                .build()
-
-        service.handleRequest(request, { /* onSaved */}, callback)
-
-        verifyZeroInteractions(controller)
-        verify(takeScreenshotExecutor, times(1)).executeScreenshotsAsync(any(), any(), any())
-
-        assertEquals("Expected one UiEvent", 0, eventLogger.numLogs())
-    }
-
-    @Test
-    fun testServiceLifecycle_multiDisplayScreenshotFlagEnabled() {
-        flags.set(MULTI_DISPLAY_SCREENSHOT, true)
-        service = createService()
-
-        service.onCreate()
-        service.onBind(null /* unused: Intent */)
-
-        service.onUnbind(null /* unused: Intent */)
-        verify(takeScreenshotExecutor, times(1)).removeWindows()
-
-        service.onDestroy()
-        verify(takeScreenshotExecutor, times(1)).onDestroy()
-    }
-
-    @Test
-    fun constructor_MultiDisplayFlagOn_screenshotControllerNotCreated() {
-        flags.set(MULTI_DISPLAY_SCREENSHOT, true)
-        clearInvocations(controllerFactory)
-
-        service = createService()
-
-        verifyZeroInteractions(controllerFactory)
-    }
-
     private fun createService(): TakeScreenshotService {
         val service =
             TakeScreenshotService(
-                controllerFactory,
                 userManager,
                 devicePolicyManager,
                 eventLogger,
                 notificationsControllerFactory,
                 mContext,
                 Runnable::run,
-                flags,
-                requestProcessor,
-                { takeScreenshotExecutor },
+                executor
             )
+
         service.attach(
             mContext,
             /* thread = */ null,
             /* className = */ null,
             /* token = */ null,
-            application,
+            mock<Application>(),
             /* activityManager = */ null
         )
         return service
     }
 }
+
+internal class FakeRequestCallback : RequestCallback {
+    var errorReported = false
+    var finished = false
+    override fun reportError() {
+        errorReported = true
+    }
+
+    override fun onFinish() {
+        finished = true
+    }
+}
+
+internal class FakeScreenshotExecutor : TakeScreenshotExecutor {
+    var requestReceived: ScreenshotRequest? = null
+    var windowsPresent = true
+    var destroyed = false
+    override fun onCloseSystemDialogsReceived() {}
+    override suspend fun executeScreenshots(
+        screenshotRequest: ScreenshotRequest,
+        onSaved: (Uri) -> Unit,
+        requestCallback: RequestCallback,
+    ) {
+        requestReceived = screenshotRequest
+    }
+
+    override fun removeWindows() {
+        windowsPresent = false
+    }
+
+    override fun onDestroy() {
+        destroyed = true
+    }
+
+    override fun executeScreenshotsAsync(
+        screenshotRequest: ScreenshotRequest,
+        onSaved: Consumer<Uri>,
+        requestCallback: RequestCallback,
+    ) {
+        runBlocking {
+            executeScreenshots(screenshotRequest, { onSaved.accept(it) }, requestCallback)
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
index 07d9350..5ca6cf1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt
@@ -41,6 +41,7 @@
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.res.R
+import com.android.systemui.scene.shared.model.sceneDataSourceDelegator
 import com.android.systemui.shade.domain.interactor.ShadeInteractor
 import com.android.systemui.statusbar.phone.SystemUIDialogFactory
 import com.android.systemui.testKosmos
@@ -104,7 +105,8 @@
                 dialogFactory,
                 keyguardTransitionInteractor,
                 shadeInteractor,
-                powerManager
+                powerManager,
+                kosmos.sceneDataSourceDelegator,
             )
         testableLooper = TestableLooper.get(this)
 
@@ -145,6 +147,7 @@
                 keyguardTransitionInteractor,
                 shadeInteractor,
                 powerManager,
+                kosmos.sceneDataSourceDelegator,
             )
 
         // First call succeeds.
@@ -268,7 +271,7 @@
     }
 
     private fun goToScene(scene: SceneKey) {
-        communalRepository.setDesiredScene(scene)
+        communalRepository.changeScene(scene)
         testableLooper.processAllMessages()
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index 5b6da0e..e957ca2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -897,8 +897,8 @@
         mConfigurationController.onConfigurationChanged(configuration);
     }
 
-    protected void onTouchEvent(MotionEvent ev) {
-        mTouchHandler.onTouch(mView, ev);
+    protected boolean onTouchEvent(MotionEvent ev) {
+        return mTouchHandler.onTouch(mView, ev);
     }
 
     protected void setDozing(boolean dozing, boolean dozingAlwaysOn) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
index 9bde517..650c45b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -364,6 +364,24 @@
     }
 
     @Test
+    public void alternateBouncerVisible_onTouchEvent_notHandled() {
+        mSetFlagsRule.enableFlags(com.android.systemui.Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR);
+        // GIVEN alternate bouncer is visible
+        when(mAlternateBouncerInteractor.isVisibleState()).thenReturn(true);
+
+        // WHEN touch DOWN event received; THEN touch is NOT handled
+        assertThat(onTouchEvent(MotionEvent.obtain(0L /* downTime */,
+                0L /* eventTime */, MotionEvent.ACTION_DOWN, 0f /* x */, 0f /* y */,
+                0 /* metaState */))).isFalse();
+
+        // WHEN touch MOVE event received; THEN touch is NOT handled
+        assertThat(onTouchEvent(MotionEvent.obtain(0L /* downTime */,
+                0L /* eventTime */, MotionEvent.ACTION_MOVE, 0f /* x */, 200f /* y */,
+                0 /* metaState */))).isFalse();
+
+    }
+
+    @Test
     public void test_onTouchEvent_startTracking() {
         // GIVEN device is NOT pulsing
         mNotificationPanelViewController.setPulsing(false);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
index a5f3f57..5abad61 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
@@ -38,6 +38,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.plugins.BcSmartspaceConfigPlugin
 import com.android.systemui.plugins.BcSmartspaceDataPlugin
@@ -180,6 +181,7 @@
     private lateinit var dateSmartspaceView: SmartspaceView
     private lateinit var weatherSmartspaceView: SmartspaceView
     private lateinit var smartspaceView: SmartspaceView
+    private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
 
     private val clock = FakeSystemClock()
     private val executor = FakeExecutor(clock)
@@ -225,6 +227,14 @@
         setAllowPrivateNotifications(userHandleSecondary, true)
         setShowNotifications(userHandlePrimary, true)
 
+        // Use the real wakefulness lifecycle instead of a mock
+        wakefulnessLifecycle = WakefulnessLifecycle(
+            context,
+            /* wallpaper= */ null,
+            clock,
+            dumpManager
+        )
+
         controller = LockscreenSmartspaceController(
                 context,
                 featureFlags,
@@ -240,6 +250,7 @@
                 deviceProvisionedController,
                 keyguardBypassController,
                 keyguardUpdateMonitor,
+                wakefulnessLifecycle,
                 dumpManager,
                 execution,
                 executor,
@@ -773,6 +784,38 @@
         verify(configurationController, never()).addCallback(any())
     }
 
+    @Test
+    fun testWakefulnessLifecycleDispatch_wake_setsSmartspaceScreenOnTrue() {
+        // Connect session
+        connectSession()
+
+        // Add mock views
+        val mockSmartspaceView = mock(SmartspaceView::class.java)
+        controller.smartspaceViews.add(mockSmartspaceView)
+
+        // Initiate wakefulness change
+        wakefulnessLifecycle.dispatchStartedWakingUp(0)
+
+        // Verify smartspace views receive screen on
+        verify(mockSmartspaceView).setScreenOn(true)
+    }
+
+    @Test
+    fun testWakefulnessLifecycleDispatch_sleep_setsSmartspaceScreenOnFalse() {
+        // Connect session
+        connectSession()
+
+        // Add mock views
+        val mockSmartspaceView = mock(SmartspaceView::class.java)
+        controller.smartspaceViews.add(mockSmartspaceView)
+
+        // Initiate wakefulness change
+        wakefulnessLifecycle.dispatchFinishedGoingToSleep()
+
+        // Verify smartspace views receive screen on
+        verify(mockSmartspaceView).setScreenOn(false)
+    }
+
     private fun connectSession() {
         val dateView = controller.buildAndConnectDateView(fakeParent)
         dateSmartspaceView = dateView as SmartspaceView
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
index 0e89d80..06a4d08 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java
@@ -59,7 +59,8 @@
 import com.android.internal.widget.CachingIconView;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.SysuiTestableContext;
-import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.flags.FakeFeatureFlagsClassic;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.notification.AboveShelfChangedListener;
@@ -90,13 +91,14 @@
 @RunWithLooper
 public class ExpandableNotificationRowTest extends SysuiTestCase {
 
-    private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
+    private final FakeFeatureFlagsClassic mFeatureFlags = new FakeFeatureFlagsClassic();
     private NotificationTestHelper mNotificationTestHelper;
     @Rule public MockitoRule mockito = MockitoJUnit.rule();
 
     @Before
     public void setUp() throws Exception {
         allowTestableLooperAsMainThread();
+        mFeatureFlags.set(Flags.ENABLE_NOTIFICATIONS_SIMULATE_SLOW_MEASURE, false);
         mNotificationTestHelper = new NotificationTestHelper(
                 mContext,
                 mDependency,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt
index 1b43851..3dee093 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt
@@ -1014,6 +1014,136 @@
         verify(spyContext, never()).startServiceAsUser(any(), any())
     }
 
+    @Test
+    fun userIsAdminAndRestricted_addUserActionsNotAdded() {
+        createUserInteractor()
+        testScope.runTest {
+            val id = 0
+            val userInfo =
+                UserInfo(
+                    id,
+                    "child",
+                    /* iconPath= */ "",
+                    /* flags= */ UserInfo.FLAG_ADMIN,
+                    UserManager.USER_TYPE_FULL_RESTRICTED,
+                )
+            whenever(
+                    manager.hasUserRestrictionForUser(
+                        UserManager.DISALLOW_ADD_USER,
+                        UserHandle.of(id)
+                    )
+                )
+                .thenReturn(true)
+
+            userRepository.setUserInfos(listOf(userInfo))
+            userRepository.setSelectedUserInfo(userInfo)
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+
+            val value = collectLastValue(underTest.actions)
+            runCurrent()
+
+            assertThat(value()).isEqualTo(listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT))
+        }
+    }
+
+    @Test
+    fun userIsNotRestrictedAndCannotAddGuests_actionsDoesNotIncludeAddGuest() {
+        createUserInteractor()
+        testScope.runTest {
+            val userInfos = createUserInfos(count = 2, includeGuest = false)
+
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+            keyguardRepository.setKeyguardShowing(false)
+
+            whenever(manager.canAddMoreUsers(UserManager.USER_TYPE_FULL_GUEST)).thenReturn(false)
+
+            val value = collectLastValue(underTest.actions)
+            runCurrent()
+
+            assertThat(value())
+                .isEqualTo(
+                    listOf(
+                        UserActionModel.ADD_USER,
+                        UserActionModel.ADD_SUPERVISED_USER,
+                        UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
+                    )
+                )
+        }
+    }
+
+    @Test
+    fun userIsNotRestrictedAndCannotAddUsers_actionsDoesNotIncludeAddUsers() {
+        createUserInteractor()
+        testScope.runTest {
+            val userInfos = createUserInfos(count = 2, includeGuest = false)
+
+            userRepository.setUserInfos(userInfos)
+            userRepository.setSelectedUserInfo(userInfos[0])
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+            keyguardRepository.setKeyguardShowing(false)
+
+            whenever(manager.canAddMoreUsers(UserManager.USER_TYPE_FULL_SECONDARY))
+                .thenReturn(false)
+
+            val value = collectLastValue(underTest.actions)
+            runCurrent()
+
+            assertThat(value())
+                .isEqualTo(
+                    listOf(
+                        UserActionModel.ENTER_GUEST_MODE,
+                        UserActionModel.NAVIGATE_TO_USER_MANAGEMENT,
+                    )
+                )
+        }
+    }
+
+    @Test
+    fun systemUserHasRestrictions_addUserActionsNotAdded() {
+        createUserInteractor()
+        testScope.runTest {
+            val systemId = 0
+            val systemUser =
+                UserInfo(
+                    systemId,
+                    "system",
+                    /* iconPath= */ "",
+                    /* flags= */ UserInfo.FLAG_SYSTEM,
+                    UserManager.USER_TYPE_SYSTEM_HEADLESS,
+                )
+            val adminId = 10
+            val adminUser =
+                UserInfo(
+                    adminId,
+                    "admin",
+                    /* iconPath= */ "",
+                    /* flags= */ UserInfo.FLAG_ADMIN,
+                    UserManager.USER_TYPE_FULL_SYSTEM,
+                )
+
+            userRepository.setUserInfos(listOf(systemUser, adminUser))
+            userRepository.setSelectedUserInfo(adminUser)
+            userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
+            keyguardRepository.setKeyguardShowing(false)
+
+            whenever(headlessSystemUserMode.isHeadlessSystemUserMode()).thenReturn(true)
+            whenever(
+                    manager.hasUserRestrictionForUser(
+                        UserManager.DISALLOW_ADD_USER,
+                        UserHandle.of(0)
+                    )
+                )
+                .thenReturn(true)
+
+            val value = collectLastValue(underTest.actions)
+            runCurrent()
+
+            assertThat(value()).isEqualTo(listOf(UserActionModel.NAVIGATE_TO_USER_MANAGEMENT))
+        }
+    }
+
     private fun assertUsers(
         models: List<UserModel>?,
         count: Int,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt
index bc0bf9d..de7b14d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt
@@ -32,6 +32,7 @@
 import com.android.systemui.deviceentry.domain.interactor.SystemUIDeviceEntryFaceAuthInteractor
 import com.android.systemui.scene.SceneContainerFrameworkModule
 import com.android.systemui.scene.shared.flag.SceneContainerFlags
+import com.android.systemui.scene.shared.model.SceneContainerConfig
 import com.android.systemui.scene.shared.model.SceneDataSource
 import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
 import com.android.systemui.shade.domain.interactor.BaseShadeInteractor
@@ -45,6 +46,7 @@
 import javax.inject.Provider
 import kotlin.coroutines.CoroutineContext
 import kotlin.coroutines.EmptyCoroutineContext
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.CoroutineStart
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
@@ -71,7 +73,10 @@
     @Binds @Main fun bindMainResources(resources: Resources): Resources
     @Binds fun bindBroadcastDispatcher(fake: FakeBroadcastDispatcher): BroadcastDispatcher
     @Binds @SysUISingleton fun bindsShadeInteractor(sii: ShadeInteractorImpl): ShadeInteractor
-    @Binds fun bindSceneDataSource(delegator: SceneDataSourceDelegator): SceneDataSource
+
+    @Binds
+    @SysUISingleton
+    fun bindSceneDataSource(delegator: SceneDataSourceDelegator): SceneDataSource
 
     @Binds
     fun provideFaceAuthInteractor(
@@ -109,6 +114,15 @@
                 sceneContainerOff.get()
             }
         }
+
+        @Provides
+        @SysUISingleton
+        fun providesSceneDataSourceDelegator(
+            @Application applicationScope: CoroutineScope,
+            config: SceneContainerConfig,
+        ): SceneDataSourceDelegator {
+            return SceneDataSourceDelegator(applicationScope, config)
+        }
     }
 }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt
index 5ff588f..9f5c6b8 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt
@@ -2,6 +2,7 @@
 
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.TransitionKey
 import com.android.systemui.communal.shared.model.CommunalScenes
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -17,11 +18,11 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 class FakeCommunalRepository(
     applicationScope: CoroutineScope,
-    override val desiredScene: MutableStateFlow<SceneKey> =
+    override val currentScene: MutableStateFlow<SceneKey> =
         MutableStateFlow(CommunalScenes.Default),
 ) : CommunalRepository {
-    override fun setDesiredScene(desiredScene: SceneKey) {
-        this.desiredScene.value = desiredScene
+    override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) {
+        this.currentScene.value = toScene
     }
 
     private val defaultTransitionState = ObservableTransitionState.Idle(CommunalScenes.Default)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelKosmos.kt
index c909dd6..b943298 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelKosmos.kt
@@ -21,11 +21,13 @@
 import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.statusbar.sysuiStatusBarStateController
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 
 val Kosmos.alternateBouncerToGoneTransitionViewModel by Fixture {
     AlternateBouncerToGoneTransitionViewModel(
         bouncerToGoneFlows = bouncerToGoneFlows,
         animationFlow = keyguardTransitionAnimationFlow,
+        statusBarStateController = sysuiStatusBarStateController,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt
index f0fedd2..1e25f7f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt
@@ -20,6 +20,7 @@
 import com.android.systemui.keyguard.domain.interactor.keyguardBlueprintInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
 import com.android.systemui.shade.domain.interactor.shadeInteractor
 
 val Kosmos.lockscreenContentViewModel by
@@ -30,5 +31,6 @@
             authController = authController,
             longPress = keyguardLongPressViewModel,
             shadeInteractor = shadeInteractor,
+            applicationScope = applicationCoroutineScope,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelKosmos.kt
index 17c3a14..7a023ee 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGoneTransitionViewModelKosmos.kt
@@ -19,11 +19,13 @@
 import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.statusbar.sysuiStatusBarStateController
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 
 @ExperimentalCoroutinesApi
 val Kosmos.lockscreenToGoneTransitionViewModel by Fixture {
     LockscreenToGoneTransitionViewModel(
         animationFlow = keyguardTransitionAnimationFlow,
+        statusBarStateController = sysuiStatusBarStateController,
     )
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractorKosmos.kt
new file mode 100644
index 0000000..b32960a
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractorKosmos.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.scene.domain.interactor
+
+import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
+
+val Kosmos.sceneContainerOcclusionInteractor by Fixture {
+    SceneContainerOcclusionInteractor(
+        keyguardOcclusionInteractor = keyguardOcclusionInteractor,
+        sceneInteractor = sceneInteractor,
+        keyguardTransitionInteractor = keyguardTransitionInteractor,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/KeyguardOcclusionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/KeyguardOcclusionInteractorKosmos.kt
index d793740..a90a9ff 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/KeyguardOcclusionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/domain/interactor/KeyguardOcclusionInteractorKosmos.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.domain.interactor
 
+import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
 import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardOcclusionInteractor
 import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
@@ -27,10 +28,11 @@
 val Kosmos.keyguardOcclusionInteractor by
     Kosmos.Fixture {
         KeyguardOcclusionInteractor(
-            scope = testScope.backgroundScope,
+            applicationScope = testScope.backgroundScope,
             repository = keyguardOcclusionRepository,
             powerInteractor = powerInteractor,
             transitionInteractor = keyguardTransitionInteractor,
             keyguardInteractor = keyguardInteractor,
+            deviceUnlockedInteractor = { deviceUnlockedInteractor },
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioRepository.kt
index a3ad2b8..4788624 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioRepository.kt
@@ -44,6 +44,8 @@
     private val models: MutableMap<AudioStream, MutableStateFlow<AudioStreamModel>> = mutableMapOf()
     private val lastAudibleVolumes: MutableMap<AudioStream, Int> = mutableMapOf()
 
+    private var isAffectedByMute: MutableMap<AudioStream, Boolean> = mutableMapOf()
+
     private fun getAudioStreamModelState(
         audioStream: AudioStream
     ): MutableStateFlow<AudioStreamModel> =
@@ -93,4 +95,15 @@
     fun setLastAudibleVolume(audioStream: AudioStream, volume: Int) {
         lastAudibleVolumes[audioStream] = volume
     }
+
+    override suspend fun setRingerMode(audioStream: AudioStream, mode: RingerMode) {
+        mutableRingerMode.value = mode
+    }
+
+    override suspend fun isAffectedByMute(audioStream: AudioStream): Boolean =
+        isAffectedByMute[audioStream] ?: true
+
+    fun setIsAffectedByMute(audioStream: AudioStream, isAffected: Boolean) {
+        isAffectedByMute[audioStream] = isAffected
+    }
 }
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index 178102e..f4337d4 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -154,3 +154,15 @@
     srcs: ["ravenwood-services-jarjar-rules.txt"],
     visibility: ["//frameworks/base"],
 }
+
+// For collecting the *stats.csv files in a known directory under out/host/linux-x86/testcases/.
+// The "test" just shows the available stats filenames.
+sh_test_host {
+    name: "ravenwood-stats-checker",
+    src: "ravenwood-stats-checker.sh",
+    test_suites: ["general-tests"],
+    data: [
+        ":framework-minus-apex.ravenwood.stats",
+        ":services.core.ravenwood.stats",
+    ],
+}
diff --git a/ravenwood/ravenwood-stats-checker.sh b/ravenwood/ravenwood-stats-checker.sh
new file mode 100755
index 0000000..fb58e72
--- /dev/null
+++ b/ravenwood/ravenwood-stats-checker.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+# Just print the available *.csv filenames.
+echo '#Stats files:'
+ls *.csv
\ No newline at end of file
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
index fb28055..1f65e15 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java
@@ -37,6 +37,8 @@
 import android.annotation.UserIdInt;
 import android.app.PendingIntent;
 import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothManager;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -695,6 +697,13 @@
             throw new IllegalArgumentException(
                     bluetoothAddress + " is not a valid Bluetooth address");
         }
+        final BluetoothManager bluetoothManager =
+                mContext.getSystemService(BluetoothManager.class);
+        final String bluetoothDeviceName = bluetoothManager == null ? null :
+                bluetoothManager.getAdapter().getBondedDevices().stream()
+                        .filter(device -> device.getAddress().equalsIgnoreCase(bluetoothAddress))
+                        .map(BluetoothDevice::getName)
+                        .findFirst().orElse(null);
         synchronized (mLock) {
             checkAccessibilityAccessLocked();
             if (mBrailleDisplayConnection != null) {
@@ -706,7 +715,10 @@
                 connection.setTestData(mTestBrailleDisplays);
             }
             connection.connectLocked(
-                    bluetoothAddress, BrailleDisplayConnection.BUS_BLUETOOTH, controller);
+                    bluetoothAddress,
+                    bluetoothDeviceName,
+                    BrailleDisplayConnection.BUS_BLUETOOTH,
+                    controller);
         }
     }
 
@@ -763,7 +775,10 @@
                 connection.setTestData(mTestBrailleDisplays);
             }
             connection.connectLocked(
-                    usbSerialNumber, BrailleDisplayConnection.BUS_USB, controller);
+                    usbSerialNumber,
+                    usbDevice.getProductName(),
+                    BrailleDisplayConnection.BUS_USB,
+                    controller);
         }
     }
 
diff --git a/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java b/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java
index 8b41873..b0da3f0 100644
--- a/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java
+++ b/services/accessibility/java/com/android/server/accessibility/BrailleDisplayConnection.java
@@ -24,6 +24,7 @@
 import android.accessibilityservice.IBrailleDisplayController;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.PermissionManuallyEnforced;
 import android.annotation.RequiresNoPermission;
 import android.bluetooth.BluetoothDevice;
@@ -33,6 +34,7 @@
 import android.os.IBinder;
 import android.os.Process;
 import android.os.RemoteException;
+import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Pair;
@@ -141,6 +143,8 @@
 
         @BusType
         int getDeviceBusType(@NonNull Path path);
+
+        String getName(@NonNull Path path);
     }
 
     /**
@@ -149,15 +153,19 @@
      * <p>If found, saves instance state for this connection and starts a thread to
      * read from the Braille display.
      *
-     * @param expectedUniqueId  The expected unique ID of the device to connect, from
-     *                          {@link UsbDevice#getSerialNumber()}
-     *                          or {@link BluetoothDevice#getAddress()}
-     * @param expectedBusType   The expected bus type from {@link BusType}.
-     * @param controller        Interface containing oneway callbacks used to communicate with the
-     *                          {@link android.accessibilityservice.BrailleDisplayController}.
+     * @param expectedUniqueId The expected unique ID of the device to connect, from
+     *                         {@link UsbDevice#getSerialNumber()} or
+     *                         {@link BluetoothDevice#getAddress()}.
+     * @param expectedName     The expected name of the device to connect, from
+     *                         {@link BluetoothDevice#getName()} or
+     *                         {@link UsbDevice#getProductName()}.
+     * @param expectedBusType  The expected bus type from {@link BusType}.
+     * @param controller       Interface containing oneway callbacks used to communicate with the
+     *                         {@link android.accessibilityservice.BrailleDisplayController}.
      */
     void connectLocked(
             @NonNull String expectedUniqueId,
+            @Nullable String expectedName,
             @BusType int expectedBusType,
             @NonNull IBrailleDisplayController controller) {
         Objects.requireNonNull(expectedUniqueId);
@@ -179,10 +187,20 @@
                 unableToGetDescriptor = true;
                 continue;
             }
+            final boolean matchesIdentifier;
             final String uniqueId = mScanner.getUniqueId(path);
+            if (uniqueId != null) {
+                matchesIdentifier = expectedUniqueId.equalsIgnoreCase(uniqueId);
+            } else {
+                // HIDIOCGRAWUNIQ was added in kernel version 5.7.
+                // If the device has an older kernel that does not support that ioctl then as a
+                // fallback we can check against the device name (from HIDIOCGRAWNAME).
+                final String name = mScanner.getName(path);
+                matchesIdentifier = !TextUtils.isEmpty(expectedName) && expectedName.equals(name);
+            }
             if (isBrailleDisplay(descriptor)
                     && mScanner.getDeviceBusType(path) == expectedBusType
-                    && expectedUniqueId.equalsIgnoreCase(uniqueId)) {
+                    && matchesIdentifier) {
                 result.add(Pair.create(path.toFile(), descriptor));
             }
         }
@@ -498,6 +516,12 @@
                 Integer busType = readFromFileDescriptor(path, nativeInterface::getHidrawBusType);
                 return busType != null ? busType : BUS_UNKNOWN;
             }
+
+            @Override
+            public String getName(@NonNull Path path) {
+                Objects.requireNonNull(path);
+                return readFromFileDescriptor(path, nativeInterface::getHidrawName);
+            }
         };
     }
 
@@ -542,6 +566,12 @@
                             BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH)
                             ? BUS_BLUETOOTH : BUS_USB;
                 }
+
+                @Override
+                public String getName(@NonNull Path path) {
+                    return brailleDisplayMap.get(path).getString(
+                            BrailleDisplayController.TEST_BRAILLE_DISPLAY_NAME);
+                }
             };
             return mScanner;
         }
@@ -579,6 +609,8 @@
          * @return the result of ioctl(HIDIOCGRAWINFO).bustype, or -1 if the ioctl fails.
          */
         int getHidrawBusType(int fd);
+
+        String getHidrawName(int fd);
     }
 
     /** Native interface that actually calls native HIDRAW ioctls. */
@@ -602,6 +634,11 @@
         public int getHidrawBusType(int fd) {
             return nativeGetHidrawBusType(fd);
         }
+
+        @Override
+        public String getHidrawName(int fd) {
+            return nativeGetHidrawName(fd);
+        }
     }
 
     private static native int nativeGetHidrawDescSize(int fd);
@@ -611,4 +648,6 @@
     private static native String nativeGetHidrawUniq(int fd);
 
     private static native int nativeGetHidrawBusType(int fd);
+
+    private static native String nativeGetHidrawName(int fd);
 }
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 993a1d5..558e07f 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -53,8 +53,10 @@
 import static android.view.autofill.AutofillManager.getSmartSuggestionModeToString;
 
 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
+import static com.android.server.autofill.FillRequestEventLogger.TRIGGER_REASON_EXPLICITLY_REQUESTED;
 import static com.android.server.autofill.FillRequestEventLogger.TRIGGER_REASON_NORMAL_TRIGGER;
 import static com.android.server.autofill.FillRequestEventLogger.TRIGGER_REASON_PRE_TRIGGER;
+import static com.android.server.autofill.FillRequestEventLogger.TRIGGER_REASON_RETRIGGER;
 import static com.android.server.autofill.FillRequestEventLogger.TRIGGER_REASON_SERVED_FROM_CACHED_RESPONSE;
 import static com.android.server.autofill.FillResponseEventLogger.AVAILABLE_COUNT_WHEN_FILL_REQUEST_FAILED_OR_TIMEOUT;
 import static com.android.server.autofill.FillResponseEventLogger.DETECTION_PREFER_AUTOFILL_PROVIDER;
@@ -543,6 +545,8 @@
                     synchronized (mLock) {
                         int requestId = intent.getIntExtra(EXTRA_REQUEST_ID, 0);
                         FillResponse response = intent.getParcelableExtra(EXTRA_FILL_RESPONSE, android.service.autofill.FillResponse.class);
+                        mFillRequestEventLogger.maybeSetRequestTriggerReason(
+                                TRIGGER_REASON_RETRIGGER);
                         mAssistReceiver.processDelayedFillLocked(requestId, response);
                     }
                 }
@@ -1268,7 +1272,13 @@
         if(mPreviouslyFillDialogPotentiallyStarted) {
             mFillRequestEventLogger.maybeSetRequestTriggerReason(TRIGGER_REASON_PRE_TRIGGER);
         } else {
-            mFillRequestEventLogger.maybeSetRequestTriggerReason(TRIGGER_REASON_NORMAL_TRIGGER);
+            if ((flags & FLAG_MANUAL_REQUEST) != 0) {
+                mFillRequestEventLogger.maybeSetRequestTriggerReason(
+                        TRIGGER_REASON_EXPLICITLY_REQUESTED);
+            } else {
+                mFillRequestEventLogger.maybeSetRequestTriggerReason(
+                        TRIGGER_REASON_NORMAL_TRIGGER);
+            }
         }
         if (existingResponse != null) {
             setViewStatesLocked(
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index 2d59309..8ac1eb9 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -82,7 +82,6 @@
 import android.os.UserManager;
 import android.util.ArraySet;
 import android.util.ExceptionUtils;
-import android.util.Log;
 import android.util.Slog;
 
 import com.android.internal.app.IAppOpsService;
@@ -121,8 +120,7 @@
 
 @SuppressLint("LongLogTag")
 public class CompanionDeviceManagerService extends SystemService {
-    static final String TAG = "CDM_CompanionDeviceManagerService";
-    static final boolean DEBUG = false;
+    private static final String TAG = "CDM_CompanionDeviceManagerService";
 
     private static final long PAIR_WITHOUT_PROMPT_WINDOW_MS = 10 * 60 * 1000; // 10 min
 
@@ -135,7 +133,6 @@
     private final IAppOpsService mAppOpsManager;
     private final PowerExemptionManager mPowerExemptionManager;
     private final PackageManagerInternal mPackageManagerInternal;
-    private final PowerManagerInternal mPowerManagerInternal;
 
     private final AssociationStore mAssociationStore;
     private final SystemDataTransferRequestStore mSystemDataTransferRequestStore;
@@ -160,7 +157,8 @@
         mAmInternal = LocalServices.getService(ActivityManagerInternal.class);
         mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
         final UserManager userManager = context.getSystemService(UserManager.class);
-        mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class);
+        final PowerManagerInternal powerManagerInternal = LocalServices.getService(
+                PowerManagerInternal.class);
 
         final AssociationDiskStore associationDiskStore = new AssociationDiskStore();
         mAssociationStore = new AssociationStore(context, userManager, associationDiskStore);
@@ -178,7 +176,7 @@
 
         mDevicePresenceProcessor = new DevicePresenceProcessor(context,
                 mCompanionAppBinder, userManager, mAssociationStore, mObservableUuidStore,
-                mPowerManagerInternal);
+                powerManagerInternal);
 
         mTransportManager = new CompanionTransportManager(context, mAssociationStore);
 
@@ -252,11 +250,6 @@
 
     private void onPackageRemoveOrDataClearedInternal(
             @UserIdInt int userId, @NonNull String packageName) {
-        if (DEBUG) {
-            Log.i(TAG, "onPackageRemove_Or_DataCleared() u" + userId + "/"
-                    + packageName);
-        }
-
         // Clear all associations for the package.
         final List<AssociationInfo> associationsForPackage =
                 mAssociationStore.getAssociationsByPackage(userId, packageName);
@@ -279,8 +272,6 @@
     }
 
     private void onPackageModifiedInternal(@UserIdInt int userId, @NonNull String packageName) {
-        if (DEBUG) Log.i(TAG, "onPackageModified() u" + userId + "/" + packageName);
-
         final List<AssociationInfo> associationsForPackage =
                 mAssociationStore.getAssociationsByPackage(userId, packageName);
         for (AssociationInfo association : associationsForPackage) {
diff --git a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
index e3f16ae..ac19d8b 100644
--- a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
+++ b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java
@@ -20,6 +20,11 @@
 import static android.provider.Settings.Global.DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS;
 import static android.view.flags.Flags.sensitiveContentAppProtection;
 
+import static com.android.internal.util.FrameworkStatsLog.SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION;
+import static com.android.internal.util.FrameworkStatsLog.SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION__SOURCE__FRAMEWORKS;
+import static com.android.internal.util.FrameworkStatsLog.SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION__STATE__START;
+import static com.android.internal.util.FrameworkStatsLog.SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION__STATE__STOP;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.content.ComponentName;
@@ -86,9 +91,11 @@
     private static class MediaProjectionSession {
         final int mUid;
         final long mSessionId;
+        final boolean mIsExempted;
 
-        MediaProjectionSession(int uid, long sessionId) {
+        MediaProjectionSession(int uid, boolean isExempted, long sessionId) {
             mUid = uid;
+            mIsExempted = isExempted;
             mSessionId = sessionId;
         }
     }
@@ -105,11 +112,28 @@
                     } finally {
                         Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
                     }
+                    FrameworkStatsLog.write(
+                            SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION,
+                            mMediaProjectionSession.mSessionId,
+                            mMediaProjectionSession.mUid,
+                            mMediaProjectionSession.mIsExempted,
+                            SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION__STATE__START,
+                            SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION__SOURCE__FRAMEWORKS
+                    );
                 }
 
                 @Override
                 public void onStop(MediaProjectionInfo info) {
                     if (DEBUG) Log.d(TAG, "onStop projection: " + info);
+                    FrameworkStatsLog.write(
+                            SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION,
+                            mMediaProjectionSession.mSessionId,
+                            mMediaProjectionSession.mUid,
+                            mMediaProjectionSession.mIsExempted,
+                            SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION__STATE__STOP,
+                            SENSITIVE_CONTENT_MEDIA_PROJECTION_SESSION__SOURCE__FRAMEWORKS
+                    );
+
                     Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER,
                             "SensitiveContentProtectionManagerService.onProjectionStop");
                     try {
@@ -137,20 +161,20 @@
         }
 
         if (DEBUG) Log.d(TAG, "onBootPhase - PHASE_BOOT_COMPLETED");
-
-        mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
         init(getContext().getSystemService(MediaProjectionManager.class),
                 LocalServices.getService(WindowManagerInternal.class),
-                getExemptedPackages());
+                LocalServices.getService(PackageManagerInternal.class),
+                getExemptedPackages()
+        );
         if (sensitiveContentAppProtection()) {
             publishBinderService(Context.SENSITIVE_CONTENT_PROTECTION_SERVICE,
-                    new SensitiveContentProtectionManagerServiceBinder(mPackageManagerInternal));
+                    new SensitiveContentProtectionManagerServiceBinder());
         }
     }
 
     @VisibleForTesting
     void init(MediaProjectionManager projectionManager, WindowManagerInternal windowManager,
-            ArraySet<String> exemptedPackages) {
+            PackageManagerInternal packageManagerInternal, ArraySet<String> exemptedPackages) {
         if (DEBUG) Log.d(TAG, "init");
 
         Objects.requireNonNull(projectionManager);
@@ -158,6 +182,7 @@
 
         mProjectionManager = projectionManager;
         mWindowManager = windowManager;
+        mPackageManagerInternal = packageManagerInternal;
         mExemptedPackages = exemptedPackages;
 
         // TODO(b/317250444): use MediaProjectionManagerService directly, reduces unnecessary
@@ -207,28 +232,27 @@
     }
 
     private void onProjectionStart(MediaProjectionInfo projectionInfo) {
-        // exempt on device screen recorder as well.
-        if ((mExemptedPackages != null && mExemptedPackages.contains(
+        boolean isPackageExempted = (mExemptedPackages != null && mExemptedPackages.contains(
                 projectionInfo.getPackageName()))
-                || canRecordSensitiveContent(projectionInfo.getPackageName())) {
-            Log.w(TAG, projectionInfo.getPackageName() + " is exempted.");
-            return;
-        }
+                || canRecordSensitiveContent(projectionInfo.getPackageName())
+                || isAutofillServiceRecorderPackage(projectionInfo.getUserHandle().getIdentifier(),
+                        projectionInfo.getPackageName());
         // TODO(b/324447419): move GlobalSettings lookup to background thread
-        boolean disableScreenShareProtections =
-                Settings.Global.getInt(getContext().getContentResolver(),
-                        DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS, 0) != 0;
-        if (disableScreenShareProtections) {
-            Log.w(TAG, "Screen share protections disabled, ignoring projection start");
+        boolean isFeatureDisabled = Settings.Global.getInt(getContext().getContentResolver(),
+                DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS, 0) != 0;
+        int uid = mPackageManagerInternal.getPackageUid(projectionInfo.getPackageName(), 0,
+                projectionInfo.getUserHandle().getIdentifier());
+        mMediaProjectionSession = new MediaProjectionSession(
+                uid, isPackageExempted || isFeatureDisabled, new Random().nextLong());
+
+        if (isPackageExempted || isFeatureDisabled) {
+            Log.w(TAG, "projection session is exempted, package ="
+                    + projectionInfo.getPackageName() + ", isFeatureDisabled=" + isFeatureDisabled);
             return;
         }
 
         synchronized (mSensitiveContentProtectionLock) {
             mProjectionActive = true;
-            int uid = mPackageManagerInternal.getPackageUid(projectionInfo.getPackageName(), 0,
-                    projectionInfo.getUserHandle().getIdentifier());
-            // TODO review sessionId, whether to use a sequence generator or random is good?
-            mMediaProjectionSession = new MediaProjectionSession(uid, new Random().nextLong());
             if (sensitiveNotificationAppProtection()) {
                 updateAppsThatShouldBlockScreenCapture();
             }
@@ -274,8 +298,9 @@
         // notify windowmanager of any currently posted sensitive content notifications
         ArraySet<PackageInfo> packageInfos =
                 getSensitivePackagesFromNotifications(notifications, rankingMap);
-
-        mWindowManager.addBlockScreenCaptureForApps(packageInfos);
+        if (packageInfos.size() > 0) {
+            mWindowManager.addBlockScreenCaptureForApps(packageInfos);
+        }
     }
 
     private ArraySet<PackageInfo> getSensitivePackagesFromNotifications(
@@ -401,6 +426,7 @@
             if (!mProjectionActive) {
                 return;
             }
+
             if (DEBUG) {
                 Log.d(TAG, "setSensitiveContentProtection - current package=" + packageInfo
                         + ", isShowingSensitiveContent=" + isShowingSensitiveContent
@@ -431,15 +457,29 @@
         }
     }
 
-    private final class SensitiveContentProtectionManagerServiceBinder
-            extends ISensitiveContentProtectionManager.Stub {
-        private final PackageManagerInternal mPackageManagerInternal;
-
-        SensitiveContentProtectionManagerServiceBinder(
-                PackageManagerInternal packageManagerInternal) {
-            mPackageManagerInternal = packageManagerInternal;
+    // TODO: b/328251279 - Autofill service exemption is temporary and will be removed in future.
+    private boolean isAutofillServiceRecorderPackage(int userId, String projectionPackage) {
+        String autofillServiceName = Settings.Secure.getStringForUser(
+                getContext().getContentResolver(), Settings.Secure.AUTOFILL_SERVICE, userId);
+        if (DEBUG) {
+            Log.d(TAG, "autofill service for user " + userId + " is " + autofillServiceName);
         }
 
+        if (autofillServiceName == null) {
+            return false;
+        }
+        ComponentName serviceComponent = ComponentName.unflattenFromString(autofillServiceName);
+        if (serviceComponent == null) {
+            return false;
+        }
+        String autofillServicePackage = serviceComponent.getPackageName();
+
+        return autofillServicePackage != null
+                && autofillServicePackage.equals(projectionPackage);
+    }
+
+    private final class SensitiveContentProtectionManagerServiceBinder
+            extends ISensitiveContentProtectionManager.Stub {
         public void setSensitiveContentProtection(IBinder windowToken, String packageName,
                 boolean isShowingSensitiveContent) {
             Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER,
diff --git a/services/core/java/com/android/server/Watchdog.java b/services/core/java/com/android/server/Watchdog.java
index c18bacb..72a55dbe 100644
--- a/services/core/java/com/android/server/Watchdog.java
+++ b/services/core/java/com/android/server/Watchdog.java
@@ -177,6 +177,7 @@
             "android.hardware.biometrics.fingerprint.IFingerprint/",
             "android.hardware.bluetooth.IBluetoothHci/",
             "android.hardware.camera.provider.ICameraProvider/",
+            "android.hardware.drm.IDrmFactory/",
             "android.hardware.gnss.IGnss/",
             "android.hardware.graphics.allocator.IAllocator/",
             "android.hardware.graphics.composer3.IComposer/",
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index ed1a763..5941fd7 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -1680,6 +1680,11 @@
     PermissionManagerServiceInternal mPermissionManagerInt;
     private TestUtilityService mTestUtilityService;
 
+    // Packages which have received a (LOCKED_)BOOT_COMPLETED broadcast since
+    // the private space profile has been started
+    @GuardedBy("this")
+    private final ArraySet<String> mPrivateSpaceBootCompletedPackages = new ArraySet<String>();
+
     /**
      * Whether to force background check on all apps (for battery saver) or not.
      */
@@ -2307,6 +2312,19 @@
         @Override
         public void onUserStopped(@NonNull TargetUser user) {
             mService.mBatteryStatsService.onCleanupUser(user.getUserIdentifier());
+
+            if (android.os.Flags.allowPrivateProfile()
+                    && android.multiuser.Flags.enablePrivateSpaceFeatures()) {
+                final UserManagerInternal umInternal =
+                        LocalServices.getService(UserManagerInternal.class);
+                UserInfo userInfo = umInternal.getUserInfo(user.getUserIdentifier());
+
+                if (userInfo != null && userInfo.isPrivateProfile()) {
+                    synchronized (mService) {
+                        mService.mPrivateSpaceBootCompletedPackages.clear();
+                    }
+                }
+            }
         }
 
         public ActivityManagerService getService() {
@@ -5042,13 +5060,32 @@
     }
 
     /**
-     * Send LOCKED_BOOT_COMPLETED and BOOT_COMPLETED to the package explicitly when unstopped
+     * Send LOCKED_BOOT_COMPLETED and BOOT_COMPLETED to the package explicitly when unstopped,
+     * or when the package first starts in private space
      */
     private void maybeSendBootCompletedLocked(ProcessRecord app) {
-        if (!android.content.pm.Flags.stayStopped()) return;
-        // Nothing to do if it wasn't previously stopped
-        if (!app.wasForceStopped() && !app.getWindowProcessController().wasForceStopped()) {
-            return;
+        boolean sendBroadcast = false;
+        if (android.os.Flags.allowPrivateProfile()
+                && android.multiuser.Flags.enablePrivateSpaceFeatures()) {
+            final UserManagerInternal umInternal =
+                    LocalServices.getService(UserManagerInternal.class);
+            UserInfo userInfo = umInternal.getUserInfo(app.userId);
+
+            if (userInfo != null && userInfo.isPrivateProfile()) {
+                // Packages in private space get deferred boot completed whenever they start the
+                // first time since profile start
+                if (!mPrivateSpaceBootCompletedPackages.contains(app.info.packageName)) {
+                    mPrivateSpaceBootCompletedPackages.add(app.info.packageName);
+                    sendBroadcast = true;
+                } // else, stopped packages in private space may still hit the logic below
+            }
+        }
+        if (!sendBroadcast) {
+            if (!android.content.pm.Flags.stayStopped()) return;
+            // Nothing to do if it wasn't previously stopped
+            if (!app.wasForceStopped() && !app.getWindowProcessController().wasForceStopped()) {
+                return;
+            }
         }
 
         // Send LOCKED_BOOT_COMPLETED, if necessary
@@ -7978,6 +8015,18 @@
         return uidRecord != null && !uidRecord.isSetIdle();
     }
 
+    @Override
+    public long getUidLastIdleElapsedTime(int uid, String callingPackage) {
+        if (!hasUsageStatsPermission(callingPackage)) {
+            enforceCallingPermission(android.Manifest.permission.PACKAGE_USAGE_STATS,
+                    "getUidLastIdleElapsedTime");
+        }
+        synchronized (mProcLock) {
+            final UidRecord uidRecord = mProcessList.getUidRecordLOSP(uid);
+            return uidRecord != null ? uidRecord.getRealLastIdleTime() : 0;
+        }
+    }
+
     @GuardedBy("mUidFrozenStateChangedCallbackList")
     private final RemoteCallbackList<IUidFrozenStateChangedCallback>
             mUidFrozenStateChangedCallbackList = new RemoteCallbackList<>();
@@ -18189,6 +18238,11 @@
         }
 
         @Override
+        public boolean startUserInBackground(final int userId) {
+            return ActivityManagerService.this.startUserInBackground(userId);
+        }
+
+        @Override
         public void killForegroundAppsForUser(@UserIdInt int userId) {
             final ArrayList<ProcessRecord> procs = new ArrayList<>();
             synchronized (mProcLock) {
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index df6481d..9c1ce66 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -3681,7 +3681,7 @@
         for (int i = N - 1; i >= 0; i--) {
             final UidRecord uidRec = mActiveUids.valueAt(i);
             final long bgTime = uidRec.getLastBackgroundTime();
-            final long idleTime = uidRec.getLastIdleTime();
+            final long idleTime = uidRec.getLastIdleTimeIfStillIdle();
             if (bgTime > 0 && (!uidRec.isIdle() || idleTime == 0)) {
                 if (bgTime <= maxBgTime) {
                     EventLogTags.writeAmUidIdle(uidRec.getUid());
diff --git a/services/core/java/com/android/server/am/UidRecord.java b/services/core/java/com/android/server/am/UidRecord.java
index 45fd470..86fa0fc 100644
--- a/services/core/java/com/android/server/am/UidRecord.java
+++ b/services/core/java/com/android/server/am/UidRecord.java
@@ -17,6 +17,7 @@
 package com.android.server.am;
 
 import android.Manifest;
+import android.annotation.ElapsedRealtimeLong;
 import android.app.ActivityManager;
 import android.content.pm.PackageManager;
 import android.os.SystemClock;
@@ -65,8 +66,19 @@
     @CompositeRWLock({"mService", "mProcLock"})
     private long mLastBackgroundTime;
 
+    /**
+     * Last time the UID became idle. This is set to 0, once the UID becomes active.
+     */
+    @ElapsedRealtimeLong
     @CompositeRWLock({"mService", "mProcLock"})
-    private long mLastIdleTime;
+    private long mLastIdleTimeIfStillIdle;
+
+    /**
+     * Last time the UID became idle. Unlike {@link #mLastIdleTimeIfStillIdle}, we never clear it.
+     */
+    @ElapsedRealtimeLong
+    @CompositeRWLock({"mService", "mProcLock"})
+    private long mRealLastIdleTime;
 
     @CompositeRWLock({"mService", "mProcLock"})
     private boolean mEphemeral;
@@ -257,14 +269,28 @@
         mLastBackgroundTime = lastBackgroundTime;
     }
 
+    /**
+     * Last time the UID became idle. This is set to 0, once the UID becomes active.
+     */
     @GuardedBy(anyOf = {"mService", "mProcLock"})
-    long getLastIdleTime() {
-        return mLastIdleTime;
+    long getLastIdleTimeIfStillIdle() {
+        return mLastIdleTimeIfStillIdle;
+    }
+
+    /**
+     * Last time the UID became idle. Unlike {@link #getLastIdleTimeIfStillIdle}, we never clear it.
+     */
+    @GuardedBy(anyOf = {"mService", "mProcLock"})
+    long getRealLastIdleTime() {
+        return mRealLastIdleTime;
     }
 
     @GuardedBy({"mService", "mProcLock"})
-    void setLastIdleTime(long lastActiveTime) {
-        mLastIdleTime = lastActiveTime;
+    void setLastIdleTime(@ElapsedRealtimeLong long lastIdleTime) {
+        mLastIdleTimeIfStillIdle = lastIdleTime;
+        if (lastIdleTime > 0) {
+            mRealLastIdleTime = lastIdleTime;
+        }
     }
 
     @GuardedBy(anyOf = {"mService", "mProcLock"})
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index c020a9f..60a8b50 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -671,6 +671,14 @@
     }
 
     private void sendLockedBootCompletedBroadcast(IIntentReceiver receiver, @UserIdInt int userId) {
+        if (android.os.Flags.allowPrivateProfile()
+                && android.multiuser.Flags.enablePrivateSpaceFeatures()) {
+            final UserInfo userInfo = getUserInfo(userId);
+            if (userInfo != null && userInfo.isPrivateProfile()) {
+                Slogf.i(TAG, "Skipping LOCKED_BOOT_COMPLETED for private profile user #" + userId);
+                return;
+            }
+        }
         final Intent intent = new Intent(Intent.ACTION_LOCKED_BOOT_COMPLETED, null);
         intent.putExtra(Intent.EXTRA_USER_HANDLE, userId);
         intent.addFlags(Intent.FLAG_RECEIVER_NO_ABORT
@@ -877,6 +885,13 @@
 
         mHandler.obtainMessage(USER_UNLOCKED_MSG, userId, 0).sendToTarget();
 
+        if (android.os.Flags.allowPrivateProfile()
+                && android.multiuser.Flags.enablePrivateSpaceFeatures()) {
+            if (userInfo.isPrivateProfile()) {
+                Slogf.i(TAG, "Skipping BOOT_COMPLETED for private profile user #" + userId);
+                return;
+            }
+        }
         Slogf.i(TAG, "Posting BOOT_COMPLETED user #" + userId);
         // Do not report secondary users, runtime restarts or first boot/upgrade
         if (userId == UserHandle.USER_SYSTEM
@@ -993,6 +1008,13 @@
         if (isCurrentUserLU(userId)) {
             return USER_OP_IS_CURRENT;
         }
+        // TODO(b/324647580): Refactor the idea of "force" and clean up. In the meantime...
+        final int parentId = mUserProfileGroupIds.get(userId, UserInfo.NO_PROFILE_GROUP_ID);
+        if (parentId != UserInfo.NO_PROFILE_GROUP_ID && parentId != userId) {
+            if ((UserHandle.USER_SYSTEM == parentId || isCurrentUserLU(parentId)) && !force) {
+                return USER_OP_ERROR_RELATED_USERS_CANNOT_STOP;
+            }
+        }
         TimingsTraceAndSlog t = new TimingsTraceAndSlog();
         int[] usersToStop = getUsersToStopLU(userId);
         // If one of related users is system or current, no related users should be stopped
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index f1eea72..951f676 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -127,8 +127,7 @@
     private final Object mDeviceStateLock = new Object();
 
     // Request to override default use of A2DP for media.
-    @GuardedBy("mDeviceStateLock")
-    private boolean mBluetoothA2dpEnabled;
+    private AtomicBoolean mBluetoothA2dpEnabled = new AtomicBoolean(false);
 
     // lock always taken when accessing AudioService.mSetModeDeathHandlers
     // TODO do not "share" the lock between AudioService and BtHelpr, see b/123769055
@@ -275,17 +274,8 @@
     }
 
     /*package*/ void setBluetoothA2dpOn_Async(boolean on, String source) {
-        synchronized (mDeviceStateLock) {
-            if (mBluetoothA2dpEnabled == on) {
-                return;
-            }
-            mBluetoothA2dpEnabled = on;
-            mBrokerHandler.removeMessages(MSG_IIL_SET_FORCE_BT_A2DP_USE);
-            sendIILMsgNoDelay(MSG_IIL_SET_FORCE_BT_A2DP_USE, SENDMSG_QUEUE,
-                    AudioSystem.FOR_MEDIA,
-                    mBluetoothA2dpEnabled ? AudioSystem.FORCE_NONE : AudioSystem.FORCE_NO_BT_A2DP,
-                    source);
-        }
+        mBluetoothA2dpEnabled.set(on);
+        sendLMsgNoDelay(MSG_L_SET_FORCE_BT_A2DP_USE, SENDMSG_REPLACE, source);
     }
 
     /**
@@ -1223,16 +1213,8 @@
         }
     }
 
-    /*package*/ boolean isAvrcpAbsoluteVolumeSupported() {
-        synchronized (mDeviceStateLock) {
-            return mBtHelper.isAvrcpAbsoluteVolumeSupported();
-        }
-    }
-
     /*package*/ boolean isBluetoothA2dpOn() {
-        synchronized (mDeviceStateLock) {
-            return mBluetoothA2dpEnabled;
-        }
+        return mBluetoothA2dpEnabled.get();
     }
 
     /*package*/ void postSetAvrcpAbsoluteVolumeIndex(int index) {
@@ -1601,15 +1583,12 @@
                 .append(") from u/pid:").append(Binder.getCallingUid()).append("/")
                 .append(Binder.getCallingPid()).append(" src:").append(source).toString();
 
-        synchronized (mDeviceStateLock) {
-            mBluetoothA2dpEnabled = on;
-            mBrokerHandler.removeMessages(MSG_IIL_SET_FORCE_BT_A2DP_USE);
-            onSetForceUse(
-                    AudioSystem.FOR_MEDIA,
-                    mBluetoothA2dpEnabled ? AudioSystem.FORCE_NONE : AudioSystem.FORCE_NO_BT_A2DP,
-                    fromA2dp,
-                    eventSource);
-        }
+        mBluetoothA2dpEnabled.set(on);
+        onSetForceUse(
+                AudioSystem.FOR_MEDIA,
+                on ? AudioSystem.FORCE_NONE : AudioSystem.FORCE_NO_BT_A2DP,
+                fromA2dp,
+                eventSource);
     }
 
     /*package*/ boolean handleDeviceConnection(@NonNull AudioDeviceAttributes attributes,
@@ -1658,9 +1637,7 @@
     }
 
     /*package*/ boolean getBluetoothA2dpEnabled() {
-        synchronized (mDeviceStateLock) {
-            return mBluetoothA2dpEnabled;
-        }
+        return mBluetoothA2dpEnabled.get();
     }
 
     /*package*/ int getLeAudioDeviceGroupId(BluetoothDevice device) {
@@ -1821,9 +1798,12 @@
                     }
                     break;
                 case MSG_IIL_SET_FORCE_USE: // intended fall-through
-                case MSG_IIL_SET_FORCE_BT_A2DP_USE:
-                    onSetForceUse(msg.arg1, msg.arg2,
-                                  (msg.what == MSG_IIL_SET_FORCE_BT_A2DP_USE), (String) msg.obj);
+                    onSetForceUse(msg.arg1, msg.arg2, false, (String) msg.obj);
+                    break;
+                case MSG_L_SET_FORCE_BT_A2DP_USE:
+                    int forcedUsage = mBluetoothA2dpEnabled.get()
+                            ? AudioSystem.FORCE_NONE : AudioSystem.FORCE_NO_BT_A2DP;
+                    onSetForceUse(AudioSystem.FOR_MEDIA, forcedUsage, true, (String) msg.obj);
                     break;
                 case MSG_REPORT_NEW_ROUTES:
                 case MSG_REPORT_NEW_ROUTES_A2DP:
@@ -1831,35 +1811,38 @@
                         mDeviceInventory.onReportNewRoutes();
                     }
                     break;
-                case MSG_L_SET_BT_ACTIVE_DEVICE:
-                    synchronized (mSetModeLock) {
-                        synchronized (mDeviceStateLock) {
-                            final BtDeviceInfo btInfo = (BtDeviceInfo) msg.obj;
-                            if (btInfo.mState == BluetoothProfile.STATE_CONNECTED
-                                    && !mBtHelper.isProfilePoxyConnected(btInfo.mProfile)) {
-                                AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
-                                        "msg: MSG_L_SET_BT_ACTIVE_DEVICE "
-                                            + "received with null profile proxy: "
-                                            + btInfo)).printLog(TAG));
-                            } else {
-                                @AudioSystem.AudioFormatNativeEnumForBtCodec final int codec =
-                                        mBtHelper.getCodecWithFallback(btInfo.mDevice,
-                                                btInfo.mProfile, btInfo.mIsLeOutput,
-                                                "MSG_L_SET_BT_ACTIVE_DEVICE");
+                case MSG_L_SET_BT_ACTIVE_DEVICE: {
+                    final BtDeviceInfo btInfo = (BtDeviceInfo) msg.obj;
+                    if (btInfo.mState == BluetoothProfile.STATE_CONNECTED
+                            && !mBtHelper.isProfilePoxyConnected(btInfo.mProfile)) {
+                        AudioService.sDeviceLogger.enqueue((new EventLogger.StringEvent(
+                                "msg: MSG_L_SET_BT_ACTIVE_DEVICE "
+                                        + "received with null profile proxy: "
+                                        + btInfo)).printLog(TAG));
+                    } else {
+                        @AudioSystem.AudioFormatNativeEnumForBtCodec final int codec =
+                                mBtHelper.getCodecWithFallback(btInfo.mDevice,
+                                        btInfo.mProfile, btInfo.mIsLeOutput,
+                                        "MSG_L_SET_BT_ACTIVE_DEVICE");
+                        synchronized (mSetModeLock) {
+                            synchronized (mDeviceStateLock) {
                                 mDeviceInventory.onSetBtActiveDevice(btInfo, codec,
                                         (btInfo.mProfile
-                                                != BluetoothProfile.LE_AUDIO || btInfo.mIsLeOutput)
+                                                != BluetoothProfile.LE_AUDIO
+                                                || btInfo.mIsLeOutput)
                                                 ? mAudioService.getBluetoothContextualVolumeStream()
                                                 : AudioSystem.STREAM_DEFAULT);
                                 if (btInfo.mProfile == BluetoothProfile.LE_AUDIO
-                                        || btInfo.mProfile == BluetoothProfile.HEARING_AID) {
-                                    onUpdateCommunicationRouteClient(isBluetoothScoRequested(),
+                                        || btInfo.mProfile
+                                        == BluetoothProfile.HEARING_AID) {
+                                    onUpdateCommunicationRouteClient(
+                                            isBluetoothScoRequested(),
                                             "setBluetoothActiveDevice");
                                 }
                             }
                         }
                     }
-                    break;
+                } break;
                 case MSG_BT_HEADSET_CNCT_FAILED:
                     synchronized (mSetModeLock) {
                         synchronized (mDeviceStateLock) {
@@ -1883,11 +1866,11 @@
                     break;
                 case MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE: {
                     final BtDeviceInfo btInfo = (BtDeviceInfo) msg.obj;
+                    @AudioSystem.AudioFormatNativeEnumForBtCodec final int codec =
+                            mBtHelper.getCodecWithFallback(btInfo.mDevice,
+                                    btInfo.mProfile, btInfo.mIsLeOutput,
+                                    "MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE");
                     synchronized (mDeviceStateLock) {
-                        @AudioSystem.AudioFormatNativeEnumForBtCodec final int codec =
-                                mBtHelper.getCodecWithFallback(btInfo.mDevice,
-                                        btInfo.mProfile, btInfo.mIsLeOutput,
-                                        "MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE");
                         mDeviceInventory.onBluetoothDeviceConfigChange(
                                 btInfo, codec, BtHelper.EVENT_DEVICE_CONFIG_CHANGE);
                     }
@@ -2098,7 +2081,7 @@
     private static final int MSG_L_SET_WIRED_DEVICE_CONNECTION_STATE = 2;
     private static final int MSG_I_BROADCAST_BT_CONNECTION_STATE = 3;
     private static final int MSG_IIL_SET_FORCE_USE = 4;
-    private static final int MSG_IIL_SET_FORCE_BT_A2DP_USE = 5;
+    private static final int MSG_L_SET_FORCE_BT_A2DP_USE = 5;
     private static final int MSG_TOGGLE_HDMI = 6;
     private static final int MSG_L_SET_BT_ACTIVE_DEVICE = 7;
     private static final int MSG_BT_HEADSET_CNCT_FAILED = 9;
@@ -2295,7 +2278,7 @@
         MESSAGES_MUTE_MUSIC.add(MSG_L_SET_BT_ACTIVE_DEVICE);
         MESSAGES_MUTE_MUSIC.add(MSG_L_BLUETOOTH_DEVICE_CONFIG_CHANGE);
         MESSAGES_MUTE_MUSIC.add(MSG_L_A2DP_DEVICE_CONNECTION_CHANGE_EXT);
-        MESSAGES_MUTE_MUSIC.add(MSG_IIL_SET_FORCE_BT_A2DP_USE);
+        MESSAGES_MUTE_MUSIC.add(MSG_L_SET_FORCE_BT_A2DP_USE);
     }
 
     private AtomicBoolean mMusicMuted = new AtomicBoolean(false);
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 649b9ef..be47f85 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -46,6 +46,7 @@
 import static com.android.media.audio.Flags.alarmMinVolumeZero;
 import static com.android.media.audio.Flags.disablePrescaleAbsoluteVolume;
 import static com.android.media.audio.Flags.ringerModeAffectsAlarm;
+import static com.android.media.audio.Flags.setStreamVolumeOrder;
 import static com.android.server.audio.SoundDoseHelper.ACTION_CHECK_MUSIC_ACTIVE;
 import static com.android.server.utils.EventLogger.Event.ALOGE;
 import static com.android.server.utils.EventLogger.Event.ALOGI;
@@ -4538,6 +4539,8 @@
                 + focusFreezeTestApi());
         pw.println("\tcom.android.media.audio.disablePrescaleAbsoluteVolume:"
                 + disablePrescaleAbsoluteVolume());
+        pw.println("\tcom.android.media.audio.setStreamVolumeOrder:"
+                + setStreamVolumeOrder());
         pw.println("\tandroid.media.audio.foregroundAudioControl:"
                 + foregroundAudioControl());
     }
@@ -4705,6 +4708,30 @@
 
         index = rescaleIndex(index * 10, streamType, streamTypeAlias);
 
+        if (setStreamVolumeOrder()) {
+            flags &= ~AudioManager.FLAG_FIXED_VOLUME;
+            if (streamTypeAlias == AudioSystem.STREAM_MUSIC && isFixedVolumeDevice(device)) {
+                flags |= AudioManager.FLAG_FIXED_VOLUME;
+
+                // volume is either 0 or max allowed for fixed volume devices
+                if (index != 0) {
+                    index = mSoundDoseHelper.getSafeMediaVolumeIndex(device);
+                    if (index < 0) {
+                        index = streamState.getMaxIndex();
+                    }
+                }
+            }
+
+            if (!mSoundDoseHelper.willDisplayWarningAfterCheckVolume(streamType, index, device,
+                    flags)) {
+                onSetStreamVolume(streamType, index, flags, device, caller, hasModifyAudioSettings,
+                        // ada is non-null when called from setDeviceVolume,
+                        // which shouldn't update the mute state
+                        canChangeMuteAndUpdateController /*canChangeMute*/);
+                index = mStreamStates[streamType].getIndex(device);
+            }
+        }
+
         if (streamTypeAlias == AudioSystem.STREAM_MUSIC
                 && AudioSystem.DEVICE_OUT_ALL_A2DP_SET.contains(device)
                 && (flags & AudioManager.FLAG_BLUETOOTH_ABS_VOLUME) == 0) {
@@ -4738,26 +4765,28 @@
             mDeviceBroker.postSetHearingAidVolumeIndex(index, streamType);
         }
 
-        flags &= ~AudioManager.FLAG_FIXED_VOLUME;
-        if (streamTypeAlias == AudioSystem.STREAM_MUSIC && isFixedVolumeDevice(device)) {
-            flags |= AudioManager.FLAG_FIXED_VOLUME;
+        if (!setStreamVolumeOrder()) {
+            flags &= ~AudioManager.FLAG_FIXED_VOLUME;
+            if (streamTypeAlias == AudioSystem.STREAM_MUSIC && isFixedVolumeDevice(device)) {
+                flags |= AudioManager.FLAG_FIXED_VOLUME;
 
-            // volume is either 0 or max allowed for fixed volume devices
-            if (index != 0) {
-                index = mSoundDoseHelper.getSafeMediaVolumeIndex(device);
-                if (index < 0) {
-                    index = streamState.getMaxIndex();
+                // volume is either 0 or max allowed for fixed volume devices
+                if (index != 0) {
+                    index = mSoundDoseHelper.getSafeMediaVolumeIndex(device);
+                    if (index < 0) {
+                        index = streamState.getMaxIndex();
+                    }
                 }
             }
-        }
 
-        if (!mSoundDoseHelper.willDisplayWarningAfterCheckVolume(streamType, index, device,
-                flags)) {
-            onSetStreamVolume(streamType, index, flags, device, caller, hasModifyAudioSettings,
-                    // ada is non-null when called from setDeviceVolume,
-                    // which shouldn't update the mute state
-                    canChangeMuteAndUpdateController /*canChangeMute*/);
-            index = mStreamStates[streamType].getIndex(device);
+            if (!mSoundDoseHelper.willDisplayWarningAfterCheckVolume(streamType, index, device,
+                    flags)) {
+                onSetStreamVolume(streamType, index, flags, device, caller, hasModifyAudioSettings,
+                        // ada is non-null when called from setDeviceVolume,
+                        // which shouldn't update the mute state
+                        canChangeMuteAndUpdateController /*canChangeMute*/);
+                index = mStreamStates[streamType].getIndex(device);
+            }
         }
 
         synchronized (mHdmiClientLock) {
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index 0f3f807..f3a5fdb 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -235,10 +235,6 @@
         mDeviceBroker.setForceUse_Async(AudioSystem.FOR_MEDIA, forMed, "onAudioServerDied()");
     }
 
-    /*package*/ synchronized boolean isAvrcpAbsoluteVolumeSupported() {
-        return (mA2dp != null && mAvrcpAbsVolSupported);
-    }
-
     /*package*/ synchronized void setAvrcpAbsoluteVolumeSupported(boolean supported) {
         mAvrcpAbsVolSupported = supported;
         Log.i(TAG, "setAvrcpAbsoluteVolumeSupported supported=" + supported);
@@ -648,8 +644,6 @@
         }
     }
 
-    // @GuardedBy("mDeviceBroker.mSetModeLock")
-    @GuardedBy("AudioDeviceBroker.this.mDeviceStateLock")
     /*package*/ synchronized boolean isProfilePoxyConnected(int profile) {
         switch (profile) {
             case BluetoothProfile.HEADSET:
diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
index 1546010..4aab9d2 100644
--- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java
+++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
@@ -29,6 +29,9 @@
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
 import android.hardware.SensorManager;
 import android.hardware.display.BrightnessConfiguration;
 import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest;
@@ -37,19 +40,20 @@
 import android.os.Message;
 import android.os.PowerManager;
 import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.Trace;
 import android.util.EventLog;
 import android.util.MathUtils;
 import android.util.Slog;
 import android.util.SparseArray;
+import android.util.TimeUtils;
 import android.view.Display;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.display.BrightnessSynchronizer;
 import com.android.internal.os.BackgroundThread;
-import com.android.internal.os.Clock;
 import com.android.server.EventLogTags;
 import com.android.server.display.brightness.BrightnessEvent;
-import com.android.server.display.brightness.LightSensorController;
 import com.android.server.display.brightness.clamper.BrightnessClamperController;
 
 import java.io.PrintWriter;
@@ -64,6 +68,8 @@
 public class AutomaticBrightnessController {
     private static final String TAG = "AutomaticBrightnessController";
 
+    private static final boolean DEBUG_PRETEND_LIGHT_SENSOR_ABSENT = false;
+
     public static final int AUTO_BRIGHTNESS_ENABLED = 1;
     public static final int AUTO_BRIGHTNESS_DISABLED = 2;
     public static final int AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE = 3;
@@ -81,20 +87,32 @@
     public static final int AUTO_BRIGHTNESS_MODE_DOZE = 2;
     public static final int AUTO_BRIGHTNESS_MODE_MAX = AUTO_BRIGHTNESS_MODE_DOZE;
 
+    // How long the current sensor reading is assumed to be valid beyond the current time.
+    // This provides a bit of prediction, as well as ensures that the weight for the last sample is
+    // non-zero, which in turn ensures that the total weight is non-zero.
+    private static final long AMBIENT_LIGHT_PREDICTION_TIME_MILLIS = 100;
+
     // Debounce for sampling user-initiated changes in display brightness to ensure
     // the user is satisfied with the result before storing the sample.
     private static final int BRIGHTNESS_ADJUSTMENT_SAMPLE_DEBOUNCE_MILLIS = 10000;
 
-    private static final int MSG_BRIGHTNESS_ADJUSTMENT_SAMPLE = 1;
-    private static final int MSG_INVALIDATE_CURRENT_SHORT_TERM_MODEL = 2;
-    private static final int MSG_UPDATE_FOREGROUND_APP = 3;
-    private static final int MSG_UPDATE_FOREGROUND_APP_SYNC = 4;
-    private static final int MSG_RUN_UPDATE = 5;
-    private static final int MSG_INVALIDATE_PAUSED_SHORT_TERM_MODEL = 6;
+    private static final int MSG_UPDATE_AMBIENT_LUX = 1;
+    private static final int MSG_BRIGHTNESS_ADJUSTMENT_SAMPLE = 2;
+    private static final int MSG_INVALIDATE_CURRENT_SHORT_TERM_MODEL = 3;
+    private static final int MSG_UPDATE_FOREGROUND_APP = 4;
+    private static final int MSG_UPDATE_FOREGROUND_APP_SYNC = 5;
+    private static final int MSG_RUN_UPDATE = 6;
+    private static final int MSG_INVALIDATE_PAUSED_SHORT_TERM_MODEL = 7;
 
     // Callbacks for requesting updates to the display's power state
     private final Callbacks mCallbacks;
 
+    // The sensor manager.
+    private final SensorManager mSensorManager;
+
+    // The light sensor, or null if not available or needed.
+    private final Sensor mLightSensor;
+
     // The mapper to translate ambient lux to screen brightness in the range [0, 1.0].
     @NonNull
     private BrightnessMappingStrategy mCurrentBrightnessMapper;
@@ -109,21 +127,94 @@
     // How much to scale doze brightness by (should be (0, 1.0]).
     private final float mDozeScaleFactor;
 
+    // Initial light sensor event rate in milliseconds.
+    private final int mInitialLightSensorRate;
+
+    // Steady-state light sensor event rate in milliseconds.
+    private final int mNormalLightSensorRate;
+
+    // The current light sensor event rate in milliseconds.
+    private int mCurrentLightSensorRate;
+
+    // Stability requirements in milliseconds for accepting a new brightness level.  This is used
+    // for debouncing the light sensor.  Different constants are used to debounce the light sensor
+    // when adapting to brighter or darker environments.  This parameter controls how quickly
+    // brightness changes occur in response to an observed change in light level that exceeds the
+    // hysteresis threshold.
+    private final long mBrighteningLightDebounceConfig;
+    private final long mDarkeningLightDebounceConfig;
+    private final long mBrighteningLightDebounceConfigIdle;
+    private final long mDarkeningLightDebounceConfigIdle;
+
+    // If true immediately after the screen is turned on the controller will try to adjust the
+    // brightness based on the current sensor reads. If false, the controller will collect more data
+    // and only then decide whether to change brightness.
+    private final boolean mResetAmbientLuxAfterWarmUpConfig;
+
+    // Period of time in which to consider light samples for a short/long-term estimate of ambient
+    // light in milliseconds.
+    private final int mAmbientLightHorizonLong;
+    private final int mAmbientLightHorizonShort;
+
+    // The intercept used for the weighting calculation. This is used in order to keep all possible
+    // weighting values positive.
+    private final int mWeightingIntercept;
+
     // Configuration object for determining thresholds to change brightness dynamically
+    private final HysteresisLevels mAmbientBrightnessThresholds;
     private final HysteresisLevels mScreenBrightnessThresholds;
+    private final HysteresisLevels mAmbientBrightnessThresholdsIdle;
     private final HysteresisLevels mScreenBrightnessThresholdsIdle;
 
     private boolean mLoggingEnabled;
+
+    // Amount of time to delay auto-brightness after screen on while waiting for
+    // the light sensor to warm-up in milliseconds.
+    // May be 0 if no warm-up is required.
+    private int mLightSensorWarmUpTimeConfig;
+
+    // Set to true if the light sensor is enabled.
+    private boolean mLightSensorEnabled;
+
+    // The time when the light sensor was enabled.
+    private long mLightSensorEnableTime;
+
     // The currently accepted nominal ambient light level.
     private float mAmbientLux;
+
+    // The last calculated ambient light level (long time window).
+    private float mSlowAmbientLux;
+
+    // The last calculated ambient light level (short time window).
+    private float mFastAmbientLux;
+
+    // The last ambient lux value prior to passing the darkening or brightening threshold.
+    private float mPreThresholdLux;
+
     // True if mAmbientLux holds a valid value.
     private boolean mAmbientLuxValid;
+
+    // The ambient light level threshold at which to brighten or darken the screen.
+    private float mAmbientBrighteningThreshold;
+    private float mAmbientDarkeningThreshold;
+
     // The last brightness value prior to passing the darkening or brightening threshold.
     private float mPreThresholdBrightness;
 
     // The screen brightness threshold at which to brighten or darken the screen.
     private float mScreenBrighteningThreshold;
     private float mScreenDarkeningThreshold;
+    // The most recent light sample.
+    private float mLastObservedLux = INVALID_LUX;
+
+    // The time of the most light recent sample.
+    private long mLastObservedLuxTime;
+
+    // The number of light samples collected since the light sensor was enabled.
+    private int mRecentLightSamples;
+
+    // A ring buffer containing all of the recent ambient light sensor readings.
+    private AmbientLightRingBuffer mAmbientLightRingBuffer;
 
     // The handler
     private AutomaticBrightnessHandler mHandler;
@@ -182,55 +273,88 @@
     private Context mContext;
     private int mState = AUTO_BRIGHTNESS_DISABLED;
 
-    private final Clock mClock;
+    private Clock mClock;
     private final Injector mInjector;
 
-    private final LightSensorController mLightSensorController;
-
-    AutomaticBrightnessController(Callbacks callbacks, Looper looper, SensorManager sensorManager,
+    AutomaticBrightnessController(Callbacks callbacks, Looper looper,
+            SensorManager sensorManager, Sensor lightSensor,
             SparseArray<BrightnessMappingStrategy> brightnessMappingStrategyMap,
-            float brightnessMin, float brightnessMax, float dozeScaleFactor,
+            int lightSensorWarmUpTime, float brightnessMin, float brightnessMax,
+            float dozeScaleFactor, int lightSensorRate, int initialLightSensorRate,
+            long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
+            long brighteningLightDebounceConfigIdle, long darkeningLightDebounceConfigIdle,
+            boolean resetAmbientLuxAfterWarmUpConfig, HysteresisLevels ambientBrightnessThresholds,
             HysteresisLevels screenBrightnessThresholds,
+            HysteresisLevels ambientBrightnessThresholdsIdle,
             HysteresisLevels screenBrightnessThresholdsIdle, Context context,
             BrightnessRangeController brightnessModeController,
-            BrightnessThrottler brightnessThrottler, float userLux, float userNits,
-            int displayId, LightSensorController.LightSensorControllerConfig config,
+            BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
+            int ambientLightHorizonLong, float userLux, float userNits,
             BrightnessClamperController brightnessClamperController) {
-        this(new Injector(), callbacks, looper,
-                brightnessMappingStrategyMap, brightnessMin, brightnessMax, dozeScaleFactor,
-                screenBrightnessThresholds,
+        this(new Injector(), callbacks, looper, sensorManager, lightSensor,
+                brightnessMappingStrategyMap, lightSensorWarmUpTime, brightnessMin, brightnessMax,
+                dozeScaleFactor, lightSensorRate, initialLightSensorRate,
+                brighteningLightDebounceConfig, darkeningLightDebounceConfig,
+                brighteningLightDebounceConfigIdle, darkeningLightDebounceConfigIdle,
+                resetAmbientLuxAfterWarmUpConfig, ambientBrightnessThresholds,
+                screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
                 screenBrightnessThresholdsIdle, context, brightnessModeController,
-                brightnessThrottler, userLux, userNits,
-                new LightSensorController(sensorManager, looper, displayId, config),
-                brightnessClamperController
+                brightnessThrottler, ambientLightHorizonShort, ambientLightHorizonLong, userLux,
+                userNits, brightnessClamperController
         );
     }
 
     @VisibleForTesting
     AutomaticBrightnessController(Injector injector, Callbacks callbacks, Looper looper,
+            SensorManager sensorManager, Sensor lightSensor,
             SparseArray<BrightnessMappingStrategy> brightnessMappingStrategyMap,
-            float brightnessMin, float brightnessMax, float dozeScaleFactor,
+            int lightSensorWarmUpTime, float brightnessMin, float brightnessMax,
+            float dozeScaleFactor, int lightSensorRate, int initialLightSensorRate,
+            long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
+            long brighteningLightDebounceConfigIdle, long darkeningLightDebounceConfigIdle,
+            boolean resetAmbientLuxAfterWarmUpConfig, HysteresisLevels ambientBrightnessThresholds,
             HysteresisLevels screenBrightnessThresholds,
+            HysteresisLevels ambientBrightnessThresholdsIdle,
             HysteresisLevels screenBrightnessThresholdsIdle, Context context,
             BrightnessRangeController brightnessRangeController,
-            BrightnessThrottler brightnessThrottler, float userLux, float userNits,
-            LightSensorController lightSensorController,
+            BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
+            int ambientLightHorizonLong, float userLux, float userNits,
             BrightnessClamperController brightnessClamperController) {
         mInjector = injector;
         mClock = injector.createClock();
         mContext = context;
         mCallbacks = callbacks;
+        mSensorManager = sensorManager;
         mCurrentBrightnessMapper = brightnessMappingStrategyMap.get(AUTO_BRIGHTNESS_MODE_DEFAULT);
         mScreenBrightnessRangeMinimum = brightnessMin;
         mScreenBrightnessRangeMaximum = brightnessMax;
+        mLightSensorWarmUpTimeConfig = lightSensorWarmUpTime;
         mDozeScaleFactor = dozeScaleFactor;
+        mNormalLightSensorRate = lightSensorRate;
+        mInitialLightSensorRate = initialLightSensorRate;
+        mCurrentLightSensorRate = -1;
+        mBrighteningLightDebounceConfig = brighteningLightDebounceConfig;
+        mDarkeningLightDebounceConfig = darkeningLightDebounceConfig;
+        mBrighteningLightDebounceConfigIdle = brighteningLightDebounceConfigIdle;
+        mDarkeningLightDebounceConfigIdle = darkeningLightDebounceConfigIdle;
+        mResetAmbientLuxAfterWarmUpConfig = resetAmbientLuxAfterWarmUpConfig;
+        mAmbientLightHorizonLong = ambientLightHorizonLong;
+        mAmbientLightHorizonShort = ambientLightHorizonShort;
+        mWeightingIntercept = ambientLightHorizonLong;
+        mAmbientBrightnessThresholds = ambientBrightnessThresholds;
+        mAmbientBrightnessThresholdsIdle = ambientBrightnessThresholdsIdle;
         mScreenBrightnessThresholds = screenBrightnessThresholds;
         mScreenBrightnessThresholdsIdle = screenBrightnessThresholdsIdle;
         mShortTermModel = new ShortTermModel();
         mPausedShortTermModel = new ShortTermModel();
         mHandler = new AutomaticBrightnessHandler(looper);
-        mLightSensorController = lightSensorController;
-        mLightSensorController.setListener(this::setAmbientLux);
+        mAmbientLightRingBuffer =
+            new AmbientLightRingBuffer(mNormalLightSensorRate, mAmbientLightHorizonLong, mClock);
+
+        if (!DEBUG_PRETEND_LIGHT_SENSOR_ABSENT) {
+            mLightSensor = lightSensor;
+        }
+
         mActivityTaskManager = ActivityTaskManager.getService();
         mPackageManager = mContext.getPackageManager();
         mTaskStackListener = new TaskStackListenerImpl();
@@ -282,13 +406,13 @@
         if (brightnessEvent != null) {
             brightnessEvent.setLux(
                     mAmbientLuxValid ? mAmbientLux : PowerManager.BRIGHTNESS_INVALID_FLOAT);
+            brightnessEvent.setPreThresholdLux(mPreThresholdLux);
             brightnessEvent.setPreThresholdBrightness(mPreThresholdBrightness);
             brightnessEvent.setRecommendedBrightness(mScreenAutoBrightness);
             brightnessEvent.setFlags(brightnessEvent.getFlags()
                     | (!mAmbientLuxValid ? BrightnessEvent.FLAG_INVALID_LUX : 0)
                     | (shouldApplyDozeScaleFactor() ? BrightnessEvent.FLAG_DOZE_SCALE : 0));
             brightnessEvent.setAutoBrightnessMode(getMode());
-            mLightSensorController.updateBrightnessEvent(brightnessEvent);
         }
 
         if (!mAmbientLuxValid) {
@@ -311,22 +435,21 @@
      */
     public float getAutomaticScreenBrightnessBasedOnLastObservedLux(
             BrightnessEvent brightnessEvent) {
-        float lastObservedLux = mLightSensorController.getLastObservedLux();
-        if (lastObservedLux == INVALID_LUX) {
+        if (mLastObservedLux == INVALID_LUX) {
             return PowerManager.BRIGHTNESS_INVALID_FLOAT;
         }
 
-        float brightness = mCurrentBrightnessMapper.getBrightness(lastObservedLux,
+        float brightness = mCurrentBrightnessMapper.getBrightness(mLastObservedLux,
                 mForegroundAppPackageName, mForegroundAppCategory);
         if (shouldApplyDozeScaleFactor()) {
             brightness *= mDozeScaleFactor;
         }
 
         if (brightnessEvent != null) {
-            brightnessEvent.setLux(lastObservedLux);
+            brightnessEvent.setLux(mLastObservedLux);
             brightnessEvent.setRecommendedBrightness(brightness);
             brightnessEvent.setFlags(brightnessEvent.getFlags()
-                    | (lastObservedLux == INVALID_LUX ? BrightnessEvent.FLAG_INVALID_LUX : 0)
+                    | (mLastObservedLux == INVALID_LUX ? BrightnessEvent.FLAG_INVALID_LUX : 0)
                     | (shouldApplyDozeScaleFactor() ? BrightnessEvent.FLAG_DOZE_SCALE : 0));
             brightnessEvent.setAutoBrightnessMode(getMode());
         }
@@ -378,7 +501,6 @@
 
     public void stop() {
         setLightSensorEnabled(false);
-        mLightSensorController.stop();
     }
 
     public boolean hasUserDataPoints() {
@@ -407,6 +529,14 @@
         return mAmbientLux;
     }
 
+    float getSlowAmbientLux() {
+        return mSlowAmbientLux;
+    }
+
+    float getFastAmbientLux() {
+        return mFastAmbientLux;
+    }
+
     private boolean setDisplayPolicy(int policy) {
         if (mDisplayPolicy == policy) {
             return false;
@@ -485,13 +615,36 @@
         pw.println("  mScreenBrightnessRangeMinimum=" + mScreenBrightnessRangeMinimum);
         pw.println("  mScreenBrightnessRangeMaximum=" + mScreenBrightnessRangeMaximum);
         pw.println("  mDozeScaleFactor=" + mDozeScaleFactor);
+        pw.println("  mInitialLightSensorRate=" + mInitialLightSensorRate);
+        pw.println("  mNormalLightSensorRate=" + mNormalLightSensorRate);
+        pw.println("  mLightSensorWarmUpTimeConfig=" + mLightSensorWarmUpTimeConfig);
+        pw.println("  mBrighteningLightDebounceConfig=" + mBrighteningLightDebounceConfig);
+        pw.println("  mDarkeningLightDebounceConfig=" + mDarkeningLightDebounceConfig);
+        pw.println("  mBrighteningLightDebounceConfigIdle=" + mBrighteningLightDebounceConfigIdle);
+        pw.println("  mDarkeningLightDebounceConfigIdle=" + mDarkeningLightDebounceConfigIdle);
+        pw.println("  mResetAmbientLuxAfterWarmUpConfig=" + mResetAmbientLuxAfterWarmUpConfig);
+        pw.println("  mAmbientLightHorizonLong=" + mAmbientLightHorizonLong);
+        pw.println("  mAmbientLightHorizonShort=" + mAmbientLightHorizonShort);
+        pw.println("  mWeightingIntercept=" + mWeightingIntercept);
+
         pw.println();
         pw.println("Automatic Brightness Controller State:");
+        pw.println("  mLightSensor=" + mLightSensor);
+        pw.println("  mLightSensorEnabled=" + mLightSensorEnabled);
+        pw.println("  mLightSensorEnableTime=" + TimeUtils.formatUptime(mLightSensorEnableTime));
+        pw.println("  mCurrentLightSensorRate=" + mCurrentLightSensorRate);
         pw.println("  mAmbientLux=" + mAmbientLux);
         pw.println("  mAmbientLuxValid=" + mAmbientLuxValid);
+        pw.println("  mPreThresholdLux=" + mPreThresholdLux);
         pw.println("  mPreThresholdBrightness=" + mPreThresholdBrightness);
+        pw.println("  mAmbientBrighteningThreshold=" + mAmbientBrighteningThreshold);
+        pw.println("  mAmbientDarkeningThreshold=" + mAmbientDarkeningThreshold);
         pw.println("  mScreenBrighteningThreshold=" + mScreenBrighteningThreshold);
         pw.println("  mScreenDarkeningThreshold=" + mScreenDarkeningThreshold);
+        pw.println("  mLastObservedLux=" + mLastObservedLux);
+        pw.println("  mLastObservedLuxTime=" + TimeUtils.formatUptime(mLastObservedLuxTime));
+        pw.println("  mRecentLightSamples=" + mRecentLightSamples);
+        pw.println("  mAmbientLightRingBuffer=" + mAmbientLightRingBuffer);
         pw.println("  mScreenAutoBrightness=" + mScreenAutoBrightness);
         pw.println("  mDisplayPolicy=" + DisplayPowerRequest.policyToString(mDisplayPolicy));
         pw.println("  mShortTermModel=");
@@ -520,21 +673,22 @@
         }
 
         pw.println();
+        pw.println("  mAmbientBrightnessThresholds=");
+        mAmbientBrightnessThresholds.dump(pw);
         pw.println("  mScreenBrightnessThresholds=");
         mScreenBrightnessThresholds.dump(pw);
         pw.println("  mScreenBrightnessThresholdsIdle=");
         mScreenBrightnessThresholdsIdle.dump(pw);
-
-        pw.println();
-        mLightSensorController.dump(pw);
+        pw.println("  mAmbientBrightnessThresholdsIdle=");
+        mAmbientBrightnessThresholdsIdle.dump(pw);
     }
 
     public float[] getLastSensorValues() {
-        return mLightSensorController.getLastSensorValues();
+        return mAmbientLightRingBuffer.getAllLuxValues();
     }
 
     public long[] getLastSensorTimestamps() {
-        return mLightSensorController.getLastSensorTimestamps();
+        return mAmbientLightRingBuffer.getAllTimestamps();
     }
 
     private String configStateToString(int state) {
@@ -551,33 +705,273 @@
     }
 
     private boolean setLightSensorEnabled(boolean enable) {
-        if (enable && mLightSensorController.enableLightSensorIfNeeded()) {
-            registerForegroundAppUpdater();
-            return true;
-        } else if (!enable && mLightSensorController.disableLightSensorIfNeeded()) {
+        if (enable) {
+            if (!mLightSensorEnabled) {
+                mLightSensorEnabled = true;
+                mLightSensorEnableTime = mClock.uptimeMillis();
+                mCurrentLightSensorRate = mInitialLightSensorRate;
+                registerForegroundAppUpdater();
+                mSensorManager.registerListener(mLightSensorListener, mLightSensor,
+                        mCurrentLightSensorRate * 1000, mHandler);
+                return true;
+            }
+        } else if (mLightSensorEnabled) {
+            mLightSensorEnabled = false;
+            mAmbientLuxValid = !mResetAmbientLuxAfterWarmUpConfig;
+            if (!mAmbientLuxValid) {
+                mPreThresholdLux = PowerManager.BRIGHTNESS_INVALID_FLOAT;
+            }
             mScreenAutoBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
             mRawScreenAutoBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
             mPreThresholdBrightness = PowerManager.BRIGHTNESS_INVALID_FLOAT;
-            mAmbientLuxValid = mLightSensorController.hasValidAmbientLux();
+            mRecentLightSamples = 0;
+            mAmbientLightRingBuffer.clear();
+            mCurrentLightSensorRate = -1;
+            mHandler.removeMessages(MSG_UPDATE_AMBIENT_LUX);
             unregisterForegroundAppUpdater();
+            mSensorManager.unregisterListener(mLightSensorListener);
         }
         return false;
     }
 
+    private void handleLightSensorEvent(long time, float lux) {
+        Trace.traceCounter(Trace.TRACE_TAG_POWER, "ALS", (int) lux);
+        mHandler.removeMessages(MSG_UPDATE_AMBIENT_LUX);
+
+        if (mAmbientLightRingBuffer.size() == 0) {
+            // switch to using the steady-state sample rate after grabbing the initial light sample
+            adjustLightSensorRate(mNormalLightSensorRate);
+        }
+        applyLightSensorMeasurement(time, lux);
+        updateAmbientLux(time);
+    }
+
+    private void applyLightSensorMeasurement(long time, float lux) {
+        mRecentLightSamples++;
+        mAmbientLightRingBuffer.prune(time - mAmbientLightHorizonLong);
+        mAmbientLightRingBuffer.push(time, lux);
+
+        // Remember this sample value.
+        mLastObservedLux = lux;
+        mLastObservedLuxTime = time;
+    }
+
+    private void adjustLightSensorRate(int lightSensorRate) {
+        // if the light sensor rate changed, update the sensor listener
+        if (lightSensorRate != mCurrentLightSensorRate) {
+            if (mLoggingEnabled) {
+                Slog.d(TAG, "adjustLightSensorRate: " +
+                        "previousRate=" + mCurrentLightSensorRate + ", " +
+                        "currentRate=" + lightSensorRate);
+            }
+            mCurrentLightSensorRate = lightSensorRate;
+            mSensorManager.unregisterListener(mLightSensorListener);
+            mSensorManager.registerListener(mLightSensorListener, mLightSensor,
+                    lightSensorRate * 1000, mHandler);
+        }
+    }
+
     private boolean setAutoBrightnessAdjustment(float adjustment) {
         return mCurrentBrightnessMapper.setAutoBrightnessAdjustment(adjustment);
     }
 
     private void setAmbientLux(float lux) {
-        // called by LightSensorController.setAmbientLux
-        mAmbientLuxValid = true;
+        if (mLoggingEnabled) {
+            Slog.d(TAG, "setAmbientLux(" + lux + ")");
+        }
+        if (lux < 0) {
+            Slog.w(TAG, "Ambient lux was negative, ignoring and setting to 0");
+            lux = 0;
+        }
         mAmbientLux = lux;
+        if (isInIdleMode()) {
+            mAmbientBrighteningThreshold =
+                    mAmbientBrightnessThresholdsIdle.getBrighteningThreshold(lux);
+            mAmbientDarkeningThreshold =
+                    mAmbientBrightnessThresholdsIdle.getDarkeningThreshold(lux);
+        } else {
+            mAmbientBrighteningThreshold =
+                    mAmbientBrightnessThresholds.getBrighteningThreshold(lux);
+            mAmbientDarkeningThreshold =
+                    mAmbientBrightnessThresholds.getDarkeningThreshold(lux);
+        }
         mBrightnessRangeController.onAmbientLuxChange(mAmbientLux);
         mBrightnessClamperController.onAmbientLuxChange(mAmbientLux);
 
         // If the short term model was invalidated and the change is drastic enough, reset it.
         mShortTermModel.maybeReset(mAmbientLux);
-        updateAutoBrightness(true /* sendUpdate */, false /* isManuallySet */);
+    }
+
+    private float calculateAmbientLux(long now, long horizon) {
+        if (mLoggingEnabled) {
+            Slog.d(TAG, "calculateAmbientLux(" + now + ", " + horizon + ")");
+        }
+        final int N = mAmbientLightRingBuffer.size();
+        if (N == 0) {
+            Slog.e(TAG, "calculateAmbientLux: No ambient light readings available");
+            return -1;
+        }
+
+        // Find the first measurement that is just outside of the horizon.
+        int endIndex = 0;
+        final long horizonStartTime = now - horizon;
+        for (int i = 0; i < N-1; i++) {
+            if (mAmbientLightRingBuffer.getTime(i + 1) <= horizonStartTime) {
+                endIndex++;
+            } else {
+                break;
+            }
+        }
+        if (mLoggingEnabled) {
+            Slog.d(TAG, "calculateAmbientLux: selected endIndex=" + endIndex + ", point=(" +
+                    mAmbientLightRingBuffer.getTime(endIndex) + ", " +
+                    mAmbientLightRingBuffer.getLux(endIndex) + ")");
+        }
+        float sum = 0;
+        float totalWeight = 0;
+        long endTime = AMBIENT_LIGHT_PREDICTION_TIME_MILLIS;
+        for (int i = N - 1; i >= endIndex; i--) {
+            long eventTime = mAmbientLightRingBuffer.getTime(i);
+            if (i == endIndex && eventTime < horizonStartTime) {
+                // If we're at the final value, make sure we only consider the part of the sample
+                // within our desired horizon.
+                eventTime = horizonStartTime;
+            }
+            final long startTime = eventTime - now;
+            float weight = calculateWeight(startTime, endTime);
+            float lux = mAmbientLightRingBuffer.getLux(i);
+            if (mLoggingEnabled) {
+                Slog.d(TAG, "calculateAmbientLux: [" + startTime + ", " + endTime + "]: " +
+                        "lux=" + lux + ", " +
+                        "weight=" + weight);
+            }
+            totalWeight += weight;
+            sum += lux * weight;
+            endTime = startTime;
+        }
+        if (mLoggingEnabled) {
+            Slog.d(TAG, "calculateAmbientLux: " +
+                    "totalWeight=" + totalWeight + ", " +
+                    "newAmbientLux=" + (sum / totalWeight));
+        }
+        return sum / totalWeight;
+    }
+
+    private float calculateWeight(long startDelta, long endDelta) {
+        return weightIntegral(endDelta) - weightIntegral(startDelta);
+    }
+
+    // Evaluates the integral of y = x + mWeightingIntercept. This is always positive for the
+    // horizon we're looking at and provides a non-linear weighting for light samples.
+    private float weightIntegral(long x) {
+        return x * (x * 0.5f + mWeightingIntercept);
+    }
+
+    private long nextAmbientLightBrighteningTransition(long time) {
+        final int N = mAmbientLightRingBuffer.size();
+        long earliestValidTime = time;
+        for (int i = N - 1; i >= 0; i--) {
+            if (mAmbientLightRingBuffer.getLux(i) <= mAmbientBrighteningThreshold) {
+                break;
+            }
+            earliestValidTime = mAmbientLightRingBuffer.getTime(i);
+        }
+        return earliestValidTime + (isInIdleMode()
+                ? mBrighteningLightDebounceConfigIdle : mBrighteningLightDebounceConfig);
+    }
+
+    private long nextAmbientLightDarkeningTransition(long time) {
+        final int N = mAmbientLightRingBuffer.size();
+        long earliestValidTime = time;
+        for (int i = N - 1; i >= 0; i--) {
+            if (mAmbientLightRingBuffer.getLux(i) >= mAmbientDarkeningThreshold) {
+                break;
+            }
+            earliestValidTime = mAmbientLightRingBuffer.getTime(i);
+        }
+        return earliestValidTime + (isInIdleMode()
+                ? mDarkeningLightDebounceConfigIdle : mDarkeningLightDebounceConfig);
+    }
+
+    private void updateAmbientLux() {
+        long time = mClock.uptimeMillis();
+        mAmbientLightRingBuffer.prune(time - mAmbientLightHorizonLong);
+        updateAmbientLux(time);
+    }
+
+    private void updateAmbientLux(long time) {
+        // If the light sensor was just turned on then immediately update our initial
+        // estimate of the current ambient light level.
+        if (!mAmbientLuxValid) {
+            final long timeWhenSensorWarmedUp =
+                mLightSensorWarmUpTimeConfig + mLightSensorEnableTime;
+            if (time < timeWhenSensorWarmedUp) {
+                if (mLoggingEnabled) {
+                    Slog.d(TAG, "updateAmbientLux: Sensor not ready yet: "
+                            + "time=" + time + ", "
+                            + "timeWhenSensorWarmedUp=" + timeWhenSensorWarmedUp);
+                }
+                mHandler.sendEmptyMessageAtTime(MSG_UPDATE_AMBIENT_LUX,
+                        timeWhenSensorWarmedUp);
+                return;
+            }
+            setAmbientLux(calculateAmbientLux(time, mAmbientLightHorizonShort));
+            mAmbientLuxValid = true;
+            if (mLoggingEnabled) {
+                Slog.d(TAG, "updateAmbientLux: Initializing: " +
+                        "mAmbientLightRingBuffer=" + mAmbientLightRingBuffer + ", " +
+                        "mAmbientLux=" + mAmbientLux);
+            }
+            updateAutoBrightness(true /* sendUpdate */, false /* isManuallySet */);
+        }
+
+        long nextBrightenTransition = nextAmbientLightBrighteningTransition(time);
+        long nextDarkenTransition = nextAmbientLightDarkeningTransition(time);
+        // Essentially, we calculate both a slow ambient lux, to ensure there's a true long-term
+        // change in lighting conditions, and a fast ambient lux to determine what the new
+        // brightness situation is since the slow lux can be quite slow to converge.
+        //
+        // Note that both values need to be checked for sufficient change before updating the
+        // proposed ambient light value since the slow value might be sufficiently far enough away
+        // from the fast value to cause a recalculation while its actually just converging on
+        // the fast value still.
+        mSlowAmbientLux = calculateAmbientLux(time, mAmbientLightHorizonLong);
+        mFastAmbientLux = calculateAmbientLux(time, mAmbientLightHorizonShort);
+
+        if ((mSlowAmbientLux >= mAmbientBrighteningThreshold
+                && mFastAmbientLux >= mAmbientBrighteningThreshold
+                && nextBrightenTransition <= time)
+                || (mSlowAmbientLux <= mAmbientDarkeningThreshold
+                        && mFastAmbientLux <= mAmbientDarkeningThreshold
+                        && nextDarkenTransition <= time)) {
+            mPreThresholdLux = mAmbientLux;
+            setAmbientLux(mFastAmbientLux);
+            if (mLoggingEnabled) {
+                Slog.d(TAG, "updateAmbientLux: "
+                        + ((mFastAmbientLux > mAmbientLux) ? "Brightened" : "Darkened") + ": "
+                        + "mAmbientBrighteningThreshold=" + mAmbientBrighteningThreshold + ", "
+                        + "mAmbientDarkeningThreshold=" + mAmbientDarkeningThreshold + ", "
+                        + "mAmbientLightRingBuffer=" + mAmbientLightRingBuffer + ", "
+                        + "mAmbientLux=" + mAmbientLux);
+            }
+            updateAutoBrightness(true /* sendUpdate */, false /* isManuallySet */);
+            nextBrightenTransition = nextAmbientLightBrighteningTransition(time);
+            nextDarkenTransition = nextAmbientLightDarkeningTransition(time);
+        }
+        long nextTransitionTime = Math.min(nextDarkenTransition, nextBrightenTransition);
+        // If one of the transitions is ready to occur, but the total weighted ambient lux doesn't
+        // exceed the necessary threshold, then it's possible we'll get a transition time prior to
+        // now. Rather than continually checking to see whether the weighted lux exceeds the
+        // threshold, schedule an update for when we'd normally expect another light sample, which
+        // should be enough time to decide whether we should actually transition to the new
+        // weighted ambient lux or not.
+        nextTransitionTime =
+                nextTransitionTime > time ? nextTransitionTime : time + mNormalLightSensorRate;
+        if (mLoggingEnabled) {
+            Slog.d(TAG, "updateAmbientLux: Scheduling ambient lux update for " +
+                    nextTransitionTime + TimeUtils.formatUptime(nextTransitionTime));
+        }
+        mHandler.sendEmptyMessageAtTime(MSG_UPDATE_AMBIENT_LUX, nextTransitionTime);
     }
 
     private void updateAutoBrightness(boolean sendUpdate, boolean isManuallySet) {
@@ -678,7 +1072,8 @@
                 if (mLoggingEnabled) {
                     Slog.d(TAG, "Auto-brightness adjustment changed by user: "
                             + "lux=" + mAmbientLux + ", "
-                            + "brightness=" + mScreenAutoBrightness);
+                            + "brightness=" + mScreenAutoBrightness + ", "
+                            + "ring=" + mAmbientLightRingBuffer);
                 }
 
                 EventLog.writeEvent(EventLogTags.AUTO_BRIGHTNESS_ADJ,
@@ -808,7 +1203,6 @@
         if (mode == AUTO_BRIGHTNESS_MODE_IDLE
                 || mCurrentBrightnessMapper.getMode() == AUTO_BRIGHTNESS_MODE_IDLE) {
             switchModeAndShortTermModels(mode);
-            mLightSensorController.setIdleMode(isInIdleMode());
         } else {
             resetShortTermModel();
             mCurrentBrightnessMapper = mBrightnessMappingStrategyMap.get(mode);
@@ -965,6 +1359,10 @@
                     updateAutoBrightness(true /*sendUpdate*/, false /*isManuallySet*/);
                     break;
 
+                case MSG_UPDATE_AMBIENT_LUX:
+                    updateAmbientLux();
+                    break;
+
                 case MSG_BRIGHTNESS_ADJUSTMENT_SAMPLE:
                     collectBrightnessAdjustmentSample();
                     break;
@@ -988,6 +1386,22 @@
         }
     }
 
+    private final SensorEventListener mLightSensorListener = new SensorEventListener() {
+        @Override
+        public void onSensorChanged(SensorEvent event) {
+            if (mLightSensorEnabled) {
+                final long time = mClock.uptimeMillis();
+                final float lux = event.values[0];
+                handleLightSensorEvent(time, lux);
+            }
+        }
+
+        @Override
+        public void onAccuracyChanged(Sensor sensor, int accuracy) {
+            // Not used.
+        }
+    };
+
     // Call back whenever the tasks stack changes, which includes tasks being created, removed, and
     // moving to top.
     class TaskStackListenerImpl extends TaskStackListener {
@@ -1002,13 +1416,192 @@
         void updateBrightness();
     }
 
+    /** Functional interface for providing time. */
+    @VisibleForTesting
+    interface Clock {
+        /**
+         * Returns current time in milliseconds since boot, not counting time spent in deep sleep.
+         */
+        long uptimeMillis();
+    }
+
+    /**
+     * A ring buffer of ambient light measurements sorted by time.
+     *
+     * Each entry consists of a timestamp and a lux measurement, and the overall buffer is sorted
+     * from oldest to newest.
+     */
+    private static final class AmbientLightRingBuffer {
+        // Proportional extra capacity of the buffer beyond the expected number of light samples
+        // in the horizon
+        private static final float BUFFER_SLACK = 1.5f;
+        private float[] mRingLux;
+        private long[] mRingTime;
+        private int mCapacity;
+
+        // The first valid element and the next open slot.
+        // Note that if mCount is zero then there are no valid elements.
+        private int mStart;
+        private int mEnd;
+        private int mCount;
+        Clock mClock;
+
+        public AmbientLightRingBuffer(long lightSensorRate, int ambientLightHorizon, Clock clock) {
+            if (lightSensorRate <= 0) {
+                throw new IllegalArgumentException("lightSensorRate must be above 0");
+            }
+            mCapacity = (int) Math.ceil(ambientLightHorizon * BUFFER_SLACK / lightSensorRate);
+            mRingLux = new float[mCapacity];
+            mRingTime = new long[mCapacity];
+            mClock = clock;
+        }
+
+        public float getLux(int index) {
+            return mRingLux[offsetOf(index)];
+        }
+
+        public float[] getAllLuxValues() {
+            float[] values = new float[mCount];
+            if (mCount == 0) {
+                return values;
+            }
+
+            if (mStart < mEnd) {
+                System.arraycopy(mRingLux, mStart, values, 0, mCount);
+            } else {
+                System.arraycopy(mRingLux, mStart, values, 0, mCapacity - mStart);
+                System.arraycopy(mRingLux, 0, values, mCapacity - mStart, mEnd);
+            }
+
+            return values;
+        }
+
+        public long getTime(int index) {
+            return mRingTime[offsetOf(index)];
+        }
+
+        public long[] getAllTimestamps() {
+            long[] values = new long[mCount];
+            if (mCount == 0) {
+                return values;
+            }
+
+            if (mStart < mEnd) {
+                System.arraycopy(mRingTime, mStart, values, 0, mCount);
+            } else {
+                System.arraycopy(mRingTime, mStart, values, 0, mCapacity - mStart);
+                System.arraycopy(mRingTime, 0, values, mCapacity - mStart, mEnd);
+            }
+
+            return values;
+        }
+
+        public void push(long time, float lux) {
+            int next = mEnd;
+            if (mCount == mCapacity) {
+                int newSize = mCapacity * 2;
+
+                float[] newRingLux = new float[newSize];
+                long[] newRingTime = new long[newSize];
+                int length = mCapacity - mStart;
+                System.arraycopy(mRingLux, mStart, newRingLux, 0, length);
+                System.arraycopy(mRingTime, mStart, newRingTime, 0, length);
+                if (mStart != 0) {
+                    System.arraycopy(mRingLux, 0, newRingLux, length, mStart);
+                    System.arraycopy(mRingTime, 0, newRingTime, length, mStart);
+                }
+                mRingLux = newRingLux;
+                mRingTime = newRingTime;
+
+                next = mCapacity;
+                mCapacity = newSize;
+                mStart = 0;
+            }
+            mRingTime[next] = time;
+            mRingLux[next] = lux;
+            mEnd = next + 1;
+            if (mEnd == mCapacity) {
+                mEnd = 0;
+            }
+            mCount++;
+        }
+
+        public void prune(long horizon) {
+            if (mCount == 0) {
+                return;
+            }
+
+            while (mCount > 1) {
+                int next = mStart + 1;
+                if (next >= mCapacity) {
+                    next -= mCapacity;
+                }
+                if (mRingTime[next] > horizon) {
+                    // Some light sensors only produce data upon a change in the ambient light
+                    // levels, so we need to consider the previous measurement as the ambient light
+                    // level for all points in time up until we receive a new measurement. Thus, we
+                    // always want to keep the youngest element that would be removed from the
+                    // buffer and just set its measurement time to the horizon time since at that
+                    // point it is the ambient light level, and to remove it would be to drop a
+                    // valid data point within our horizon.
+                    break;
+                }
+                mStart = next;
+                mCount -= 1;
+            }
+
+            if (mRingTime[mStart] < horizon) {
+                mRingTime[mStart] = horizon;
+            }
+        }
+
+        public int size() {
+            return mCount;
+        }
+
+        public void clear() {
+            mStart = 0;
+            mEnd = 0;
+            mCount = 0;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder buf = new StringBuilder();
+            buf.append('[');
+            for (int i = 0; i < mCount; i++) {
+                final long next = i + 1 < mCount ? getTime(i + 1) : mClock.uptimeMillis();
+                if (i != 0) {
+                    buf.append(", ");
+                }
+                buf.append(getLux(i));
+                buf.append(" / ");
+                buf.append(next - getTime(i));
+                buf.append("ms");
+            }
+            buf.append(']');
+            return buf.toString();
+        }
+
+        private int offsetOf(int index) {
+            if (index >= mCount || index < 0) {
+                throw new ArrayIndexOutOfBoundsException(index);
+            }
+            index += mStart;
+            if (index >= mCapacity) {
+                index -= mCapacity;
+            }
+            return index;
+        }
+    }
+
     public static class Injector {
         public Handler getBackgroundThreadHandler() {
             return BackgroundThread.getHandler();
         }
 
         Clock createClock() {
-            return Clock.SYSTEM_CLOCK;
+            return SystemClock::uptimeMillis;
         }
     }
 }
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index 641b6a2..61ecb93 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -1930,7 +1930,6 @@
      *
      * @return true if even dimmer mode is enabled
      */
-    @VisibleForTesting
     public boolean isEvenDimmerAvailable() {
         return mEvenDimmerBrightnessData != null;
     }
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index ba21a32..434985e 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -464,10 +464,11 @@
     // May be used outside of the lock but only on the handler thread.
     private final ArrayList<CallbackRecord> mTempCallbacks = new ArrayList<CallbackRecord>();
 
-    // Pending callback records indexed by calling process uid.
+    // Pending callback records indexed by calling process uid and pid.
     // Must be used outside of the lock mSyncRoot and should be selflocked.
     @GuardedBy("mPendingCallbackSelfLocked")
-    public final SparseArray<PendingCallback> mPendingCallbackSelfLocked = new SparseArray<>();
+    public final SparseArray<SparseArray<PendingCallback>> mPendingCallbackSelfLocked =
+            new SparseArray<>();
 
     // Temporary viewports, used when sending new viewport information to the
     // input system.  May be used outside of the lock but only on the handler thread.
@@ -1011,8 +1012,8 @@
                 }
 
                 // Do we care about this uid?
-                PendingCallback pendingCallback = mPendingCallbackSelfLocked.get(uid);
-                if (pendingCallback == null) {
+                SparseArray<PendingCallback> pendingCallbacks = mPendingCallbackSelfLocked.get(uid);
+                if (pendingCallbacks == null) {
                     return;
                 }
 
@@ -1020,7 +1021,12 @@
                 if (DEBUG) {
                     Slog.d(TAG, "Uid " + uid + " becomes " + importance);
                 }
-                pendingCallback.sendPendingDisplayEvent();
+                for (int i = 0; i < pendingCallbacks.size(); i++) {
+                    PendingCallback pendingCallback = pendingCallbacks.valueAt(i);
+                    if (pendingCallback != null) {
+                        pendingCallback.sendPendingDisplayEvent();
+                    }
+                }
                 mPendingCallbackSelfLocked.delete(uid);
             }
         }
@@ -3193,16 +3199,23 @@
         for (int i = 0; i < mTempCallbacks.size(); i++) {
             CallbackRecord callbackRecord = mTempCallbacks.get(i);
             final int uid = callbackRecord.mUid;
+            final int pid = callbackRecord.mPid;
             if (isUidCached(uid)) {
                 // For cached apps, save the pending event until it becomes non-cached
                 synchronized (mPendingCallbackSelfLocked) {
-                    PendingCallback pendingCallback = mPendingCallbackSelfLocked.get(uid);
+                    SparseArray<PendingCallback> pendingCallbacks = mPendingCallbackSelfLocked.get(
+                            uid);
                     if (extraLogging(callbackRecord.mPackageName)) {
-                        Slog.i(TAG,
-                                "Uid is cached: " + uid + ", pendingCallback: " + pendingCallback);
+                        Slog.i(TAG, "Uid is cached: " + uid
+                                + ", pendingCallbacks: " + pendingCallbacks);
                     }
+                    if (pendingCallbacks == null) {
+                        pendingCallbacks = new SparseArray<>();
+                        mPendingCallbackSelfLocked.put(uid, pendingCallbacks);
+                    }
+                    PendingCallback pendingCallback = pendingCallbacks.get(pid);
                     if (pendingCallback == null) {
-                        mPendingCallbackSelfLocked.put(uid,
+                        pendingCallbacks.put(pid,
                                 new PendingCallback(callbackRecord, displayId, event));
                     } else {
                         pendingCallback.addDisplayEvent(displayId, event);
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 043ff0e..b21a89a 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -82,7 +82,6 @@
 import com.android.server.display.brightness.BrightnessReason;
 import com.android.server.display.brightness.BrightnessUtils;
 import com.android.server.display.brightness.DisplayBrightnessController;
-import com.android.server.display.brightness.LightSensorController;
 import com.android.server.display.brightness.clamper.BrightnessClamperController;
 import com.android.server.display.brightness.strategy.AutomaticBrightnessStrategy;
 import com.android.server.display.color.ColorDisplayService.ColorDisplayServiceInternal;
@@ -1050,13 +1049,102 @@
         }
 
         if (defaultModeBrightnessMapper != null) {
+            // Ambient Lux - Active Mode Brightness Thresholds
+            float[] ambientBrighteningThresholds =
+                    mDisplayDeviceConfig.getAmbientBrighteningPercentages();
+            float[] ambientDarkeningThresholds =
+                    mDisplayDeviceConfig.getAmbientDarkeningPercentages();
+            float[] ambientBrighteningLevels =
+                    mDisplayDeviceConfig.getAmbientBrighteningLevels();
+            float[] ambientDarkeningLevels =
+                    mDisplayDeviceConfig.getAmbientDarkeningLevels();
+            float ambientDarkeningMinThreshold =
+                    mDisplayDeviceConfig.getAmbientLuxDarkeningMinThreshold();
+            float ambientBrighteningMinThreshold =
+                    mDisplayDeviceConfig.getAmbientLuxBrighteningMinThreshold();
+            HysteresisLevels ambientBrightnessThresholds = mInjector.getHysteresisLevels(
+                    ambientBrighteningThresholds, ambientDarkeningThresholds,
+                    ambientBrighteningLevels, ambientDarkeningLevels, ambientDarkeningMinThreshold,
+                    ambientBrighteningMinThreshold);
+
             // Display - Active Mode Brightness Thresholds
-            HysteresisLevels screenBrightnessThresholds =
-                    mInjector.getBrightnessThresholdsHysteresisLevels(mDisplayDeviceConfig);
+            float[] screenBrighteningThresholds =
+                    mDisplayDeviceConfig.getScreenBrighteningPercentages();
+            float[] screenDarkeningThresholds =
+                    mDisplayDeviceConfig.getScreenDarkeningPercentages();
+            float[] screenBrighteningLevels =
+                    mDisplayDeviceConfig.getScreenBrighteningLevels();
+            float[] screenDarkeningLevels =
+                    mDisplayDeviceConfig.getScreenDarkeningLevels();
+            float screenDarkeningMinThreshold =
+                    mDisplayDeviceConfig.getScreenDarkeningMinThreshold();
+            float screenBrighteningMinThreshold =
+                    mDisplayDeviceConfig.getScreenBrighteningMinThreshold();
+            HysteresisLevels screenBrightnessThresholds = mInjector.getHysteresisLevels(
+                    screenBrighteningThresholds, screenDarkeningThresholds,
+                    screenBrighteningLevels, screenDarkeningLevels, screenDarkeningMinThreshold,
+                    screenBrighteningMinThreshold, true);
+
+            // Ambient Lux - Idle Screen Brightness Thresholds
+            float ambientDarkeningMinThresholdIdle =
+                    mDisplayDeviceConfig.getAmbientLuxDarkeningMinThresholdIdle();
+            float ambientBrighteningMinThresholdIdle =
+                    mDisplayDeviceConfig.getAmbientLuxBrighteningMinThresholdIdle();
+            float[] ambientBrighteningThresholdsIdle =
+                    mDisplayDeviceConfig.getAmbientBrighteningPercentagesIdle();
+            float[] ambientDarkeningThresholdsIdle =
+                    mDisplayDeviceConfig.getAmbientDarkeningPercentagesIdle();
+            float[] ambientBrighteningLevelsIdle =
+                    mDisplayDeviceConfig.getAmbientBrighteningLevelsIdle();
+            float[] ambientDarkeningLevelsIdle =
+                    mDisplayDeviceConfig.getAmbientDarkeningLevelsIdle();
+            HysteresisLevels ambientBrightnessThresholdsIdle = mInjector.getHysteresisLevels(
+                    ambientBrighteningThresholdsIdle, ambientDarkeningThresholdsIdle,
+                    ambientBrighteningLevelsIdle, ambientDarkeningLevelsIdle,
+                    ambientDarkeningMinThresholdIdle, ambientBrighteningMinThresholdIdle);
 
             // Display - Idle Screen Brightness Thresholds
-            HysteresisLevels screenBrightnessThresholdsIdle =
-                    mInjector.getBrightnessThresholdsIdleHysteresisLevels(mDisplayDeviceConfig);
+            float screenDarkeningMinThresholdIdle =
+                    mDisplayDeviceConfig.getScreenDarkeningMinThresholdIdle();
+            float screenBrighteningMinThresholdIdle =
+                    mDisplayDeviceConfig.getScreenBrighteningMinThresholdIdle();
+            float[] screenBrighteningThresholdsIdle =
+                    mDisplayDeviceConfig.getScreenBrighteningPercentagesIdle();
+            float[] screenDarkeningThresholdsIdle =
+                    mDisplayDeviceConfig.getScreenDarkeningPercentagesIdle();
+            float[] screenBrighteningLevelsIdle =
+                    mDisplayDeviceConfig.getScreenBrighteningLevelsIdle();
+            float[] screenDarkeningLevelsIdle =
+                    mDisplayDeviceConfig.getScreenDarkeningLevelsIdle();
+            HysteresisLevels screenBrightnessThresholdsIdle = mInjector.getHysteresisLevels(
+                    screenBrighteningThresholdsIdle, screenDarkeningThresholdsIdle,
+                    screenBrighteningLevelsIdle, screenDarkeningLevelsIdle,
+                    screenDarkeningMinThresholdIdle, screenBrighteningMinThresholdIdle);
+
+            long brighteningLightDebounce = mDisplayDeviceConfig
+                    .getAutoBrightnessBrighteningLightDebounce();
+            long darkeningLightDebounce = mDisplayDeviceConfig
+                    .getAutoBrightnessDarkeningLightDebounce();
+            long brighteningLightDebounceIdle = mDisplayDeviceConfig
+                    .getAutoBrightnessBrighteningLightDebounceIdle();
+            long darkeningLightDebounceIdle = mDisplayDeviceConfig
+                    .getAutoBrightnessDarkeningLightDebounceIdle();
+            boolean autoBrightnessResetAmbientLuxAfterWarmUp = context.getResources().getBoolean(
+                    R.bool.config_autoBrightnessResetAmbientLuxAfterWarmUp);
+
+            int lightSensorWarmUpTimeConfig = context.getResources().getInteger(
+                    R.integer.config_lightSensorWarmupTime);
+            int lightSensorRate = context.getResources().getInteger(
+                    R.integer.config_autoBrightnessLightSensorRate);
+            int initialLightSensorRate = context.getResources().getInteger(
+                    R.integer.config_autoBrightnessInitialLightSensorRate);
+            if (initialLightSensorRate == -1) {
+                initialLightSensorRate = lightSensorRate;
+            } else if (initialLightSensorRate > lightSensorRate) {
+                Slog.w(mTag, "Expected config_autoBrightnessInitialLightSensorRate ("
+                        + initialLightSensorRate + ") to be less than or equal to "
+                        + "config_autoBrightnessLightSensorRate (" + lightSensorRate + ").");
+            }
 
             loadAmbientLightSensor();
             // BrightnessTracker should only use one light sensor, we want to use the light sensor
@@ -1068,15 +1156,17 @@
             if (mAutomaticBrightnessController != null) {
                 mAutomaticBrightnessController.stop();
             }
-
-            LightSensorController.LightSensorControllerConfig config =
-                    mInjector.getLightSensorControllerConfig(context, mDisplayDeviceConfig);
             mAutomaticBrightnessController = mInjector.getAutomaticBrightnessController(
-                    this, handler.getLooper(), mSensorManager, brightnessMappers,
-                    PowerManager.BRIGHTNESS_MIN, PowerManager.BRIGHTNESS_MAX, mDozeScaleFactor,
-                    screenBrightnessThresholds, screenBrightnessThresholdsIdle,
-                    mContext, mBrightnessRangeController,
-                    mBrightnessThrottler, userLux, userNits, mDisplayId, config,
+                    this, handler.getLooper(), mSensorManager, mLightSensor,
+                    brightnessMappers, lightSensorWarmUpTimeConfig, PowerManager.BRIGHTNESS_MIN,
+                    PowerManager.BRIGHTNESS_MAX, mDozeScaleFactor, lightSensorRate,
+                    initialLightSensorRate, brighteningLightDebounce, darkeningLightDebounce,
+                    brighteningLightDebounceIdle, darkeningLightDebounceIdle,
+                    autoBrightnessResetAmbientLuxAfterWarmUp, ambientBrightnessThresholds,
+                    screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
+                    screenBrightnessThresholdsIdle, mContext, mBrightnessRangeController,
+                    mBrightnessThrottler, mDisplayDeviceConfig.getAmbientHorizonShort(),
+                    mDisplayDeviceConfig.getAmbientHorizonLong(), userLux, userNits,
                     mBrightnessClamperController);
             mDisplayBrightnessController.setAutomaticBrightnessController(
                     mAutomaticBrightnessController);
@@ -3083,34 +3173,32 @@
 
         AutomaticBrightnessController getAutomaticBrightnessController(
                 AutomaticBrightnessController.Callbacks callbacks, Looper looper,
-                SensorManager sensorManager,
+                SensorManager sensorManager, Sensor lightSensor,
                 SparseArray<BrightnessMappingStrategy> brightnessMappingStrategyMap,
-                float brightnessMin, float brightnessMax, float dozeScaleFactor,
+                int lightSensorWarmUpTime, float brightnessMin, float brightnessMax,
+                float dozeScaleFactor, int lightSensorRate, int initialLightSensorRate,
+                long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
+                long brighteningLightDebounceConfigIdle, long darkeningLightDebounceConfigIdle,
+                boolean resetAmbientLuxAfterWarmUpConfig,
+                HysteresisLevels ambientBrightnessThresholds,
                 HysteresisLevels screenBrightnessThresholds,
+                HysteresisLevels ambientBrightnessThresholdsIdle,
                 HysteresisLevels screenBrightnessThresholdsIdle, Context context,
                 BrightnessRangeController brightnessModeController,
-                BrightnessThrottler brightnessThrottler, float userLux, float userNits,
-                int displayId, LightSensorController.LightSensorControllerConfig config,
+                BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
+                int ambientLightHorizonLong, float userLux, float userNits,
                 BrightnessClamperController brightnessClamperController) {
-            return new AutomaticBrightnessController(callbacks, looper, sensorManager,
-                    brightnessMappingStrategyMap, brightnessMin, brightnessMax, dozeScaleFactor,
-                    screenBrightnessThresholds, screenBrightnessThresholdsIdle, context,
-                    brightnessModeController, brightnessThrottler, userLux, userNits, displayId,
-                    config, brightnessClamperController);
-        }
 
-        LightSensorController.LightSensorControllerConfig getLightSensorControllerConfig(
-                Context context, DisplayDeviceConfig displayDeviceConfig) {
-            return LightSensorController.LightSensorControllerConfig.create(
-                    context.getResources(), displayDeviceConfig);
-        }
-
-        HysteresisLevels getBrightnessThresholdsIdleHysteresisLevels(DisplayDeviceConfig ddc) {
-            return HysteresisLevels.getBrightnessThresholdsIdle(ddc);
-        }
-
-        HysteresisLevels getBrightnessThresholdsHysteresisLevels(DisplayDeviceConfig ddc) {
-            return HysteresisLevels.getBrightnessThresholds(ddc);
+            return new AutomaticBrightnessController(callbacks, looper, sensorManager, lightSensor,
+                    brightnessMappingStrategyMap, lightSensorWarmUpTime, brightnessMin,
+                    brightnessMax, dozeScaleFactor, lightSensorRate, initialLightSensorRate,
+                    brighteningLightDebounceConfig, darkeningLightDebounceConfig,
+                    brighteningLightDebounceConfigIdle, darkeningLightDebounceConfigIdle,
+                    resetAmbientLuxAfterWarmUpConfig, ambientBrightnessThresholds,
+                    screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
+                    screenBrightnessThresholdsIdle, context, brightnessModeController,
+                    brightnessThrottler, ambientLightHorizonShort, ambientLightHorizonLong, userLux,
+                    userNits, brightnessClamperController);
         }
 
         BrightnessMappingStrategy getDefaultModeBrightnessMapper(Context context,
@@ -3120,6 +3208,25 @@
                     AUTO_BRIGHTNESS_MODE_DEFAULT, displayWhiteBalanceController);
         }
 
+        HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+                float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+                float[] darkeningThresholdLevels, float minDarkeningThreshold,
+                float minBrighteningThreshold) {
+            return new HysteresisLevels(brighteningThresholdsPercentages,
+                    darkeningThresholdsPercentages, brighteningThresholdLevels,
+                    darkeningThresholdLevels, minDarkeningThreshold, minBrighteningThreshold);
+        }
+
+        HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+                float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+                float[] darkeningThresholdLevels, float minDarkeningThreshold,
+                float minBrighteningThreshold, boolean potentialOldBrightnessRange) {
+            return new HysteresisLevels(brighteningThresholdsPercentages,
+                    darkeningThresholdsPercentages, brighteningThresholdLevels,
+                    darkeningThresholdLevels, minDarkeningThreshold, minBrighteningThreshold,
+                    potentialOldBrightnessRange);
+        }
+
         ScreenOffBrightnessSensorController getScreenOffBrightnessSensorController(
                 SensorManager sensorManager,
                 Sensor lightSensor,
diff --git a/services/core/java/com/android/server/display/HysteresisLevels.java b/services/core/java/com/android/server/display/HysteresisLevels.java
index bb349e7..0521b8a 100644
--- a/services/core/java/com/android/server/display/HysteresisLevels.java
+++ b/services/core/java/com/android/server/display/HysteresisLevels.java
@@ -18,7 +18,6 @@
 
 import android.util.Slog;
 
-import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.display.utils.DebugUtils;
 
 import java.io.PrintWriter;
@@ -53,8 +52,7 @@
      * @param potentialOldBrightnessRange whether or not the values used could be from the old
      *                                    screen brightness range ie, between 1-255.
     */
-    @VisibleForTesting
-    public HysteresisLevels(float[] brighteningThresholdsPercentages,
+    HysteresisLevels(float[] brighteningThresholdsPercentages,
             float[] darkeningThresholdsPercentages,
             float[] brighteningThresholdLevels, float[] darkeningThresholdLevels,
             float minDarkeningThreshold, float minBrighteningThreshold,
@@ -140,10 +138,7 @@
         return levelArray;
     }
 
-    /**
-     * Print the object's debug information into the given stream.
-     */
-    public void dump(PrintWriter pw) {
+    void dump(PrintWriter pw) {
         pw.println("HysteresisLevels");
         pw.println("  mBrighteningThresholdLevels=" + Arrays.toString(mBrighteningThresholdLevels));
         pw.println("  mBrighteningThresholdsPercentages="
@@ -154,45 +149,4 @@
                 + Arrays.toString(mDarkeningThresholdsPercentages));
         pw.println("  mMinDarkening=" + mMinDarkening);
     }
-
-
-    /**
-     * Creates hysteresis levels for Active Ambient Lux
-     */
-    public static HysteresisLevels getAmbientBrightnessThresholds(DisplayDeviceConfig ddc) {
-        return new HysteresisLevels(ddc.getAmbientBrighteningPercentages(),
-                ddc.getAmbientDarkeningPercentages(), ddc.getAmbientBrighteningLevels(),
-                ddc.getAmbientDarkeningLevels(), ddc.getAmbientLuxDarkeningMinThreshold(),
-                ddc.getAmbientLuxBrighteningMinThreshold());
-    }
-
-    /**
-     * Creates hysteresis levels for Active Screen Brightness
-     */
-    public static HysteresisLevels getBrightnessThresholds(DisplayDeviceConfig ddc) {
-        return new HysteresisLevels(ddc.getScreenBrighteningPercentages(),
-                ddc.getScreenDarkeningPercentages(), ddc.getScreenBrighteningLevels(),
-                ddc.getScreenDarkeningLevels(), ddc.getScreenDarkeningMinThreshold(),
-                ddc.getScreenBrighteningMinThreshold(), true);
-    }
-
-    /**
-     * Creates hysteresis levels for Idle Ambient Lux
-     */
-    public static HysteresisLevels getAmbientBrightnessThresholdsIdle(DisplayDeviceConfig ddc) {
-        return new HysteresisLevels(ddc.getAmbientBrighteningPercentagesIdle(),
-                ddc.getAmbientDarkeningPercentagesIdle(), ddc.getAmbientBrighteningLevelsIdle(),
-                ddc.getAmbientDarkeningLevelsIdle(), ddc.getAmbientLuxDarkeningMinThresholdIdle(),
-                ddc.getAmbientLuxBrighteningMinThresholdIdle());
-    }
-
-    /**
-     * Creates hysteresis levels for Idle Screen Brightness
-     */
-    public static HysteresisLevels getBrightnessThresholdsIdle(DisplayDeviceConfig ddc) {
-        return new HysteresisLevels(ddc.getScreenBrighteningPercentagesIdle(),
-                ddc.getScreenDarkeningPercentagesIdle(), ddc.getScreenBrighteningLevelsIdle(),
-                ddc.getScreenDarkeningLevelsIdle(), ddc.getScreenDarkeningMinThresholdIdle(),
-                ddc.getScreenBrighteningMinThresholdIdle());
-    }
 }
diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
index a577e22..1dfe037 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -940,7 +940,9 @@
                             final float nits = backlightToNits(backlight);
                             final float sdrNits = backlightToNits(sdrBacklight);
 
-                            if (getFeatureFlags().isEvenDimmerEnabled()) {
+                            if (getFeatureFlags().isEvenDimmerEnabled()
+                                    && mDisplayDeviceConfig != null
+                                    && mDisplayDeviceConfig.isEvenDimmerAvailable()) {
                                 applyColorMatrixBasedDimming(brightnessState);
                             }
 
diff --git a/services/core/java/com/android/server/display/brightness/LightSensorController.java b/services/core/java/com/android/server/display/brightness/LightSensorController.java
deleted file mode 100644
index d82d698..0000000
--- a/services/core/java/com/android/server/display/brightness/LightSensorController.java
+++ /dev/null
@@ -1,868 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.display.brightness;
-
-import static com.android.server.display.BrightnessMappingStrategy.INVALID_LUX;
-
-import android.annotation.Nullable;
-import android.content.res.Resources;
-import android.hardware.Sensor;
-import android.hardware.SensorEvent;
-import android.hardware.SensorEventListener;
-import android.hardware.SensorManager;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.PowerManager;
-import android.os.Trace;
-import android.util.Slog;
-import android.util.TimeUtils;
-import android.view.Display;
-
-import com.android.internal.R;
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.os.Clock;
-import com.android.server.display.DisplayDeviceConfig;
-import com.android.server.display.HysteresisLevels;
-import com.android.server.display.config.SensorData;
-import com.android.server.display.utils.SensorUtils;
-
-import java.io.PrintWriter;
-
-/**
- * Manages light sensor subscription and notifies its listeners about ambient lux changes based on
- * configuration
- */
-public class LightSensorController {
-    // How long the current sensor reading is assumed to be valid beyond the current time.
-    // This provides a bit of prediction, as well as ensures that the weight for the last sample is
-    // non-zero, which in turn ensures that the total weight is non-zero.
-    private static final long AMBIENT_LIGHT_PREDICTION_TIME_MILLIS = 100;
-
-    // Proportional extra capacity of the buffer beyond the expected number of light samples
-    // in the horizon
-    private static final float BUFFER_SLACK = 1.5f;
-
-    private boolean mLoggingEnabled;
-    private boolean mLightSensorEnabled;
-    private long mLightSensorEnableTime;
-    // The current light sensor event rate in milliseconds.
-    private int mCurrentLightSensorRate = -1;
-    // The number of light samples collected since the light sensor was enabled.
-    private int mRecentLightSamples;
-    private float mAmbientLux;
-    // True if mAmbientLux holds a valid value.
-    private boolean mAmbientLuxValid;
-    // The last ambient lux value prior to passing the darkening or brightening threshold.
-    private float mPreThresholdLux;
-    // The most recent light sample.
-    private float mLastObservedLux = INVALID_LUX;
-    // The time of the most light recent sample.
-    private long mLastObservedLuxTime;
-    // The last calculated ambient light level (long time window).
-    private float mSlowAmbientLux;
-    // The last calculated ambient light level (short time window).
-    private float mFastAmbientLux;
-    private volatile boolean mIsIdleMode;
-    // The ambient light level threshold at which to brighten or darken the screen.
-    private float mAmbientBrighteningThreshold;
-    private float mAmbientDarkeningThreshold;
-
-    private final LightSensorControllerConfig mConfig;
-
-    // The light sensor, or null if not available or needed.
-    @Nullable
-    private final Sensor mLightSensor;
-
-    // A ring buffer containing all of the recent ambient light sensor readings.
-    private final AmbientLightRingBuffer mAmbientLightRingBuffer;
-
-    private final Injector mInjector;
-
-    private final SensorEventListener mLightSensorListener = new SensorEventListener() {
-        @Override
-        public void onSensorChanged(SensorEvent event) {
-            if (mLightSensorEnabled) {
-                final long time = mClock.uptimeMillis();
-                final float lux = event.values[0];
-                handleLightSensorEvent(time, lux);
-            }
-        }
-
-        @Override
-        public void onAccuracyChanged(Sensor sensor, int accuracy) {
-            // Not used.
-        }
-    };
-
-    // Runnable used to delay ambient lux update when:
-    // 1) update triggered before configured warm up time
-    // 2) next brightening or darkening transition need to happen
-    private final Runnable mAmbientLuxUpdater = this::updateAmbientLux;
-
-    private final Clock mClock;
-
-    private final Handler mHandler;
-
-    private final String mTag;
-
-    private LightSensorListener mListener;
-
-    public LightSensorController(
-            SensorManager sensorManager,
-            Looper looper,
-            int displayId,
-            LightSensorControllerConfig config) {
-        this(config, new RealInjector(sensorManager, displayId), new LightSensorHandler(looper));
-    }
-
-    @VisibleForTesting
-    LightSensorController(
-            LightSensorControllerConfig config,
-            Injector injector,
-            Handler handler) {
-        if (config.mNormalLightSensorRate <= 0) {
-            throw new IllegalArgumentException("lightSensorRate must be above 0");
-        }
-        mInjector = injector;
-        int bufferInitialCapacity = (int) Math.ceil(
-                config.mAmbientLightHorizonLong * BUFFER_SLACK / config.mNormalLightSensorRate);
-        mClock = injector.getClock();
-        mHandler = handler;
-        mAmbientLightRingBuffer = new AmbientLightRingBuffer(bufferInitialCapacity, mClock);
-        mConfig = config;
-        mLightSensor = mInjector.getLightSensor(mConfig);
-        mTag = mInjector.getTag();
-    }
-
-    public void setListener(LightSensorListener listener) {
-        mListener = listener;
-    }
-
-    /**
-     * @return true if sensor registered, false if sensor already registered
-     */
-    public boolean enableLightSensorIfNeeded() {
-        if (!mLightSensorEnabled) {
-            mLightSensorEnabled = true;
-            mLightSensorEnableTime = mClock.uptimeMillis();
-            mCurrentLightSensorRate = mConfig.mInitialLightSensorRate;
-            mInjector.registerLightSensorListener(
-                    mLightSensorListener, mLightSensor, mCurrentLightSensorRate, mHandler);
-            return true;
-        }
-        return false;
-    }
-
-    /**
-     * @return true if sensor unregistered, false if sensor already unregistered
-     */
-    public boolean disableLightSensorIfNeeded() {
-        if (mLightSensorEnabled) {
-            mLightSensorEnabled = false;
-            mAmbientLuxValid = !mConfig.mResetAmbientLuxAfterWarmUpConfig;
-            if (!mAmbientLuxValid) {
-                mPreThresholdLux = PowerManager.BRIGHTNESS_INVALID_FLOAT;
-            }
-            mRecentLightSamples = 0;
-            mAmbientLightRingBuffer.clear();
-            mCurrentLightSensorRate = -1;
-            mInjector.unregisterLightSensorListener(mLightSensorListener);
-            return true;
-        }
-        return false;
-    }
-
-    public void setLoggingEnabled(boolean loggingEnabled) {
-        mLoggingEnabled = loggingEnabled;
-    }
-
-    /**
-     * Updates BrightnessEvent with LightSensorController details
-     */
-    public void updateBrightnessEvent(BrightnessEvent brightnessEvent) {
-        brightnessEvent.setPreThresholdLux(mPreThresholdLux);
-    }
-
-    /**
-     * Print the object's debug information into the given stream.
-     */
-    public void dump(PrintWriter pw) {
-        pw.println("LightSensorController state:");
-        pw.println("  mLightSensorEnabled=" + mLightSensorEnabled);
-        pw.println("  mLightSensorEnableTime=" + TimeUtils.formatUptime(mLightSensorEnableTime));
-        pw.println("  mCurrentLightSensorRate=" + mCurrentLightSensorRate);
-        pw.println("  mRecentLightSamples=" + mRecentLightSamples);
-        pw.println("  mAmbientLux=" + mAmbientLux);
-        pw.println("  mAmbientLuxValid=" + mAmbientLuxValid);
-        pw.println("  mPreThresholdLux=" + mPreThresholdLux);
-        pw.println("  mLastObservedLux=" + mLastObservedLux);
-        pw.println("  mLastObservedLuxTime=" + TimeUtils.formatUptime(mLastObservedLuxTime));
-        pw.println("  mSlowAmbientLux=" + mSlowAmbientLux);
-        pw.println("  mFastAmbientLux=" + mFastAmbientLux);
-        pw.println("  mIsIdleMode=" + mIsIdleMode);
-        pw.println("  mAmbientBrighteningThreshold=" + mAmbientBrighteningThreshold);
-        pw.println("  mAmbientDarkeningThreshold=" + mAmbientDarkeningThreshold);
-        pw.println("  mAmbientLightRingBuffer=" + mAmbientLightRingBuffer);
-        pw.println("  mLightSensor=" + mLightSensor);
-        mConfig.dump(pw);
-    }
-
-    /**
-     * This method should be called when this LightSensorController is no longer in use
-     * i.e. when corresponding display removed
-     */
-    public void stop() {
-        mHandler.removeCallbacksAndMessages(null);
-        disableLightSensorIfNeeded();
-    }
-
-    public void setIdleMode(boolean isIdleMode) {
-        mIsIdleMode = isIdleMode;
-    }
-
-    /**
-     * returns true if LightSensorController holds valid ambient lux value
-     */
-    public boolean hasValidAmbientLux() {
-        return mAmbientLuxValid;
-    }
-
-    /**
-     * returns all last observed sensor values
-     */
-    public float[] getLastSensorValues() {
-        return mAmbientLightRingBuffer.getAllLuxValues();
-    }
-
-    /**
-     * returns all last observed sensor event timestamps
-     */
-    public long[] getLastSensorTimestamps() {
-        return mAmbientLightRingBuffer.getAllTimestamps();
-    }
-
-    public float getLastObservedLux() {
-        return mLastObservedLux;
-    }
-
-    private void handleLightSensorEvent(long time, float lux) {
-        Trace.traceCounter(Trace.TRACE_TAG_POWER, "ALS", (int) lux);
-        mHandler.removeCallbacks(mAmbientLuxUpdater);
-
-        if (mAmbientLightRingBuffer.size() == 0) {
-            // switch to using the steady-state sample rate after grabbing the initial light sample
-            adjustLightSensorRate(mConfig.mNormalLightSensorRate);
-        }
-        applyLightSensorMeasurement(time, lux);
-        updateAmbientLux(time);
-    }
-
-    private void applyLightSensorMeasurement(long time, float lux) {
-        mRecentLightSamples++;
-        mAmbientLightRingBuffer.prune(time - mConfig.mAmbientLightHorizonLong);
-        mAmbientLightRingBuffer.push(time, lux);
-        // Remember this sample value.
-        mLastObservedLux = lux;
-        mLastObservedLuxTime = time;
-    }
-
-    private void adjustLightSensorRate(int lightSensorRate) {
-        // if the light sensor rate changed, update the sensor listener
-        if (lightSensorRate != mCurrentLightSensorRate) {
-            if (mLoggingEnabled) {
-                Slog.d(mTag, "adjustLightSensorRate: "
-                        + "previousRate=" + mCurrentLightSensorRate + ", "
-                        + "currentRate=" + lightSensorRate);
-            }
-            mCurrentLightSensorRate = lightSensorRate;
-            mInjector.unregisterLightSensorListener(mLightSensorListener);
-            mInjector.registerLightSensorListener(
-                    mLightSensorListener, mLightSensor, lightSensorRate, mHandler);
-        }
-    }
-
-    private void setAmbientLux(float lux) {
-        if (mLoggingEnabled) {
-            Slog.d(mTag, "setAmbientLux(" + lux + ")");
-        }
-        if (lux < 0) {
-            Slog.w(mTag, "Ambient lux was negative, ignoring and setting to 0");
-            lux = 0;
-        }
-        mAmbientLux = lux;
-
-        if (mIsIdleMode) {
-            mAmbientBrighteningThreshold =
-                    mConfig.mAmbientBrightnessThresholdsIdle.getBrighteningThreshold(lux);
-            mAmbientDarkeningThreshold =
-                    mConfig.mAmbientBrightnessThresholdsIdle.getDarkeningThreshold(lux);
-        } else {
-            mAmbientBrighteningThreshold =
-                    mConfig.mAmbientBrightnessThresholds.getBrighteningThreshold(lux);
-            mAmbientDarkeningThreshold =
-                    mConfig.mAmbientBrightnessThresholds.getDarkeningThreshold(lux);
-        }
-
-        mListener.onAmbientLuxChange(mAmbientLux);
-    }
-
-    private float calculateAmbientLux(long now, long horizon) {
-        if (mLoggingEnabled) {
-            Slog.d(mTag, "calculateAmbientLux(" + now + ", " + horizon + ")");
-        }
-        final int size = mAmbientLightRingBuffer.size();
-        if (size == 0) {
-            Slog.e(mTag, "calculateAmbientLux: No ambient light readings available");
-            return -1;
-        }
-
-        // Find the first measurement that is just outside of the horizon.
-        int endIndex = 0;
-        final long horizonStartTime = now - horizon;
-        for (int i = 0; i < size - 1; i++) {
-            if (mAmbientLightRingBuffer.getTime(i + 1) <= horizonStartTime) {
-                endIndex++;
-            } else {
-                break;
-            }
-        }
-        if (mLoggingEnabled) {
-            Slog.d(mTag, "calculateAmbientLux: selected endIndex=" + endIndex + ", point=("
-                    + mAmbientLightRingBuffer.getTime(endIndex) + ", "
-                    + mAmbientLightRingBuffer.getLux(endIndex) + ")");
-        }
-        float sum = 0;
-        float totalWeight = 0;
-        long endTime = AMBIENT_LIGHT_PREDICTION_TIME_MILLIS;
-        for (int i = size - 1; i >= endIndex; i--) {
-            long eventTime = mAmbientLightRingBuffer.getTime(i);
-            if (i == endIndex && eventTime < horizonStartTime) {
-                // If we're at the final value, make sure we only consider the part of the sample
-                // within our desired horizon.
-                eventTime = horizonStartTime;
-            }
-            final long startTime = eventTime - now;
-            float weight = calculateWeight(startTime, endTime);
-            float lux = mAmbientLightRingBuffer.getLux(i);
-            if (mLoggingEnabled) {
-                Slog.d(mTag, "calculateAmbientLux: [" + startTime + ", " + endTime + "]: "
-                        + "lux=" + lux + ", "
-                        + "weight=" + weight);
-            }
-            totalWeight += weight;
-            sum += lux * weight;
-            endTime = startTime;
-        }
-        if (mLoggingEnabled) {
-            Slog.d(mTag, "calculateAmbientLux: "
-                    + "totalWeight=" + totalWeight + ", "
-                    + "newAmbientLux=" + (sum / totalWeight));
-        }
-        return sum / totalWeight;
-    }
-
-    private float calculateWeight(long startDelta, long endDelta) {
-        return weightIntegral(endDelta) - weightIntegral(startDelta);
-    }
-
-    // Evaluates the integral of y = x + mWeightingIntercept. This is always positive for the
-    // horizon we're looking at and provides a non-linear weighting for light samples.
-    private float weightIntegral(long x) {
-        return x * (x * 0.5f + mConfig.mWeightingIntercept);
-    }
-
-    private long nextAmbientLightBrighteningTransition(long time) {
-        final int size = mAmbientLightRingBuffer.size();
-        long earliestValidTime = time;
-        for (int i = size - 1; i >= 0; i--) {
-            if (mAmbientLightRingBuffer.getLux(i) <= mAmbientBrighteningThreshold) {
-                break;
-            }
-            earliestValidTime = mAmbientLightRingBuffer.getTime(i);
-        }
-        return earliestValidTime + (mIsIdleMode ? mConfig.mBrighteningLightDebounceConfigIdle
-                : mConfig.mBrighteningLightDebounceConfig);
-    }
-
-    private long nextAmbientLightDarkeningTransition(long time) {
-        final int size = mAmbientLightRingBuffer.size();
-        long earliestValidTime = time;
-        for (int i = size - 1; i >= 0; i--) {
-            if (mAmbientLightRingBuffer.getLux(i) >= mAmbientDarkeningThreshold) {
-                break;
-            }
-            earliestValidTime = mAmbientLightRingBuffer.getTime(i);
-        }
-        return earliestValidTime + (mIsIdleMode ? mConfig.mDarkeningLightDebounceConfigIdle
-                : mConfig.mDarkeningLightDebounceConfig);
-    }
-
-    private void updateAmbientLux() {
-        long time = mClock.uptimeMillis();
-        mAmbientLightRingBuffer.prune(time - mConfig.mAmbientLightHorizonLong);
-        updateAmbientLux(time);
-    }
-
-    private void updateAmbientLux(long time) {
-        // If the light sensor was just turned on then immediately update our initial
-        // estimate of the current ambient light level.
-        if (!mAmbientLuxValid) {
-            final long timeWhenSensorWarmedUp =
-                    mConfig.mLightSensorWarmUpTimeConfig + mLightSensorEnableTime;
-            if (time < timeWhenSensorWarmedUp) {
-                if (mLoggingEnabled) {
-                    Slog.d(mTag, "updateAmbientLux: Sensor not ready yet: "
-                            + "time=" + time + ", "
-                            + "timeWhenSensorWarmedUp=" + timeWhenSensorWarmedUp);
-                }
-                mHandler.postAtTime(mAmbientLuxUpdater, timeWhenSensorWarmedUp);
-                return;
-            }
-            mAmbientLuxValid = true;
-            setAmbientLux(calculateAmbientLux(time, mConfig.mAmbientLightHorizonShort));
-            if (mLoggingEnabled) {
-                Slog.d(mTag, "updateAmbientLux: Initializing: "
-                        + "mAmbientLightRingBuffer=" + mAmbientLightRingBuffer + ", "
-                        + "mAmbientLux=" + mAmbientLux);
-            }
-        }
-
-        long nextBrightenTransition = nextAmbientLightBrighteningTransition(time);
-        long nextDarkenTransition = nextAmbientLightDarkeningTransition(time);
-        // Essentially, we calculate both a slow ambient lux, to ensure there's a true long-term
-        // change in lighting conditions, and a fast ambient lux to determine what the new
-        // brightness situation is since the slow lux can be quite slow to converge.
-        //
-        // Note that both values need to be checked for sufficient change before updating the
-        // proposed ambient light value since the slow value might be sufficiently far enough away
-        // from the fast value to cause a recalculation while its actually just converging on
-        // the fast value still.
-        mSlowAmbientLux = calculateAmbientLux(time, mConfig.mAmbientLightHorizonLong);
-        mFastAmbientLux = calculateAmbientLux(time, mConfig.mAmbientLightHorizonShort);
-
-        if ((mSlowAmbientLux >= mAmbientBrighteningThreshold
-                && mFastAmbientLux >= mAmbientBrighteningThreshold
-                && nextBrightenTransition <= time)
-                || (mSlowAmbientLux <= mAmbientDarkeningThreshold
-                && mFastAmbientLux <= mAmbientDarkeningThreshold
-                && nextDarkenTransition <= time)) {
-            mPreThresholdLux = mAmbientLux;
-            setAmbientLux(mFastAmbientLux);
-            if (mLoggingEnabled) {
-                Slog.d(mTag, "updateAmbientLux: "
-                        + ((mFastAmbientLux > mAmbientLux) ? "Brightened" : "Darkened") + ": "
-                        + "mAmbientBrighteningThreshold=" + mAmbientBrighteningThreshold + ", "
-                        + "mAmbientDarkeningThreshold=" + mAmbientDarkeningThreshold + ", "
-                        + "mAmbientLightRingBuffer=" + mAmbientLightRingBuffer + ", "
-                        + "mAmbientLux=" + mAmbientLux);
-            }
-            nextBrightenTransition = nextAmbientLightBrighteningTransition(time);
-            nextDarkenTransition = nextAmbientLightDarkeningTransition(time);
-        }
-        long nextTransitionTime = Math.min(nextDarkenTransition, nextBrightenTransition);
-        // If one of the transitions is ready to occur, but the total weighted ambient lux doesn't
-        // exceed the necessary threshold, then it's possible we'll get a transition time prior to
-        // now. Rather than continually checking to see whether the weighted lux exceeds the
-        // threshold, schedule an update for when we'd normally expect another light sample, which
-        // should be enough time to decide whether we should actually transition to the new
-        // weighted ambient lux or not.
-        nextTransitionTime = nextTransitionTime > time ? nextTransitionTime
-                : time + mConfig.mNormalLightSensorRate;
-        if (mLoggingEnabled) {
-            Slog.d(mTag, "updateAmbientLux: Scheduling ambient lux update for "
-                    + nextTransitionTime + TimeUtils.formatUptime(nextTransitionTime));
-        }
-        mHandler.postAtTime(mAmbientLuxUpdater, nextTransitionTime);
-    }
-
-    public interface LightSensorListener {
-        /**
-         * Called when new ambient lux value is ready
-         */
-        void onAmbientLuxChange(float ambientLux);
-    }
-
-    private static final class LightSensorHandler extends Handler {
-        private LightSensorHandler(Looper looper) {
-            super(looper, /* callback= */ null, /* async= */ true);
-        }
-    }
-
-    /**
-     * A ring buffer of ambient light measurements sorted by time.
-     * Each entry consists of a timestamp and a lux measurement, and the overall buffer is sorted
-     * from oldest to newest.
-     */
-    @VisibleForTesting
-    static final class AmbientLightRingBuffer {
-
-        private float[] mRingLux;
-        private long[] mRingTime;
-        private int mCapacity;
-
-        // The first valid element and the next open slot.
-        // Note that if mCount is zero then there are no valid elements.
-        private int mStart;
-        private int mEnd;
-        private int mCount;
-
-        private final Clock mClock;
-
-        @VisibleForTesting
-        AmbientLightRingBuffer(int initialCapacity, Clock clock) {
-            mCapacity = initialCapacity;
-            mRingLux = new float[mCapacity];
-            mRingTime = new long[mCapacity];
-            mClock = clock;
-
-        }
-
-        @VisibleForTesting
-        float getLux(int index) {
-            return mRingLux[offsetOf(index)];
-        }
-
-        @VisibleForTesting
-        float[] getAllLuxValues() {
-            float[] values = new float[mCount];
-            if (mCount == 0) {
-                return values;
-            }
-
-            if (mStart < mEnd) {
-                System.arraycopy(mRingLux, mStart, values, 0, mCount);
-            } else {
-                System.arraycopy(mRingLux, mStart, values, 0, mCapacity - mStart);
-                System.arraycopy(mRingLux, 0, values, mCapacity - mStart, mEnd);
-            }
-
-            return values;
-        }
-
-        @VisibleForTesting
-        long getTime(int index) {
-            return mRingTime[offsetOf(index)];
-        }
-
-        @VisibleForTesting
-        long[] getAllTimestamps() {
-            long[] values = new long[mCount];
-            if (mCount == 0) {
-                return values;
-            }
-
-            if (mStart < mEnd) {
-                System.arraycopy(mRingTime, mStart, values, 0, mCount);
-            } else {
-                System.arraycopy(mRingTime, mStart, values, 0, mCapacity - mStart);
-                System.arraycopy(mRingTime, 0, values, mCapacity - mStart, mEnd);
-            }
-
-            return values;
-        }
-
-        @VisibleForTesting
-        void push(long time, float lux) {
-            int next = mEnd;
-            if (mCount == mCapacity) {
-                int newSize = mCapacity * 2;
-
-                float[] newRingLux = new float[newSize];
-                long[] newRingTime = new long[newSize];
-                int length = mCapacity - mStart;
-                System.arraycopy(mRingLux, mStart, newRingLux, 0, length);
-                System.arraycopy(mRingTime, mStart, newRingTime, 0, length);
-                if (mStart != 0) {
-                    System.arraycopy(mRingLux, 0, newRingLux, length, mStart);
-                    System.arraycopy(mRingTime, 0, newRingTime, length, mStart);
-                }
-                mRingLux = newRingLux;
-                mRingTime = newRingTime;
-
-                next = mCapacity;
-                mCapacity = newSize;
-                mStart = 0;
-            }
-            mRingTime[next] = time;
-            mRingLux[next] = lux;
-            mEnd = next + 1;
-            if (mEnd == mCapacity) {
-                mEnd = 0;
-            }
-            mCount++;
-        }
-
-        @VisibleForTesting
-        void prune(long horizon) {
-            if (mCount == 0) {
-                return;
-            }
-
-            while (mCount > 1) {
-                int next = mStart + 1;
-                if (next >= mCapacity) {
-                    next -= mCapacity;
-                }
-                if (mRingTime[next] > horizon) {
-                    // Some light sensors only produce data upon a change in the ambient light
-                    // levels, so we need to consider the previous measurement as the ambient light
-                    // level for all points in time up until we receive a new measurement. Thus, we
-                    // always want to keep the youngest element that would be removed from the
-                    // buffer and just set its measurement time to the horizon time since at that
-                    // point it is the ambient light level, and to remove it would be to drop a
-                    // valid data point within our horizon.
-                    break;
-                }
-                mStart = next;
-                mCount -= 1;
-            }
-
-            if (mRingTime[mStart] < horizon) {
-                mRingTime[mStart] = horizon;
-            }
-        }
-
-        @VisibleForTesting
-        int size() {
-            return mCount;
-        }
-
-        @VisibleForTesting
-        void clear() {
-            mStart = 0;
-            mEnd = 0;
-            mCount = 0;
-        }
-
-        @Override
-        public String toString() {
-            StringBuilder buf = new StringBuilder();
-            buf.append('[');
-            for (int i = 0; i < mCount; i++) {
-                final long next = i + 1 < mCount ? getTime(i + 1) : mClock.uptimeMillis();
-                if (i != 0) {
-                    buf.append(", ");
-                }
-                buf.append(getLux(i));
-                buf.append(" / ");
-                buf.append(next - getTime(i));
-                buf.append("ms");
-            }
-            buf.append(']');
-            return buf.toString();
-        }
-
-        private int offsetOf(int index) {
-            if (index >= mCount || index < 0) {
-                throw new ArrayIndexOutOfBoundsException(index);
-            }
-            index += mStart;
-            if (index >= mCapacity) {
-                index -= mCapacity;
-            }
-            return index;
-        }
-    }
-
-    @VisibleForTesting
-    interface Injector {
-        Clock getClock();
-
-        Sensor getLightSensor(LightSensorControllerConfig config);
-
-        boolean registerLightSensorListener(
-                SensorEventListener listener, Sensor sensor, int rate, Handler handler);
-
-        void unregisterLightSensorListener(SensorEventListener listener);
-
-        String getTag();
-
-    }
-
-    private static class RealInjector implements Injector {
-        private final SensorManager mSensorManager;
-        private final int mSensorFallbackType;
-
-        private final String mTag;
-
-        private RealInjector(SensorManager sensorManager, int displayId) {
-            mSensorManager = sensorManager;
-            mSensorFallbackType = displayId == Display.DEFAULT_DISPLAY
-                    ? Sensor.TYPE_LIGHT : SensorUtils.NO_FALLBACK;
-            mTag = "LightSensorController [" + displayId + "]";
-        }
-
-        @Override
-        public Clock getClock() {
-            return Clock.SYSTEM_CLOCK;
-        }
-
-        @Override
-        public Sensor getLightSensor(LightSensorControllerConfig config) {
-            return SensorUtils.findSensor(
-                    mSensorManager, config.mAmbientLightSensor, mSensorFallbackType);
-        }
-
-        @Override
-        public boolean registerLightSensorListener(
-                SensorEventListener listener, Sensor sensor, int rate, Handler handler) {
-            return mSensorManager.registerListener(listener, sensor, rate * 1000, handler);
-        }
-
-        @Override
-        public void unregisterLightSensorListener(SensorEventListener listener) {
-            mSensorManager.unregisterListener(listener);
-        }
-
-        @Override
-        public String getTag() {
-            return mTag;
-        }
-    }
-
-    public static class LightSensorControllerConfig {
-        // Steady-state light sensor event rate in milliseconds.
-        private final int mNormalLightSensorRate;
-        private final int mInitialLightSensorRate;
-
-        // If true immediately after the screen is turned on the controller will try to adjust the
-        // brightness based on the current sensor reads. If false, the controller will collect
-        // more data
-        // and only then decide whether to change brightness.
-        private final boolean mResetAmbientLuxAfterWarmUpConfig;
-
-        // Period of time in which to consider light samples for a short/long-term estimate of
-        // ambient
-        // light in milliseconds.
-        private final int mAmbientLightHorizonShort;
-        private final int mAmbientLightHorizonLong;
-
-
-        // Amount of time to delay auto-brightness after screen on while waiting for
-        // the light sensor to warm-up in milliseconds.
-        // May be 0 if no warm-up is required.
-        private final int mLightSensorWarmUpTimeConfig;
-
-
-        // The intercept used for the weighting calculation. This is used in order to keep all
-        // possible
-        // weighting values positive.
-        private final int mWeightingIntercept;
-
-        // Configuration object for determining thresholds to change brightness dynamically
-        private final HysteresisLevels mAmbientBrightnessThresholds;
-        private final HysteresisLevels mAmbientBrightnessThresholdsIdle;
-
-
-        // Stability requirements in milliseconds for accepting a new brightness level.  This is
-        // used
-        // for debouncing the light sensor.  Different constants are used to debounce the light
-        // sensor
-        // when adapting to brighter or darker environments.  This parameter controls how quickly
-        // brightness changes occur in response to an observed change in light level that exceeds
-        // the
-        // hysteresis threshold.
-        private final long mBrighteningLightDebounceConfig;
-        private final long mDarkeningLightDebounceConfig;
-        private final long mBrighteningLightDebounceConfigIdle;
-        private final long mDarkeningLightDebounceConfigIdle;
-
-        private final SensorData mAmbientLightSensor;
-
-        @VisibleForTesting
-        LightSensorControllerConfig(int initialLightSensorRate, int normalLightSensorRate,
-                boolean resetAmbientLuxAfterWarmUpConfig, int ambientLightHorizonShort,
-                int ambientLightHorizonLong, int lightSensorWarmUpTimeConfig,
-                int weightingIntercept, HysteresisLevels ambientBrightnessThresholds,
-                HysteresisLevels ambientBrightnessThresholdsIdle,
-                long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
-                long brighteningLightDebounceConfigIdle, long darkeningLightDebounceConfigIdle,
-                SensorData ambientLightSensor) {
-            mInitialLightSensorRate = initialLightSensorRate;
-            mNormalLightSensorRate = normalLightSensorRate;
-            mResetAmbientLuxAfterWarmUpConfig = resetAmbientLuxAfterWarmUpConfig;
-            mAmbientLightHorizonShort = ambientLightHorizonShort;
-            mAmbientLightHorizonLong = ambientLightHorizonLong;
-            mLightSensorWarmUpTimeConfig = lightSensorWarmUpTimeConfig;
-            mWeightingIntercept = weightingIntercept;
-            mAmbientBrightnessThresholds = ambientBrightnessThresholds;
-            mAmbientBrightnessThresholdsIdle = ambientBrightnessThresholdsIdle;
-            mBrighteningLightDebounceConfig = brighteningLightDebounceConfig;
-            mDarkeningLightDebounceConfig = darkeningLightDebounceConfig;
-            mBrighteningLightDebounceConfigIdle = brighteningLightDebounceConfigIdle;
-            mDarkeningLightDebounceConfigIdle = darkeningLightDebounceConfigIdle;
-            mAmbientLightSensor = ambientLightSensor;
-        }
-
-        private void dump(PrintWriter pw) {
-            pw.println("LightSensorControllerConfig:");
-            pw.println("  mInitialLightSensorRate=" + mInitialLightSensorRate);
-            pw.println("  mNormalLightSensorRate=" + mNormalLightSensorRate);
-            pw.println("  mResetAmbientLuxAfterWarmUpConfig=" + mResetAmbientLuxAfterWarmUpConfig);
-            pw.println("  mAmbientLightHorizonShort=" + mAmbientLightHorizonShort);
-            pw.println("  mAmbientLightHorizonLong=" + mAmbientLightHorizonLong);
-            pw.println("  mLightSensorWarmUpTimeConfig=" + mLightSensorWarmUpTimeConfig);
-            pw.println("  mWeightingIntercept=" + mWeightingIntercept);
-            pw.println("  mAmbientBrightnessThresholds=");
-            mAmbientBrightnessThresholds.dump(pw);
-            pw.println("  mAmbientBrightnessThresholdsIdle=");
-            mAmbientBrightnessThresholdsIdle.dump(pw);
-            pw.println("  mBrighteningLightDebounceConfig=" + mBrighteningLightDebounceConfig);
-            pw.println("  mDarkeningLightDebounceConfig=" + mDarkeningLightDebounceConfig);
-            pw.println(
-                    "  mBrighteningLightDebounceConfigIdle=" + mBrighteningLightDebounceConfigIdle);
-            pw.println("  mDarkeningLightDebounceConfigIdle=" + mDarkeningLightDebounceConfigIdle);
-            pw.println("  mAmbientLightSensor=" + mAmbientLightSensor);
-        }
-
-        /**
-         * Creates LightSensorControllerConfig object form Resources and DisplayDeviceConfig
-         */
-        public static LightSensorControllerConfig create(Resources res, DisplayDeviceConfig ddc) {
-            int lightSensorRate = res.getInteger(R.integer.config_autoBrightnessLightSensorRate);
-            int initialLightSensorRate = res.getInteger(
-                    R.integer.config_autoBrightnessInitialLightSensorRate);
-            if (initialLightSensorRate == -1) {
-                initialLightSensorRate = lightSensorRate;
-            } else if (initialLightSensorRate > lightSensorRate) {
-                Slog.w("LightSensorControllerConfig",
-                        "Expected config_autoBrightnessInitialLightSensorRate ("
-                                + initialLightSensorRate + ") to be less than or equal to "
-                                + "config_autoBrightnessLightSensorRate (" + lightSensorRate
-                                + ").");
-            }
-
-            boolean resetAmbientLuxAfterWarmUp = res.getBoolean(
-                    R.bool.config_autoBrightnessResetAmbientLuxAfterWarmUp);
-            int lightSensorWarmUpTimeConfig = res.getInteger(
-                    R.integer.config_lightSensorWarmupTime);
-
-            return new LightSensorControllerConfig(initialLightSensorRate, lightSensorRate,
-                    resetAmbientLuxAfterWarmUp, ddc.getAmbientHorizonShort(),
-                    ddc.getAmbientHorizonLong(), lightSensorWarmUpTimeConfig,
-                    ddc.getAmbientHorizonLong(),
-                    HysteresisLevels.getAmbientBrightnessThresholds(ddc),
-                    HysteresisLevels.getAmbientBrightnessThresholdsIdle(ddc),
-                    ddc.getAutoBrightnessBrighteningLightDebounce(),
-                    ddc.getAutoBrightnessDarkeningLightDebounce(),
-                    ddc.getAutoBrightnessBrighteningLightDebounceIdle(),
-                    ddc.getAutoBrightnessDarkeningLightDebounceIdle(),
-                    ddc.getAmbientLightSensor()
-            );
-        }
-    }
-}
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
index 9c7504d..a46975fb 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
@@ -285,7 +285,7 @@
             List<BrightnessStateModifier> modifiers = new ArrayList<>();
             modifiers.add(new DisplayDimModifier(context));
             modifiers.add(new BrightnessLowPowerModeModifier());
-            if (flags.isEvenDimmerEnabled()) {
+            if (flags.isEvenDimmerEnabled() && displayDeviceConfig != null) {
                 modifiers.add(new BrightnessLowLuxModifier(handler, listener, context,
                         displayDeviceConfig));
             }
diff --git a/services/core/java/com/android/server/inputmethod/ImeTrackerService.java b/services/core/java/com/android/server/inputmethod/ImeTrackerService.java
index 85ab773..1c14fc1 100644
--- a/services/core/java/com/android/server/inputmethod/ImeTrackerService.java
+++ b/services/core/java/com/android/server/inputmethod/ImeTrackerService.java
@@ -165,6 +165,15 @@
         }
     }
 
+    @EnforcePermission(Manifest.permission.TEST_INPUT_METHOD)
+    @Override
+    public void finishTrackingPendingImeVisibilityRequests() {
+        super.finishTrackingPendingImeVisibilityRequests_enforcePermission();
+        synchronized (mLock) {
+            mHistory.mLiveEntries.clear();
+        }
+    }
+
     /**
      * A circular buffer storing the most recent few {@link ImeTracker.Token} entries information.
      */
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 095a233..4da280b 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -2446,7 +2446,7 @@
             ComponentName intentFilterVerifierComponent =
                     getIntentFilterVerifierComponentNameLPr(computer);
             ComponentName domainVerificationAgent =
-                    getDomainVerificationAgentComponentNameLPr(computer);
+                    getDomainVerificationAgentComponentNameLPr(computer, UserHandle.USER_SYSTEM);
 
             DomainVerificationProxy domainVerificationProxy = DomainVerificationProxy.makeProxy(
                     intentFilterVerifierComponent, domainVerificationAgent, mContext,
@@ -2754,12 +2754,13 @@
     }
 
     @Nullable
-    private ComponentName getDomainVerificationAgentComponentNameLPr(@NonNull Computer computer) {
+    private ComponentName getDomainVerificationAgentComponentNameLPr(@NonNull Computer computer,
+            int userId) {
         Intent intent = new Intent(Intent.ACTION_DOMAINS_NEED_VERIFICATION);
         List<ResolveInfo> matches =
                 mResolveIntentHelper.queryIntentReceiversInternal(computer, intent, null,
                         MATCH_SYSTEM_ONLY | MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE,
-                        UserHandle.USER_SYSTEM, Binder.getCallingUid());
+                        userId, Binder.getCallingUid());
         ResolveInfo best = null;
         final int N = matches.size();
         for (int i = 0; i < N; i++) {
@@ -2767,7 +2768,7 @@
             final String packageName = cur.getComponentInfo().packageName;
             if (checkPermission(
                     android.Manifest.permission.DOMAIN_VERIFICATION_AGENT, packageName,
-                    UserHandle.USER_SYSTEM) != PackageManager.PERMISSION_GRANTED) {
+                    userId) != PackageManager.PERMISSION_GRANTED) {
                 Slog.w(TAG, "Domain verification agent found but does not hold permission: "
                         + packageName);
                 continue;
@@ -2775,7 +2776,7 @@
 
             if (best == null || cur.priority > best.priority) {
                 if (computer.isComponentEffectivelyEnabled(cur.getComponentInfo(),
-                        UserHandle.SYSTEM)) {
+                        UserHandle.of(userId))) {
                     best = cur;
                 } else {
                     Slog.w(TAG, "Domain verification agent found but not enabled");
@@ -6272,9 +6273,13 @@
                     final int[] userIds = resolveUserIds(UserHandle.USER_ALL);
                     final String reason = "The mimeGroup is changed";
                     for (int i = 0; i < userIds.length; i++) {
-                        final int packageUid = UserHandle.getUid(userIds[i], appId);
-                        mBroadcastHelper.sendPackageChangedBroadcast(snapShot, packageName,
-                                true /* dontKillApp */, components, packageUid, reason);
+                        final PackageUserStateInternal pkgUserState =
+                                packageState.getUserStates().get(userIds[i]);
+                        if (pkgUserState != null && pkgUserState.isInstalled()) {
+                            final int packageUid = UserHandle.getUid(userIds[i], appId);
+                            mBroadcastHelper.sendPackageChangedBroadcast(snapShot, packageName,
+                                    true /* dontKillApp */, components, packageUid, reason);
+                        }
                     }
                 });
             }
@@ -6508,13 +6513,13 @@
 
         @Override
         @Nullable
-        public ComponentName getDomainVerificationAgent() {
+        public ComponentName getDomainVerificationAgent(int userId) {
             final int callerUid = Binder.getCallingUid();
             if (!PackageManagerServiceUtils.isRootOrShell(callerUid)) {
                 throw new SecurityException("Not allowed to query domain verification agent");
             }
             final Computer snapshot = snapshotComputer();
-            return getDomainVerificationAgentComponentNameLPr(snapshot);
+            return getDomainVerificationAgentComponentNameLPr(snapshot, userId);
         }
 
         @Override
diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
index a9e1725..59faf24 100644
--- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
+++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java
@@ -4412,8 +4412,31 @@
 
     private int runGetDomainVerificationAgent() throws RemoteException {
         final PrintWriter pw = getOutPrintWriter();
+        int userId = UserHandle.USER_ALL;
+
+        String opt;
+        while ((opt = getNextOption()) != null) {
+            if (opt.equals("--user")) {
+                userId = UserHandle.parseUserArg(getNextArgRequired());
+                if (userId != UserHandle.USER_ALL && userId != UserHandle.USER_CURRENT) {
+                    UserManagerInternal umi =
+                            LocalServices.getService(UserManagerInternal.class);
+                    UserInfo userInfo = umi.getUserInfo(userId);
+                    if (userInfo == null) {
+                        pw.println("Failure [user " + userId + " doesn't exist]");
+                        return 1;
+                    }
+                }
+            } else {
+                pw.println("Error: Unknown option: " + opt);
+                return 1;
+            }
+        }
+        final int translatedUserId =
+                translateUserId(userId, UserHandle.USER_SYSTEM, "runGetDomainVerificationAgent");
         try {
-            final ComponentName domainVerificationAgent = mInterface.getDomainVerificationAgent();
+            final ComponentName domainVerificationAgent =
+                    mInterface.getDomainVerificationAgent(translatedUserId);
             pw.println(domainVerificationAgent == null
                     ? "No Domain Verifier available!" : domainVerificationAgent.flattenToString());
         } catch (Exception e) {
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 8e3c6ac..3a84897 100644
--- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
+++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java
@@ -3844,6 +3844,7 @@
         public abstract T instantiateObject();
     }
 
+    @SuppressWarnings("ParcelableCreator")
     public static class ControllerActivityCounterImpl extends ControllerActivityCounter
             implements Parcelable {
         private final Clock mClock;
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 17e6996..baf274d 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -819,6 +819,12 @@
     @Nullable
     private Rect mLetterboxBoundsForFixedOrientationAndAspectRatio;
 
+    // Bounds populated in resolveAspectRatioRestriction when this activity is letterboxed for
+    // aspect ratio. If not null, they are used as parent container in
+    // resolveSizeCompatModeConfiguration and in a constructor of CompatDisplayInsets.
+    @Nullable
+    private Rect mLetterboxBoundsForAspectRatio;
+
     // Whether the activity is eligible to be letterboxed for fixed orientation with respect to its
     // requested orientation, even when it's letterbox for another reason (e.g., size compat mode)
     // and therefore #isLetterboxedForFixedOrientationAndAspectRatio returns false.
@@ -6501,9 +6507,7 @@
         }
         newIntents = null;
 
-        if (isActivityTypeHome()) {
-            mTaskSupervisor.updateHomeProcess(task.getBottomMostActivity().app);
-        }
+        mTaskSupervisor.updateHomeProcessIfNeeded(this);
 
         if (nowVisible) {
             mTaskSupervisor.stopWaitingForActivityVisible(this);
@@ -8423,10 +8427,14 @@
                     fullConfig.windowConfiguration.getRotation());
         }
 
+        final Rect letterboxedContainerBounds =
+                mLetterboxBoundsForFixedOrientationAndAspectRatio != null
+                ? mLetterboxBoundsForFixedOrientationAndAspectRatio
+                : mLetterboxBoundsForAspectRatio;
+
         // The role of CompatDisplayInsets is like the override bounds.
         mCompatDisplayInsets =
-                new CompatDisplayInsets(
-                        mDisplayContent, this, mLetterboxBoundsForFixedOrientationAndAspectRatio);
+                new CompatDisplayInsets(mDisplayContent, this, letterboxedContainerBounds);
     }
 
     private void clearSizeCompatModeAttributes() {
@@ -8498,6 +8506,7 @@
         mIsAspectRatioApplied = false;
         mIsEligibleForFixedOrientationLetterbox = false;
         mLetterboxBoundsForFixedOrientationAndAspectRatio = null;
+        mLetterboxBoundsForAspectRatio = null;
 
         // Can't use resolvedConfig.windowConfiguration.getWindowingMode() because it can be
         // different from windowing mode of the task (PiP) during transition from fullscreen to PiP
@@ -8507,8 +8516,9 @@
 
         applySizeOverrideIfNeeded(newParentConfiguration, parentWindowingMode, resolvedConfig);
 
-        final boolean isFixedOrientationLetterboxAllowed =
-                parentWindowingMode == WINDOWING_MODE_MULTI_WINDOW
+        // Bubble activities should always fill their parent and should not be letterboxed.
+        final boolean isFixedOrientationLetterboxAllowed = !getLaunchedFromBubble()
+                && (parentWindowingMode == WINDOWING_MODE_MULTI_WINDOW
                         || parentWindowingMode == WINDOWING_MODE_FULLSCREEN
                         // When starting to switch between PiP and fullscreen, the task is pinned
                         // and the activity is fullscreen. But only allow to apply letterbox if the
@@ -8516,7 +8526,7 @@
                         || (!mWaitForEnteringPinnedMode
                                 && parentWindowingMode == WINDOWING_MODE_PINNED
                                 && resolvedConfig.windowConfiguration.getWindowingMode()
-                                        == WINDOWING_MODE_FULLSCREEN);
+                                        == WINDOWING_MODE_FULLSCREEN));
         // TODO(b/181207944): Consider removing the if condition and always run
         // resolveFixedOrientationConfiguration() since this should be applied for all cases.
         if (isFixedOrientationLetterboxAllowed) {
@@ -8535,9 +8545,11 @@
                 getTaskFragment().computeConfigResourceOverrides(resolvedConfig,
                         newParentConfiguration);
             }
+        }
         // If activity in fullscreen mode is letterboxed because of fixed orientation then bounds
-        // are already calculated in resolveFixedOrientationConfiguration.
-        } else if (!isLetterboxedForFixedOrientationAndAspectRatio()) {
+        // are already calculated in resolveFixedOrientationConfiguration, or if in size compat
+        // mode, it should already be calculated in resolveSizeCompatModeConfiguration
+        if (!isLetterboxedForFixedOrientationAndAspectRatio() && !mInSizeCompatModeForBounds) {
             resolveAspectRatioRestriction(newParentConfiguration);
         }
 
@@ -9031,7 +9043,8 @@
         }
         final CompatDisplayInsets compatDisplayInsets = getCompatDisplayInsets();
 
-        if (compatDisplayInsets != null && !compatDisplayInsets.mIsInFixedOrientationLetterbox) {
+        if (compatDisplayInsets != null
+                && !compatDisplayInsets.mIsInFixedOrientationOrAspectRatioLetterbox) {
             // App prefers to keep its original size.
             // If the size compat is from previous fixed orientation letterboxing, we may want to
             // have fixed orientation letterbox again, otherwise it will show the size compat
@@ -9163,6 +9176,7 @@
             // restrict, the bounds should be the requested override bounds.
             getTaskFragment().computeConfigResourceOverrides(resolvedConfig, newParentConfiguration,
                     getFixedRotationTransformDisplayInfo());
+            mLetterboxBoundsForAspectRatio = new Rect(resolvedBounds);
         }
     }
 
@@ -10727,10 +10741,10 @@
         /** Whether the {@link Task} windowingMode represents a floating window*/
         final boolean mIsFloating;
         /**
-         * Whether is letterboxed because of fixed orientation when the unresizable activity is
-         * first shown.
+         * Whether is letterboxed because of fixed orientation or aspect ratio when the
+         * unresizable activity is first shown.
          */
-        final boolean mIsInFixedOrientationLetterbox;
+        final boolean mIsInFixedOrientationOrAspectRatioLetterbox;
         /**
          * The nonDecorInsets for each rotation. Includes the navigation bar and cutout insets. It
          * is used to compute the appBounds.
@@ -10745,7 +10759,7 @@
 
         /** Constructs the environment to simulate the bounds behavior of the given container. */
         CompatDisplayInsets(DisplayContent display, ActivityRecord container,
-                @Nullable Rect fixedOrientationBounds) {
+                @Nullable Rect letterboxedContainerBounds) {
             mOriginalRotation = display.getRotation();
             mIsFloating = container.getWindowConfiguration().tasksAreFloating();
             mOriginalRequestedOrientation = container.getRequestedConfigurationOrientation();
@@ -10760,22 +10774,21 @@
                     mNonDecorInsets[rotation] = emptyRect;
                     mStableInsets[rotation] = emptyRect;
                 }
-                mIsInFixedOrientationLetterbox = false;
+                mIsInFixedOrientationOrAspectRatioLetterbox = false;
                 return;
             }
 
             final Task task = container.getTask();
 
-            mIsInFixedOrientationLetterbox = fixedOrientationBounds != null;
-
+            mIsInFixedOrientationOrAspectRatioLetterbox = letterboxedContainerBounds != null;
             // Store the bounds of the Task for the non-resizable activity to use in size compat
             // mode so that the activity will not be resized regardless the windowing mode it is
             // currently in.
-            // When an activity needs to be letterboxed because of fixed orientation, use fixed
-            // orientation bounds instead of task bounds since the activity will be displayed
-            // within these even if it is in size compat mode.
-            final Rect filledContainerBounds = mIsInFixedOrientationLetterbox
-                    ? fixedOrientationBounds
+            // When an activity needs to be letterboxed because of fixed orientation or aspect
+            // ratio, use resolved bounds instead of task bounds since the activity will be
+            // displayed within these even if it is in size compat mode.
+            final Rect filledContainerBounds = mIsInFixedOrientationOrAspectRatioLetterbox
+                    ? letterboxedContainerBounds
                     : task != null ? task.getBounds() : display.getBounds();
             final int filledContainerRotation = task != null
                     ? task.getConfiguration().windowConfiguration.getRotation()
diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
index 07f52574..430232c 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
@@ -902,10 +902,7 @@
                                 + " andResume=" + andResume);
                 EventLogTags.writeWmRestartActivity(r.mUserId, System.identityHashCode(r),
                         task.mTaskId, r.shortComponentName);
-                if (r.isActivityTypeHome()) {
-                    // Home process is the root process of the task.
-                    updateHomeProcess(task.getBottomMostActivity().app);
-                }
+                updateHomeProcessIfNeeded(r);
                 mService.getPackageManagerInternalLocked().notifyPackageUse(
                         r.intent.getComponent().getPackageName(), NOTIFY_PACKAGE_USE_ACTIVITY);
                 mService.getAppWarningsLocked().onStartActivity(r);
@@ -1050,6 +1047,16 @@
         return true;
     }
 
+    void updateHomeProcessIfNeeded(@NonNull ActivityRecord r) {
+        if (!r.isActivityTypeHome()) return;
+        // Make sure that we use the bottom most activity from the same package, because the home
+        // task can also embed third-party -1 activities.
+        final ActivityRecord bottom = r.getTask().getBottomMostActivityInSamePackage();
+        if (bottom != null) {
+            updateHomeProcess(bottom.app);
+        }
+    }
+
     void updateHomeProcess(WindowProcessController app) {
         if (app != null && mService.mHomeProcess != app) {
             scheduleStartHome("homeChanged");
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index 9336338..47f4a66 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -783,7 +783,7 @@
         if (balShowToastsBlocked()
                 && (state.mResultForCaller.allows() || state.mResultForRealCaller.allows())) {
             // only show a toast if either caller or real caller could launch if they opted in
-            showToast("BAL blocked. go/debug-bal");
+            showToast("BAL blocked. goo.gle/android-bal");
         }
         return statsLog(BalVerdict.BLOCK, state);
     }
diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
index 21326be..8c4f9ef 100644
--- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
@@ -38,7 +38,6 @@
 import android.view.InsetsSourceControl;
 import android.view.WindowInsets;
 import android.view.inputmethod.ImeTracker;
-import android.window.TaskSnapshot;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.protolog.common.ProtoLog;
@@ -78,17 +77,22 @@
         final InsetsSourceControl control = super.getControl(target);
         if (control != null && target != null && target.getWindow() != null) {
             final WindowState targetWin = target.getWindow();
+            final Task task = targetWin.getTask();
             // If the control target changes during the app transition with the task snapshot
             // starting window and the IME snapshot is visible, in case not have duplicated IME
             // showing animation during transitioning, use a flag to inform IME source control to
             // skip showing animation once.
-            final TaskSnapshot snapshot = targetWin.getRootTask() != null
-                    ? targetWin.mWmService.getTaskSnapshot(targetWin.getRootTask().mTaskId,
-                        0 /* userId */, false /* isLowResolution */, false /* restoreFromDisk */)
-                    : null;
-            control.setSkipAnimationOnce(targetWin.mActivityRecord != null
-                    && targetWin.mActivityRecord.hasStartingWindow()
-                    && snapshot != null && snapshot.hasImeSurface());
+            StartingData startingData = null;
+            if (task != null) {
+                startingData = targetWin.mActivityRecord.mStartingData;
+                if (startingData == null) {
+                    final WindowState startingWin = task.topStartingWindow();
+                    if (startingWin != null) {
+                        startingData = startingWin.mStartingData;
+                    }
+                }
+            }
+            control.setSkipAnimationOnce(startingData != null && startingData.hasImeSurface());
         }
         return control;
     }
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index b89c12b..6f1c834 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -6799,6 +6799,15 @@
         }
     }
 
+    @Nullable
+    ActivityRecord getBottomMostActivityInSamePackage() {
+        if (realActivity == null) {
+            return null;
+        }
+        return getActivity(ar -> ar.packageName.equals(
+                realActivity.getPackageName()), false /* traverseTopToBottom */);
+    }
+
     /**
      * Associates the decor surface with the given TF, or create one if there
      * isn't one in the Task yet. The surface will be removed with the TF,
diff --git a/services/core/jni/com_android_server_accessibility_BrailleDisplayConnection.cpp b/services/core/jni/com_android_server_accessibility_BrailleDisplayConnection.cpp
index c337523..180081c 100644
--- a/services/core/jni/com_android_server_accessibility_BrailleDisplayConnection.cpp
+++ b/services/core/jni/com_android_server_accessibility_BrailleDisplayConnection.cpp
@@ -32,10 +32,10 @@
 
 namespace {
 
-// Max size we allow for the result from HIDIOCGRAWUNIQ (Bluetooth address or USB serial number).
-// Copied from linux/hid.h struct hid_device->uniq char array size; the ioctl implementation
-// writes at most this many bytes to the provided buffer.
-constexpr int UNIQ_SIZE_MAX = 64;
+// Max sizes we allow for results from string ioctl calls, copied from UAPI linux/uhid.h.
+// The ioctl implementation writes at most this many bytes to the provided buffer:
+constexpr int NAME_SIZE_MAX = 128; // HIDIOCGRAWNAME (device name)
+constexpr int UNIQ_SIZE_MAX = 64;  // HIDIOCGRAWUNIQ (BT address or USB serial number)
 
 } // anonymous namespace
 
@@ -82,6 +82,16 @@
     return info.bustype;
 }
 
+static jstring com_android_server_accessibility_BrailleDisplayConnection_getHidrawName(
+        JNIEnv* env, jclass /*clazz*/, int fd) {
+    char buf[NAME_SIZE_MAX];
+    if (ioctl(fd, HIDIOCGRAWNAME(NAME_SIZE_MAX), buf) < 0) {
+        return nullptr;
+    }
+    // Local ref is not deleted because it is returned to Java
+    return env->NewStringUTF(buf);
+}
+
 static const JNINativeMethod gMethods[] = {
         {"nativeGetHidrawDescSize", "(I)I",
          (void*)com_android_server_accessibility_BrailleDisplayConnection_getHidrawDescSize},
@@ -91,6 +101,8 @@
          (void*)com_android_server_accessibility_BrailleDisplayConnection_getHidrawUniq},
         {"nativeGetHidrawBusType", "(I)I",
          (void*)com_android_server_accessibility_BrailleDisplayConnection_getHidrawBusType},
+        {"nativeGetHidrawName", "(I)Ljava/lang/String;",
+         (void*)com_android_server_accessibility_BrailleDisplayConnection_getHidrawName},
 };
 
 int register_com_android_server_accessibility_BrailleDisplayConnection(JNIEnv* env) {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index c3175d6..318042e 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -9030,7 +9030,13 @@
 
     void sendDeviceOwnerOrProfileOwnerCommand(String action, Bundle extras, int userId) {
         if (userId == UserHandle.USER_ALL) {
-            userId = UserHandle.USER_SYSTEM;
+            if (Flags.headlessDeviceOwnerDelegateSecurityLoggingBugFix()
+                    && getHeadlessDeviceOwnerModeForDeviceOwner()
+                    == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER) {
+                userId = mOwners.getDeviceOwnerUserId();
+            } else {
+                userId = UserHandle.USER_SYSTEM;
+            }
         }
         boolean inForeground = false;
         ComponentName receiverComponent = null;
diff --git a/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
index dd87572..54de64e 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
@@ -22,7 +22,9 @@
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DOZE;
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_IDLE;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyFloat;
@@ -36,6 +38,9 @@
 
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
+import android.hardware.Sensor;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
 import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest;
 import android.os.Handler;
 import android.os.PowerManager;
@@ -47,8 +52,6 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
-import com.android.internal.os.Clock;
-import com.android.server.display.brightness.LightSensorController;
 import com.android.server.display.brightness.clamper.BrightnessClamperController;
 import com.android.server.testutils.OffsettableClock;
 
@@ -58,6 +61,7 @@
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
 @SmallTest
@@ -65,18 +69,31 @@
 public class AutomaticBrightnessControllerTest {
     private static final float BRIGHTNESS_MIN_FLOAT = 0.0f;
     private static final float BRIGHTNESS_MAX_FLOAT = 1.0f;
+    private static final int LIGHT_SENSOR_RATE = 20;
     private static final int INITIAL_LIGHT_SENSOR_RATE = 20;
+    private static final int BRIGHTENING_LIGHT_DEBOUNCE_CONFIG = 2000;
+    private static final int DARKENING_LIGHT_DEBOUNCE_CONFIG = 4000;
+    private static final int BRIGHTENING_LIGHT_DEBOUNCE_CONFIG_IDLE = 1000;
+    private static final int DARKENING_LIGHT_DEBOUNCE_CONFIG_IDLE = 2000;
     private static final float DOZE_SCALE_FACTOR = 0.54f;
+    private static final boolean RESET_AMBIENT_LUX_AFTER_WARMUP_CONFIG = false;
+    private static final int LIGHT_SENSOR_WARMUP_TIME = 0;
+    private static final int AMBIENT_LIGHT_HORIZON_SHORT = 1000;
+    private static final int AMBIENT_LIGHT_HORIZON_LONG = 2000;
     private static final float EPSILON = 0.001f;
     private OffsettableClock mClock = new OffsettableClock();
     private TestLooper mTestLooper;
     private Context mContext;
     private AutomaticBrightnessController mController;
+    private Sensor mLightSensor;
 
+    @Mock SensorManager mSensorManager;
     @Mock BrightnessMappingStrategy mBrightnessMappingStrategy;
     @Mock BrightnessMappingStrategy mIdleBrightnessMappingStrategy;
     @Mock BrightnessMappingStrategy mDozeBrightnessMappingStrategy;
+    @Mock HysteresisLevels mAmbientBrightnessThresholds;
     @Mock HysteresisLevels mScreenBrightnessThresholds;
+    @Mock HysteresisLevels mAmbientBrightnessThresholdsIdle;
     @Mock HysteresisLevels mScreenBrightnessThresholdsIdle;
     @Mock Handler mNoOpHandler;
     @Mock BrightnessRangeController mBrightnessRangeController;
@@ -84,18 +101,17 @@
     BrightnessClamperController mBrightnessClamperController;
     @Mock BrightnessThrottler mBrightnessThrottler;
 
-    @Mock
-    LightSensorController mLightSensorController;
-
     @Before
     public void setUp() throws Exception {
         // Share classloader to allow package private access.
         System.setProperty("dexmaker.share_classloader", "true");
         MockitoAnnotations.initMocks(this);
 
+        mLightSensor = TestUtils.createSensor(Sensor.TYPE_LIGHT, "Light Sensor");
         mContext = InstrumentationRegistry.getContext();
         setupController(BrightnessMappingStrategy.INVALID_LUX,
-                BrightnessMappingStrategy.INVALID_NITS);
+                BrightnessMappingStrategy.INVALID_NITS, /* applyDebounce= */ false,
+                /* useHorizon= */ true);
     }
 
     @After
@@ -107,7 +123,8 @@
         }
     }
 
-    private void setupController(float userLux, float userNits) {
+    private void setupController(float userLux, float userNits, boolean applyDebounce,
+            boolean useHorizon) {
         mClock = new OffsettableClock.Stopped();
         mTestLooper = new TestLooper(mClock::now);
 
@@ -130,22 +147,25 @@
                     }
 
                     @Override
-                    Clock createClock() {
-                        return new Clock() {
-                            @Override
-                            public long uptimeMillis() {
-                                return mClock.now();
-                            }
-                        };
+                    AutomaticBrightnessController.Clock createClock() {
+                        return mClock::now;
                     }
 
                 }, // pass in test looper instead, pass in offsettable clock
-                () -> { }, mTestLooper.getLooper(),
-                brightnessMappingStrategyMap, BRIGHTNESS_MIN_FLOAT,
-                BRIGHTNESS_MAX_FLOAT, DOZE_SCALE_FACTOR, mScreenBrightnessThresholds,
-                mScreenBrightnessThresholdsIdle,
+                () -> { }, mTestLooper.getLooper(), mSensorManager, mLightSensor,
+                brightnessMappingStrategyMap, LIGHT_SENSOR_WARMUP_TIME, BRIGHTNESS_MIN_FLOAT,
+                BRIGHTNESS_MAX_FLOAT, DOZE_SCALE_FACTOR, LIGHT_SENSOR_RATE,
+                INITIAL_LIGHT_SENSOR_RATE, applyDebounce ? BRIGHTENING_LIGHT_DEBOUNCE_CONFIG : 0,
+                applyDebounce ? DARKENING_LIGHT_DEBOUNCE_CONFIG : 0,
+                applyDebounce ? BRIGHTENING_LIGHT_DEBOUNCE_CONFIG_IDLE : 0,
+                applyDebounce ? DARKENING_LIGHT_DEBOUNCE_CONFIG_IDLE : 0,
+                RESET_AMBIENT_LUX_AFTER_WARMUP_CONFIG,
+                mAmbientBrightnessThresholds, mScreenBrightnessThresholds,
+                mAmbientBrightnessThresholdsIdle, mScreenBrightnessThresholdsIdle,
                 mContext, mBrightnessRangeController, mBrightnessThrottler,
-                userLux, userNits, mLightSensorController, mBrightnessClamperController
+                useHorizon ? AMBIENT_LIGHT_HORIZON_SHORT : 1,
+                useHorizon ? AMBIENT_LIGHT_HORIZON_LONG : 10000, userLux, userNits,
+                mBrightnessClamperController
         );
 
         when(mBrightnessRangeController.getCurrentBrightnessMax()).thenReturn(
@@ -166,15 +186,20 @@
 
     @Test
     public void testNoHysteresisAtMinBrightness() throws Exception {
-        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
-                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
-        verify(mLightSensorController).setListener(listenerCaptor.capture());
-        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
 
         // Set up system to return 0.02f as a brightness value
         float lux1 = 100.0f;
         // Brightness as float (from 0.0f to 1.0f)
         float normalizedBrightness1 = 0.02f;
+        when(mAmbientBrightnessThresholds.getBrighteningThreshold(lux1))
+                .thenReturn(lux1);
+        when(mAmbientBrightnessThresholds.getDarkeningThreshold(lux1))
+                .thenReturn(lux1);
         when(mBrightnessMappingStrategy.getBrightness(eq(lux1), eq(null), anyInt()))
                 .thenReturn(normalizedBrightness1);
 
@@ -185,31 +210,39 @@
                 .thenReturn(1.0f);
 
         // Send new sensor value and verify
-        listener.onAmbientLuxChange(lux1);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux1));
         assertEquals(normalizedBrightness1, mController.getAutomaticScreenBrightness(), EPSILON);
 
         // Set up system to return 0.0f (minimum possible brightness) as a brightness value
         float lux2 = 10.0f;
         float normalizedBrightness2 = 0.0f;
+        when(mAmbientBrightnessThresholds.getBrighteningThreshold(lux2))
+                .thenReturn(lux2);
+        when(mAmbientBrightnessThresholds.getDarkeningThreshold(lux2))
+                .thenReturn(lux2);
         when(mBrightnessMappingStrategy.getBrightness(anyFloat(), eq(null), anyInt()))
                 .thenReturn(normalizedBrightness2);
 
         // Send new sensor value and verify
-        listener.onAmbientLuxChange(lux2);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux2));
         assertEquals(normalizedBrightness2, mController.getAutomaticScreenBrightness(), EPSILON);
     }
 
     @Test
     public void testNoHysteresisAtMaxBrightness() throws Exception {
-        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
-                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
-        verify(mLightSensorController).setListener(listenerCaptor.capture());
-        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
 
         // Set up system to return 0.98f as a brightness value
         float lux1 = 100.0f;
         float normalizedBrightness1 = 0.98f;
-
+        when(mAmbientBrightnessThresholds.getBrighteningThreshold(lux1))
+                .thenReturn(lux1);
+        when(mAmbientBrightnessThresholds.getDarkeningThreshold(lux1))
+                .thenReturn(lux1);
         when(mBrightnessMappingStrategy.getBrightness(eq(lux1), eq(null), anyInt()))
                 .thenReturn(normalizedBrightness1);
 
@@ -220,30 +253,35 @@
                 .thenReturn(1.1f);
 
         // Send new sensor value and verify
-        listener.onAmbientLuxChange(lux1);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux1));
         assertEquals(normalizedBrightness1, mController.getAutomaticScreenBrightness(), EPSILON);
 
 
         // Set up system to return 1.0f as a brightness value (brightness_max)
         float lux2 = 110.0f;
         float normalizedBrightness2 = 1.0f;
+        when(mAmbientBrightnessThresholds.getBrighteningThreshold(lux2))
+                .thenReturn(lux2);
+        when(mAmbientBrightnessThresholds.getDarkeningThreshold(lux2))
+                .thenReturn(lux2);
         when(mBrightnessMappingStrategy.getBrightness(anyFloat(), eq(null), anyInt()))
                 .thenReturn(normalizedBrightness2);
 
         // Send new sensor value and verify
-        listener.onAmbientLuxChange(lux2);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux2));
         assertEquals(normalizedBrightness2, mController.getAutomaticScreenBrightness(), EPSILON);
     }
 
     @Test
     public void testUserAddUserDataPoint() throws Exception {
-        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
-                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
-        verify(mLightSensorController).setListener(listenerCaptor.capture());
-        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
 
         // Sensor reads 1000 lux,
-        listener.onAmbientLuxChange(1000);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1000));
 
         // User sets brightness to 100
         mController.configure(AUTO_BRIGHTNESS_ENABLED, null /* configuration= */,
@@ -260,11 +298,12 @@
     public void testRecalculateSplines() throws Exception {
         // Enabling the light sensor, and setting the ambient lux to 1000
         int currentLux = 1000;
-        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
-                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
-        verify(mLightSensorController).setListener(listenerCaptor.capture());
-        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
-        listener.onAmbientLuxChange(currentLux);
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, currentLux));
 
         // User sets brightness to 0.5f
         when(mBrightnessMappingStrategy.getBrightness(currentLux,
@@ -294,13 +333,14 @@
 
     @Test
     public void testShortTermModelTimesOut() throws Exception {
-        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
-                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
-        verify(mLightSensorController).setListener(listenerCaptor.capture());
-        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
 
         // Sensor reads 123 lux,
-        listener.onAmbientLuxChange(123);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 123));
         // User sets brightness to 100
         mController.configure(AUTO_BRIGHTNESS_ENABLED, /* configuration= */ null,
                 /* brightness= */ 0.5f, /* userChangedBrightness= */ true, /* adjustment= */ 0,
@@ -314,7 +354,7 @@
                 123f, 0.5f)).thenReturn(true);
 
         // Sensor reads 1000 lux,
-        listener.onAmbientLuxChange(1000);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1000));
         mTestLooper.moveTimeForward(
                 mBrightnessMappingStrategy.getShortTermModelTimeout() + 1000);
         mTestLooper.dispatchAll();
@@ -333,13 +373,14 @@
 
     @Test
     public void testShortTermModelDoesntTimeOut() throws Exception {
-        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
-                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
-        verify(mLightSensorController).setListener(listenerCaptor.capture());
-        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
 
         // Sensor reads 123 lux,
-        listener.onAmbientLuxChange(123);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 123));
         // User sets brightness to 100
         mController.configure(AUTO_BRIGHTNESS_ENABLED, null /* configuration= */,
                 0.51f /* brightness= */, true /* userChangedBrightness= */, 0 /* adjustment= */,
@@ -358,7 +399,7 @@
         mTestLooper.dispatchAll();
 
         // Sensor reads 100000 lux,
-        listener.onAmbientLuxChange(678910);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 678910));
         mController.switchMode(AUTO_BRIGHTNESS_MODE_DEFAULT);
 
         // Verify short term model is not reset.
@@ -372,13 +413,14 @@
 
     @Test
     public void testShortTermModelIsRestoredWhenSwitchingWithinTimeout() throws Exception {
-        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
-                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
-        verify(mLightSensorController).setListener(listenerCaptor.capture());
-        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
 
         // Sensor reads 123 lux,
-        listener.onAmbientLuxChange(123);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 123));
         // User sets brightness to 100
         mController.configure(AUTO_BRIGHTNESS_ENABLED, /* configuration= */ null,
                 /* brightness= */ 0.5f, /* userChangedBrightness= */ true, /* adjustment= */ 0,
@@ -398,7 +440,7 @@
                 123f, 0.5f)).thenReturn(true);
 
         // Sensor reads 1000 lux,
-        listener.onAmbientLuxChange(1000);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1000));
         mTestLooper.moveTimeForward(
                 mBrightnessMappingStrategy.getShortTermModelTimeout() + 1000);
         mTestLooper.dispatchAll();
@@ -417,13 +459,14 @@
 
     @Test
     public void testShortTermModelNotRestoredAfterTimeout() throws Exception {
-        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
-                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
-        verify(mLightSensorController).setListener(listenerCaptor.capture());
-        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
 
         // Sensor reads 123 lux,
-        listener.onAmbientLuxChange(123);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 123));
         // User sets brightness to 100
         mController.configure(AUTO_BRIGHTNESS_ENABLED, /* configuration= */ null,
                 /* brightness= */ 0.5f, /* userChangedBrightness= */ true, /* adjustment= */ 0,
@@ -445,7 +488,7 @@
                 123f, 0.5f)).thenReturn(true);
 
         // Sensor reads 1000 lux,
-        listener.onAmbientLuxChange(1000);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1000));
         // Do not fast-forward time.
         mTestLooper.dispatchAll();
 
@@ -463,13 +506,14 @@
 
     @Test
     public void testSwitchBetweenModesNoUserInteractions() throws Exception {
-        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
-                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
-        verify(mLightSensorController).setListener(listenerCaptor.capture());
-        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
 
         // Sensor reads 123 lux,
-        listener.onAmbientLuxChange(123);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 123));
         when(mBrightnessMappingStrategy.getShortTermModelTimeout()).thenReturn(2000L);
         when(mBrightnessMappingStrategy.getUserBrightness()).thenReturn(
                 PowerManager.BRIGHTNESS_INVALID_FLOAT);
@@ -485,7 +529,7 @@
                 BrightnessMappingStrategy.INVALID_LUX);
 
         // Sensor reads 1000 lux,
-        listener.onAmbientLuxChange(1000);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1000));
         // Do not fast-forward time.
         mTestLooper.dispatchAll();
 
@@ -501,19 +545,14 @@
 
     @Test
     public void testSwitchToIdleMappingStrategy() throws Exception {
-        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
-                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
-        verify(mLightSensorController).setListener(listenerCaptor.capture());
-        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
-        clearInvocations(mBrightnessMappingStrategy);
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
 
         // Sensor reads 1000 lux,
-        listener.onAmbientLuxChange(1000);
-
-
-        verify(mBrightnessMappingStrategy).getBrightness(anyFloat(), any(), anyInt());
-
-        clearInvocations(mBrightnessMappingStrategy);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1000));
 
         // User sets brightness to 100
         mController.configure(AUTO_BRIGHTNESS_ENABLED, null /* configuration= */,
@@ -522,19 +561,22 @@
                 /* shouldResetShortTermModel= */ true);
 
         // There should be a user data point added to the mapper.
-        verify(mBrightnessMappingStrategy).addUserDataPoint(/* lux= */ 1000f,
+        verify(mBrightnessMappingStrategy, times(1)).addUserDataPoint(/* lux= */ 1000f,
                 /* brightness= */ 0.5f);
-        verify(mBrightnessMappingStrategy).setBrightnessConfiguration(any());
-        verify(mBrightnessMappingStrategy).getBrightness(anyFloat(), any(), anyInt());
+        verify(mBrightnessMappingStrategy, times(2)).setBrightnessConfiguration(any());
+        verify(mBrightnessMappingStrategy, times(3)).getBrightness(anyFloat(), any(), anyInt());
 
-        clearInvocations(mBrightnessMappingStrategy);
         // Now let's do the same for idle mode
         mController.switchMode(AUTO_BRIGHTNESS_MODE_IDLE);
-
-        verify(mBrightnessMappingStrategy).getMode();
-        verify(mBrightnessMappingStrategy).getShortTermModelTimeout();
-        verify(mBrightnessMappingStrategy).getUserBrightness();
-        verify(mBrightnessMappingStrategy).getUserLux();
+        // Called once when switching,
+        // setAmbientLux() is called twice and once in updateAutoBrightness(),
+        // nextAmbientLightBrighteningTransition() and nextAmbientLightDarkeningTransition() are
+        // called twice each.
+        verify(mBrightnessMappingStrategy, times(8)).getMode();
+        // Called when switching.
+        verify(mBrightnessMappingStrategy, times(1)).getShortTermModelTimeout();
+        verify(mBrightnessMappingStrategy, times(1)).getUserBrightness();
+        verify(mBrightnessMappingStrategy, times(1)).getUserLux();
 
         // Ensure, after switching, original BMS is not used anymore
         verifyNoMoreInteractions(mBrightnessMappingStrategy);
@@ -546,25 +588,154 @@
                 /* shouldResetShortTermModel= */ true);
 
         // Ensure we use the correct mapping strategy
-        verify(mIdleBrightnessMappingStrategy).addUserDataPoint(/* lux= */ 1000f,
+        verify(mIdleBrightnessMappingStrategy, times(1)).addUserDataPoint(/* lux= */ 1000f,
                 /* brightness= */ 0.5f);
     }
 
     @Test
+    public void testAmbientLightHorizon() throws Exception {
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
+
+        long increment = 500;
+        // set autobrightness to low
+        // t = 0
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0));
+
+        // t = 500
+        mClock.fastForward(increment);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0));
+
+        // t = 1000
+        mClock.fastForward(increment);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0));
+        assertEquals(0.0f, mController.getAmbientLux(), EPSILON);
+
+        // t = 1500
+        mClock.fastForward(increment);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0));
+        assertEquals(0.0f, mController.getAmbientLux(), EPSILON);
+
+        // t = 2000
+        // ensure that our reading is at 0.
+        mClock.fastForward(increment);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0));
+        assertEquals(0.0f, mController.getAmbientLux(), EPSILON);
+
+        // t = 2500
+        // first 10000 lux sensor event reading
+        mClock.fastForward(increment);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000));
+        assertTrue(mController.getAmbientLux() > 0.0f);
+        assertTrue(mController.getAmbientLux() < 10000.0f);
+
+        // t = 3000
+        // lux reading should still not yet be 10000.
+        mClock.fastForward(increment);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000));
+        assertTrue(mController.getAmbientLux() > 0.0f);
+        assertTrue(mController.getAmbientLux() < 10000.0f);
+
+        // t = 3500
+        mClock.fastForward(increment);
+        // lux has been high (10000) for 1000ms.
+        // lux reading should be 10000
+        // short horizon (ambient lux) is high, long horizon is still not high
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000));
+        assertEquals(10000.0f, mController.getAmbientLux(), EPSILON);
+
+        // t = 4000
+        // stay high
+        mClock.fastForward(increment);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000));
+        assertEquals(10000.0f, mController.getAmbientLux(), EPSILON);
+
+        // t = 4500
+        Mockito.clearInvocations(mBrightnessMappingStrategy);
+        mClock.fastForward(increment);
+        // short horizon is high, long horizon is high too
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 10000));
+        verify(mBrightnessMappingStrategy, times(1)).getBrightness(10000, null, -1);
+        assertEquals(10000.0f, mController.getAmbientLux(), EPSILON);
+
+        // t = 5000
+        mClock.fastForward(increment);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0));
+        assertTrue(mController.getAmbientLux() > 0.0f);
+        assertTrue(mController.getAmbientLux() < 10000.0f);
+
+        // t = 5500
+        mClock.fastForward(increment);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0));
+        assertTrue(mController.getAmbientLux() > 0.0f);
+        assertTrue(mController.getAmbientLux() < 10000.0f);
+
+        // t = 6000
+        mClock.fastForward(increment);
+        // ambient lux goes to 0
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 0));
+        assertEquals(0.0f, mController.getAmbientLux(), EPSILON);
+
+        // only the values within the horizon should be kept
+        assertArrayEquals(new float[] {10000, 10000, 0, 0, 0}, mController.getLastSensorValues(),
+                EPSILON);
+        assertArrayEquals(new long[] {4000, 4500, 5000, 5500, 6000},
+                mController.getLastSensorTimestamps());
+    }
+
+    @Test
+    public void testHysteresisLevels() {
+        float[] ambientBrighteningThresholds = {50, 100};
+        float[] ambientDarkeningThresholds = {10, 20};
+        float[] ambientThresholdLevels = {0, 500};
+        float ambientDarkeningMinChangeThreshold = 3.0f;
+        float ambientBrighteningMinChangeThreshold = 1.5f;
+        HysteresisLevels hysteresisLevels = new HysteresisLevels(ambientBrighteningThresholds,
+                ambientDarkeningThresholds, ambientThresholdLevels, ambientThresholdLevels,
+                ambientDarkeningMinChangeThreshold, ambientBrighteningMinChangeThreshold);
+
+        // test low, activate minimum change thresholds.
+        assertEquals(1.5f, hysteresisLevels.getBrighteningThreshold(0.0f), EPSILON);
+        assertEquals(0f, hysteresisLevels.getDarkeningThreshold(0.0f), EPSILON);
+        assertEquals(1f, hysteresisLevels.getDarkeningThreshold(4.0f), EPSILON);
+
+        // test max
+        // epsilon is x2 here, since the next floating point value about 20,000 is 0.0019531 greater
+        assertEquals(20000f, hysteresisLevels.getBrighteningThreshold(10000.0f), EPSILON * 2);
+        assertEquals(8000f, hysteresisLevels.getDarkeningThreshold(10000.0f), EPSILON);
+
+        // test just below threshold
+        assertEquals(748.5f, hysteresisLevels.getBrighteningThreshold(499f), EPSILON);
+        assertEquals(449.1f, hysteresisLevels.getDarkeningThreshold(499f), EPSILON);
+
+        // test at (considered above) threshold
+        assertEquals(1000f, hysteresisLevels.getBrighteningThreshold(500f), EPSILON);
+        assertEquals(400f, hysteresisLevels.getDarkeningThreshold(500f), EPSILON);
+    }
+
+    @Test
     public void testBrightnessGetsThrottled() throws Exception {
-        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
-                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
-        verify(mLightSensorController).setListener(listenerCaptor.capture());
-        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
 
         // Set up system to return max brightness at 100 lux
         final float normalizedBrightness = BRIGHTNESS_MAX_FLOAT;
         final float lux = 100.0f;
+        when(mAmbientBrightnessThresholds.getBrighteningThreshold(lux))
+                .thenReturn(lux);
+        when(mAmbientBrightnessThresholds.getDarkeningThreshold(lux))
+                .thenReturn(lux);
         when(mBrightnessMappingStrategy.getBrightness(eq(lux), eq(null), anyInt()))
                 .thenReturn(normalizedBrightness);
 
         // Sensor reads 100 lux. We should get max brightness.
-        listener.onAmbientLuxChange(lux);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux));
         assertEquals(BRIGHTNESS_MAX_FLOAT, mController.getAutomaticScreenBrightness(), 0.0f);
         assertEquals(BRIGHTNESS_MAX_FLOAT, mController.getRawAutomaticScreenBrightness(), 0.0f);
 
@@ -592,6 +763,94 @@
     }
 
     @Test
+    public void testGetSensorReadings() throws Exception {
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
+
+        // Choose values such that the ring buffer's capacity is extended and the buffer is pruned
+        int increment = 11;
+        int lux = 5000;
+        for (int i = 0; i < 1000; i++) {
+            lux += increment;
+            mClock.fastForward(increment);
+            listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, lux));
+        }
+
+        int valuesCount = (int) Math.ceil((double) AMBIENT_LIGHT_HORIZON_LONG / increment + 1);
+        float[] sensorValues = mController.getLastSensorValues();
+        long[] sensorTimestamps = mController.getLastSensorTimestamps();
+
+        // Only the values within the horizon should be kept
+        assertEquals(valuesCount, sensorValues.length);
+        assertEquals(valuesCount, sensorTimestamps.length);
+
+        long sensorTimestamp = mClock.now();
+        for (int i = valuesCount - 1; i >= 1; i--) {
+            assertEquals(lux, sensorValues[i], EPSILON);
+            assertEquals(sensorTimestamp, sensorTimestamps[i]);
+            lux -= increment;
+            sensorTimestamp -= increment;
+        }
+        assertEquals(lux, sensorValues[0], EPSILON);
+        assertEquals(mClock.now() - AMBIENT_LIGHT_HORIZON_LONG, sensorTimestamps[0]);
+    }
+
+    @Test
+    public void testGetSensorReadingsFullBuffer() throws Exception {
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
+        int initialCapacity = 150;
+
+        // Choose values such that the ring buffer is pruned
+        int increment1 = 200;
+        int lux = 5000;
+        for (int i = 0; i < 20; i++) {
+            lux += increment1;
+            mClock.fastForward(increment1);
+            listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, lux));
+        }
+
+        int valuesCount = (int) Math.ceil((double) AMBIENT_LIGHT_HORIZON_LONG / increment1 + 1);
+
+        // Choose values such that the buffer becomes full
+        int increment2 = 1;
+        for (int i = 0; i < initialCapacity - valuesCount; i++) {
+            lux += increment2;
+            mClock.fastForward(increment2);
+            listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, lux));
+        }
+
+        float[] sensorValues = mController.getLastSensorValues();
+        long[] sensorTimestamps = mController.getLastSensorTimestamps();
+
+        // The buffer should be full
+        assertEquals(initialCapacity, sensorValues.length);
+        assertEquals(initialCapacity, sensorTimestamps.length);
+
+        long sensorTimestamp = mClock.now();
+        for (int i = initialCapacity - 1; i >= 1; i--) {
+            assertEquals(lux, sensorValues[i], EPSILON);
+            assertEquals(sensorTimestamp, sensorTimestamps[i]);
+
+            if (i >= valuesCount) {
+                lux -= increment2;
+                sensorTimestamp -= increment2;
+            } else {
+                lux -= increment1;
+                sensorTimestamp -= increment1;
+            }
+        }
+        assertEquals(lux, sensorValues[0], EPSILON);
+        assertEquals(mClock.now() - AMBIENT_LIGHT_HORIZON_LONG, sensorTimestamps[0]);
+    }
+
+    @Test
     public void testResetShortTermModelWhenConfigChanges() {
         when(mBrightnessMappingStrategy.setBrightnessConfiguration(any())).thenReturn(true);
 
@@ -616,22 +875,179 @@
         float userNits = 500;
         float userBrightness = 0.3f;
         when(mBrightnessMappingStrategy.getBrightnessFromNits(userNits)).thenReturn(userBrightness);
-        setupController(userLux, userNits);
+        setupController(userLux, userNits, /* applyDebounce= */ true,
+                /* useHorizon= */ false);
         verify(mBrightnessMappingStrategy).addUserDataPoint(userLux, userBrightness);
     }
 
     @Test
+    public void testBrighteningLightDebounce() throws Exception {
+        clearInvocations(mSensorManager);
+        setupController(BrightnessMappingStrategy.INVALID_LUX,
+                BrightnessMappingStrategy.INVALID_NITS, /* applyDebounce= */ true,
+                /* useHorizon= */ false);
+
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
+
+        // t = 0
+        // Initial lux
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500));
+        assertEquals(500, mController.getAmbientLux(), EPSILON);
+
+        // t = 1000
+        // Lux isn't steady yet
+        mClock.fastForward(1000);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200));
+        assertEquals(500, mController.getAmbientLux(), EPSILON);
+
+        // t = 1500
+        // Lux isn't steady yet
+        mClock.fastForward(500);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200));
+        assertEquals(500, mController.getAmbientLux(), EPSILON);
+
+        // t = 2500
+        // Lux is steady now
+        mClock.fastForward(1000);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200));
+        assertEquals(1200, mController.getAmbientLux(), EPSILON);
+    }
+
+    @Test
+    public void testDarkeningLightDebounce() throws Exception {
+        clearInvocations(mSensorManager);
+        when(mAmbientBrightnessThresholds.getBrighteningThreshold(anyFloat()))
+                .thenReturn(10000f);
+        when(mAmbientBrightnessThresholds.getDarkeningThreshold(anyFloat()))
+                .thenReturn(10000f);
+        setupController(BrightnessMappingStrategy.INVALID_LUX,
+                BrightnessMappingStrategy.INVALID_NITS, /* applyDebounce= */ true,
+                /* useHorizon= */ false);
+
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
+
+        // t = 0
+        // Initial lux
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200));
+        assertEquals(1200, mController.getAmbientLux(), EPSILON);
+
+        // t = 2000
+        // Lux isn't steady yet
+        mClock.fastForward(2000);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500));
+        assertEquals(1200, mController.getAmbientLux(), EPSILON);
+
+        // t = 2500
+        // Lux isn't steady yet
+        mClock.fastForward(500);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500));
+        assertEquals(1200, mController.getAmbientLux(), EPSILON);
+
+        // t = 4500
+        // Lux is steady now
+        mClock.fastForward(2000);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500));
+        assertEquals(500, mController.getAmbientLux(), EPSILON);
+    }
+
+    @Test
+    public void testBrighteningLightDebounceIdle() throws Exception {
+        clearInvocations(mSensorManager);
+        setupController(BrightnessMappingStrategy.INVALID_LUX,
+                BrightnessMappingStrategy.INVALID_NITS, /* applyDebounce= */ true,
+                /* useHorizon= */ false);
+
+        mController.switchMode(AUTO_BRIGHTNESS_MODE_IDLE);
+
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
+
+        // t = 0
+        // Initial lux
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500));
+        assertEquals(500, mController.getAmbientLux(), EPSILON);
+
+        // t = 500
+        // Lux isn't steady yet
+        mClock.fastForward(500);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200));
+        assertEquals(500, mController.getAmbientLux(), EPSILON);
+
+        // t = 1500
+        // Lux is steady now
+        mClock.fastForward(1000);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200));
+        assertEquals(1200, mController.getAmbientLux(), EPSILON);
+    }
+
+    @Test
+    public void testDarkeningLightDebounceIdle() throws Exception {
+        clearInvocations(mSensorManager);
+        when(mAmbientBrightnessThresholdsIdle.getBrighteningThreshold(anyFloat()))
+                .thenReturn(10000f);
+        when(mAmbientBrightnessThresholdsIdle.getDarkeningThreshold(anyFloat()))
+                .thenReturn(10000f);
+        setupController(BrightnessMappingStrategy.INVALID_LUX,
+                BrightnessMappingStrategy.INVALID_NITS, /* applyDebounce= */ true,
+                /* useHorizon= */ false);
+
+        mController.switchMode(AUTO_BRIGHTNESS_MODE_IDLE);
+
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
+
+        // t = 0
+        // Initial lux
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 1200));
+        assertEquals(1200, mController.getAmbientLux(), EPSILON);
+
+        // t = 1000
+        // Lux isn't steady yet
+        mClock.fastForward(1000);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500));
+        assertEquals(1200, mController.getAmbientLux(), EPSILON);
+
+        // t = 2500
+        // Lux is steady now
+        mClock.fastForward(1500);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, 500));
+        assertEquals(500, mController.getAmbientLux(), EPSILON);
+    }
+
+    @Test
     public void testBrightnessBasedOnLastObservedLux() throws Exception {
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
+
         // Set up system to return 0.3f as a brightness value
         float lux = 100.0f;
         // Brightness as float (from 0.0f to 1.0f)
         float normalizedBrightness = 0.3f;
-        when(mLightSensorController.getLastObservedLux()).thenReturn(lux);
+        when(mAmbientBrightnessThresholds.getBrighteningThreshold(lux)).thenReturn(lux);
+        when(mAmbientBrightnessThresholds.getDarkeningThreshold(lux)).thenReturn(lux);
         when(mBrightnessMappingStrategy.getBrightness(eq(lux), /* packageName= */ eq(null),
                 /* category= */ anyInt())).thenReturn(normalizedBrightness);
         when(mBrightnessThrottler.getBrightnessCap()).thenReturn(BRIGHTNESS_MAX_FLOAT);
 
         // Send a new sensor value, disable the sensor and verify
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux));
         mController.configure(AUTO_BRIGHTNESS_DISABLED, /* configuration= */ null,
                 /* brightness= */ 0, /* userChangedBrightness= */ false, /* adjustment= */ 0,
                 /* userChanged= */ false, DisplayPowerRequest.POLICY_BRIGHT, Display.STATE_ON,
@@ -643,19 +1059,21 @@
 
     @Test
     public void testAutoBrightnessInDoze() throws Exception {
-        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
-                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
-        verify(mLightSensorController).setListener(listenerCaptor.capture());
-        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
 
         // Set up system to return 0.3f as a brightness value
         float lux = 100.0f;
         // Brightness as float (from 0.0f to 1.0f)
         float normalizedBrightness = 0.3f;
+        when(mAmbientBrightnessThresholds.getBrighteningThreshold(lux)).thenReturn(lux);
+        when(mAmbientBrightnessThresholds.getDarkeningThreshold(lux)).thenReturn(lux);
         when(mBrightnessMappingStrategy.getBrightness(eq(lux), /* packageName= */ eq(null),
                 /* category= */ anyInt())).thenReturn(normalizedBrightness);
         when(mBrightnessThrottler.getBrightnessCap()).thenReturn(BRIGHTNESS_MAX_FLOAT);
-        when(mLightSensorController.getLastObservedLux()).thenReturn(lux);
 
         // Set policy to DOZE
         mController.configure(AUTO_BRIGHTNESS_ENABLED, /* configuration= */ null,
@@ -664,7 +1082,7 @@
                 /* shouldResetShortTermModel= */ true);
 
         // Send a new sensor value
-        listener.onAmbientLuxChange(lux);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux));
 
         // The brightness should be scaled by the doze factor
         assertEquals(normalizedBrightness * DOZE_SCALE_FACTOR,
@@ -677,19 +1095,21 @@
 
     @Test
     public void testAutoBrightnessInDoze_ShouldNotScaleIfUsingDozeCurve() throws Exception {
-        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
-                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
-        verify(mLightSensorController).setListener(listenerCaptor.capture());
-        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
 
         // Set up system to return 0.3f as a brightness value
         float lux = 100.0f;
         // Brightness as float (from 0.0f to 1.0f)
         float normalizedBrightness = 0.3f;
+        when(mAmbientBrightnessThresholds.getBrighteningThreshold(lux)).thenReturn(lux);
+        when(mAmbientBrightnessThresholds.getDarkeningThreshold(lux)).thenReturn(lux);
         when(mDozeBrightnessMappingStrategy.getBrightness(eq(lux), /* packageName= */ eq(null),
                 /* category= */ anyInt())).thenReturn(normalizedBrightness);
         when(mBrightnessThrottler.getBrightnessCap()).thenReturn(BRIGHTNESS_MAX_FLOAT);
-        when(mLightSensorController.getLastObservedLux()).thenReturn(lux);
 
         // Switch mode to DOZE
         mController.switchMode(AUTO_BRIGHTNESS_MODE_DOZE);
@@ -701,7 +1121,7 @@
                 /* shouldResetShortTermModel= */ true);
 
         // Send a new sensor value
-        listener.onAmbientLuxChange(lux);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux));
 
         // The brightness should not be scaled by the doze factor
         assertEquals(normalizedBrightness,
@@ -713,19 +1133,21 @@
 
     @Test
     public void testAutoBrightnessInDoze_ShouldNotScaleIfScreenOn() throws Exception {
-        ArgumentCaptor<LightSensorController.LightSensorListener> listenerCaptor =
-                ArgumentCaptor.forClass(LightSensorController.LightSensorListener.class);
-        verify(mLightSensorController).setListener(listenerCaptor.capture());
-        LightSensorController.LightSensorListener listener = listenerCaptor.getValue();
+        ArgumentCaptor<SensorEventListener> listenerCaptor =
+                ArgumentCaptor.forClass(SensorEventListener.class);
+        verify(mSensorManager).registerListener(listenerCaptor.capture(), eq(mLightSensor),
+                eq(INITIAL_LIGHT_SENSOR_RATE * 1000), any(Handler.class));
+        SensorEventListener listener = listenerCaptor.getValue();
 
         // Set up system to return 0.3f as a brightness value
         float lux = 100.0f;
         // Brightness as float (from 0.0f to 1.0f)
         float normalizedBrightness = 0.3f;
+        when(mAmbientBrightnessThresholds.getBrighteningThreshold(lux)).thenReturn(lux);
+        when(mAmbientBrightnessThresholds.getDarkeningThreshold(lux)).thenReturn(lux);
         when(mBrightnessMappingStrategy.getBrightness(eq(lux), /* packageName= */ eq(null),
                 /* category= */ anyInt())).thenReturn(normalizedBrightness);
         when(mBrightnessThrottler.getBrightnessCap()).thenReturn(BRIGHTNESS_MAX_FLOAT);
-        when(mLightSensorController.getLastObservedLux()).thenReturn(lux);
 
         // Set policy to DOZE
         mController.configure(AUTO_BRIGHTNESS_ENABLED, /* configuration= */ null,
@@ -734,7 +1156,7 @@
                 /* shouldResetShortTermModel= */ true);
 
         // Send a new sensor value
-        listener.onAmbientLuxChange(lux);
+        listener.onSensorChanged(TestUtils.createSensorEvent(mLightSensor, (int) lux));
 
         // The brightness should not be scaled by the doze factor
         assertEquals(normalizedBrightness,
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
index db2a1f4..afb87d1 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -28,6 +28,7 @@
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyFloat;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isA;
@@ -78,8 +79,6 @@
 import com.android.server.display.RampAnimator.DualRampAnimator;
 import com.android.server.display.brightness.BrightnessEvent;
 import com.android.server.display.brightness.BrightnessReason;
-import com.android.server.display.brightness.LightSensorController;
-import com.android.server.display.brightness.TestUtilsKt;
 import com.android.server.display.brightness.clamper.BrightnessClamperController;
 import com.android.server.display.brightness.clamper.HdrClamper;
 import com.android.server.display.color.ColorDisplayService;
@@ -1167,19 +1166,30 @@
                 any(AutomaticBrightnessController.Callbacks.class),
                 any(Looper.class),
                 eq(mSensorManagerMock),
+                /* lightSensor= */ any(),
                 /* brightnessMappingStrategyMap= */ any(SparseArray.class),
+                /* lightSensorWarmUpTime= */ anyInt(),
                 /* brightnessMin= */ anyFloat(),
                 /* brightnessMax= */ anyFloat(),
                 /* dozeScaleFactor */ anyFloat(),
+                /* lightSensorRate= */ anyInt(),
+                /* initialLightSensorRate= */ anyInt(),
+                /* brighteningLightDebounceConfig */ anyLong(),
+                /* darkeningLightDebounceConfig */ anyLong(),
+                /* brighteningLightDebounceConfigIdle= */ anyLong(),
+                /* darkeningLightDebounceConfigIdle= */ anyLong(),
+                /* resetAmbientLuxAfterWarmUpConfig= */ anyBoolean(),
+                any(HysteresisLevels.class),
+                any(HysteresisLevels.class),
                 any(HysteresisLevels.class),
                 any(HysteresisLevels.class),
                 eq(mContext),
                 any(BrightnessRangeController.class),
                 any(BrightnessThrottler.class),
+                /* ambientLightHorizonShort= */ anyInt(),
+                /* ambientLightHorizonLong= */ anyInt(),
                 eq(lux),
                 eq(nits),
-                eq(DISPLAY_ID),
-                any(LightSensorController.LightSensorControllerConfig.class),
                 any(BrightnessClamperController.class)
         );
     }
@@ -2148,22 +2158,22 @@
         }
 
         @Override
-        LightSensorController.LightSensorControllerConfig getLightSensorControllerConfig(
-                Context context, DisplayDeviceConfig displayDeviceConfig) {
-            return TestUtilsKt.createLightSensorControllerConfig();
-        }
-
-        @Override
         AutomaticBrightnessController getAutomaticBrightnessController(
                 AutomaticBrightnessController.Callbacks callbacks, Looper looper,
-                SensorManager sensorManager,
+                SensorManager sensorManager, Sensor lightSensor,
                 SparseArray<BrightnessMappingStrategy> brightnessMappingStrategyMap,
-                float brightnessMin, float brightnessMax, float dozeScaleFactor,
+                int lightSensorWarmUpTime, float brightnessMin, float brightnessMax,
+                float dozeScaleFactor, int lightSensorRate, int initialLightSensorRate,
+                long brighteningLightDebounceConfig, long darkeningLightDebounceConfig,
+                long brighteningLightDebounceConfigIdle, long darkeningLightDebounceConfigIdle,
+                boolean resetAmbientLuxAfterWarmUpConfig,
+                HysteresisLevels ambientBrightnessThresholds,
                 HysteresisLevels screenBrightnessThresholds,
+                HysteresisLevels ambientBrightnessThresholdsIdle,
                 HysteresisLevels screenBrightnessThresholdsIdle, Context context,
                 BrightnessRangeController brightnessRangeController,
-                BrightnessThrottler brightnessThrottler, float userLux, float userNits,
-                int displayId, LightSensorController.LightSensorControllerConfig config,
+                BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
+                int ambientLightHorizonLong, float userLux, float userNits,
                 BrightnessClamperController brightnessClamperController) {
             return mAutomaticBrightnessController;
         }
@@ -2176,12 +2186,18 @@
         }
 
         @Override
-        HysteresisLevels getBrightnessThresholdsIdleHysteresisLevels(DisplayDeviceConfig ddc) {
+        HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+                float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+                float[] darkeningThresholdLevels, float minDarkeningThreshold,
+                float minBrighteningThreshold) {
             return mHysteresisLevels;
         }
 
         @Override
-        HysteresisLevels getBrightnessThresholdsHysteresisLevels(DisplayDeviceConfig ddc) {
+        HysteresisLevels getHysteresisLevels(float[] brighteningThresholdsPercentages,
+                float[] darkeningThresholdsPercentages, float[] brighteningThresholdLevels,
+                float[] darkeningThresholdLevels, float minDarkeningThreshold,
+                float minBrighteningThreshold, boolean potentialOldBrightnessRange) {
             return mHysteresisLevels;
         }
 
diff --git a/services/tests/displayservicetests/src/com/android/server/display/HysteresisLevelsTest.kt b/services/tests/displayservicetests/src/com/android/server/display/HysteresisLevelsTest.kt
deleted file mode 100644
index 02d6946..0000000
--- a/services/tests/displayservicetests/src/com/android/server/display/HysteresisLevelsTest.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.display
-
-import androidx.test.filters.SmallTest
-import com.android.server.display.brightness.createHysteresisLevels
-import kotlin.test.assertEquals
-import org.junit.Test
-
-private const val FLOAT_TOLERANCE = 0.001f
-@SmallTest
-class HysteresisLevelsTest {
-    @Test
-    fun `test hysteresis levels`() {
-        val hysteresisLevels = createHysteresisLevels(
-            brighteningThresholdsPercentages = floatArrayOf(50f, 100f),
-            darkeningThresholdsPercentages = floatArrayOf(10f, 20f),
-            brighteningThresholdLevels = floatArrayOf(0f, 500f),
-            darkeningThresholdLevels = floatArrayOf(0f, 500f),
-            minDarkeningThreshold = 3f,
-            minBrighteningThreshold = 1.5f
-        )
-
-        // test low, activate minimum change thresholds.
-        assertEquals(1.5f, hysteresisLevels.getBrighteningThreshold(0.0f), FLOAT_TOLERANCE)
-        assertEquals(0f, hysteresisLevels.getDarkeningThreshold(0.0f), FLOAT_TOLERANCE)
-        assertEquals(1f, hysteresisLevels.getDarkeningThreshold(4.0f), FLOAT_TOLERANCE)
-
-        // test max
-        // epsilon is x2 here, since the next floating point value about 20,000 is 0.0019531 greater
-        assertEquals(
-            20000f, hysteresisLevels.getBrighteningThreshold(10000.0f), FLOAT_TOLERANCE * 2)
-        assertEquals(8000f, hysteresisLevels.getDarkeningThreshold(10000.0f), FLOAT_TOLERANCE)
-
-        // test just below threshold
-        assertEquals(748.5f, hysteresisLevels.getBrighteningThreshold(499f), FLOAT_TOLERANCE)
-        assertEquals(449.1f, hysteresisLevels.getDarkeningThreshold(499f), FLOAT_TOLERANCE)
-
-        // test at (considered above) threshold
-        assertEquals(1000f, hysteresisLevels.getBrighteningThreshold(500f), FLOAT_TOLERANCE)
-        assertEquals(400f, hysteresisLevels.getDarkeningThreshold(500f), FLOAT_TOLERANCE)
-    }
-}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/AmbientLightRingBufferTest.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/AmbientLightRingBufferTest.kt
deleted file mode 100644
index 5fe9178..0000000
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/AmbientLightRingBufferTest.kt
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.display.brightness
-
-import androidx.test.filters.SmallTest
-import com.android.internal.os.Clock
-import com.android.server.display.brightness.LightSensorController.AmbientLightRingBuffer
-import com.google.common.truth.Truth.assertThat
-import org.junit.Assert.assertThrows
-import org.junit.Test
-import org.mockito.kotlin.mock
-
-
-private const val BUFFER_INITIAL_CAPACITY = 3
-
-@SmallTest
-class AmbientLightRingBufferTest {
-
-    private val buffer = AmbientLightRingBuffer(BUFFER_INITIAL_CAPACITY, mock<Clock>())
-
-    @Test
-    fun `test created empty`() {
-        assertThat(buffer.size()).isEqualTo(0)
-    }
-
-    @Test
-    fun `test push to empty buffer`() {
-        buffer.push(1000, 0.5f)
-
-        assertThat(buffer.size()).isEqualTo(1)
-        assertThat(buffer.getLux(0)).isEqualTo(0.5f)
-        assertThat(buffer.getTime(0)).isEqualTo(1000)
-    }
-
-    @Test
-    fun `test prune keeps youngest outside horizon and sets time to horizon`() {
-        buffer.push(1000, 0.5f)
-        buffer.push(2000, 0.6f)
-        buffer.push(3000, 0.7f)
-
-        buffer.prune(2500)
-
-        assertThat(buffer.size()).isEqualTo(2)
-
-        assertThat(buffer.getLux(0)).isEqualTo(0.6f)
-        assertThat(buffer.getTime(0)).isEqualTo(2500)
-    }
-
-    @Test
-    fun `test prune keeps inside horizon`() {
-        buffer.push(1000, 0.5f)
-        buffer.push(2000, 0.6f)
-        buffer.push(3000, 0.7f)
-
-        buffer.prune(2500)
-
-        assertThat(buffer.size()).isEqualTo(2)
-
-        assertThat(buffer.getLux(1)).isEqualTo(0.7f)
-        assertThat(buffer.getTime(1)).isEqualTo(3000)
-    }
-
-
-    @Test
-    fun `test pushes correctly after prune`() {
-        buffer.push(1000, 0.5f)
-        buffer.push(2000, 0.6f)
-        buffer.push(3000, 0.7f)
-        buffer.prune(2500)
-
-        buffer.push(4000, 0.8f)
-
-        assertThat(buffer.size()).isEqualTo(3)
-
-        assertThat(buffer.getLux(0)).isEqualTo(0.6f)
-        assertThat(buffer.getTime(0)).isEqualTo(2500)
-        assertThat(buffer.getLux(1)).isEqualTo(0.7f)
-        assertThat(buffer.getTime(1)).isEqualTo(3000)
-        assertThat(buffer.getLux(2)).isEqualTo(0.8f)
-        assertThat(buffer.getTime(2)).isEqualTo(4000)
-    }
-
-    @Test
-    fun `test increase buffer size`() {
-        buffer.push(1000, 0.5f)
-        buffer.push(2000, 0.6f)
-        buffer.push(3000, 0.7f)
-
-        buffer.push(4000, 0.8f)
-
-        assertThat(buffer.size()).isEqualTo(4)
-
-        assertThat(buffer.getLux(0)).isEqualTo(0.5f)
-        assertThat(buffer.getTime(0)).isEqualTo(1000)
-        assertThat(buffer.getLux(1)).isEqualTo(0.6f)
-        assertThat(buffer.getTime(1)).isEqualTo(2000)
-        assertThat(buffer.getLux(2)).isEqualTo(0.7f)
-        assertThat(buffer.getTime(2)).isEqualTo(3000)
-        assertThat(buffer.getLux(3)).isEqualTo(0.8f)
-        assertThat(buffer.getTime(3)).isEqualTo(4000)
-    }
-
-    @Test
-    fun `test buffer clear`() {
-        buffer.push(1000, 0.5f)
-        buffer.push(2000, 0.6f)
-        buffer.push(3000, 0.7f)
-
-        buffer.clear()
-
-        assertThat(buffer.size()).isEqualTo(0)
-        assertThrows(ArrayIndexOutOfBoundsException::class.java) {
-            buffer.getLux(0)
-        }
-    }
-}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/LightSensorControllerTest.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/LightSensorControllerTest.kt
deleted file mode 100644
index 966134a..0000000
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/LightSensorControllerTest.kt
+++ /dev/null
@@ -1,432 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.display.brightness
-
-import android.hardware.Sensor
-import android.hardware.SensorEvent
-import android.hardware.SensorEventListener
-import android.os.Handler
-import androidx.test.filters.SmallTest
-import com.android.internal.os.Clock
-import com.android.server.display.TestUtils
-import com.android.server.display.brightness.LightSensorController.Injector
-import com.android.server.display.brightness.LightSensorController.LightSensorControllerConfig
-import com.android.server.testutils.OffsettableClock
-import com.android.server.testutils.TestHandler
-import com.google.common.truth.Truth.assertThat
-import kotlin.math.ceil
-import kotlin.test.assertEquals
-import org.junit.Assert.assertArrayEquals
-import org.junit.Assert.assertTrue
-import org.junit.Test
-
-private const val FLOAT_TOLERANCE = 0.001f
-
-@SmallTest
-class LightSensorControllerTest {
-
-    private val testHandler = TestHandler(null)
-    private val testInjector = TestInjector()
-
-    @Test
-    fun `test ambient light horizon`() {
-        val lightSensorController = LightSensorController(
-            createLightSensorControllerConfig(
-                lightSensorWarmUpTimeConfig = 0, // no warmUp time, can use first event
-                brighteningLightDebounceConfig = 0,
-                darkeningLightDebounceConfig = 0,
-                ambientLightHorizonShort = 1000,
-                ambientLightHorizonLong = 2000
-            ), testInjector, testHandler)
-
-        var reportedAmbientLux = 0f
-        lightSensorController.setListener { lux ->
-            reportedAmbientLux = lux
-        }
-        lightSensorController.enableLightSensorIfNeeded()
-
-        assertThat(testInjector.sensorEventListener).isNotNull()
-
-        val timeIncrement = 500L
-        // set ambient lux to low
-        // t = 0
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(0f))
-
-        // t = 500
-        //
-        testInjector.clock.fastForward(timeIncrement)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(0f))
-
-        // t = 1000
-        testInjector.clock.fastForward(timeIncrement)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(0f))
-        assertEquals(0f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // t = 1500
-        testInjector.clock.fastForward(timeIncrement)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(0f))
-        assertEquals(0f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // t = 2000
-        // ensure that our reading is at 0.
-        testInjector.clock.fastForward(timeIncrement)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(0f))
-        assertEquals(0f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // t = 2500
-        // first 10000 lux sensor event reading
-        testInjector.clock.fastForward(timeIncrement)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(10_000f))
-        assertTrue(reportedAmbientLux > 0f)
-        assertTrue(reportedAmbientLux < 10_000f)
-
-        // t = 3000
-        // lux reading should still not yet be 10000.
-        testInjector.clock.fastForward(timeIncrement)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(10_000f))
-        assertTrue(reportedAmbientLux > 0)
-        assertTrue(reportedAmbientLux < 10_000f)
-
-        // t = 3500
-        testInjector.clock.fastForward(timeIncrement)
-        // at short horizon,  first value outside will be used in calculation (t = 2000)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(10_000f))
-        assertTrue(reportedAmbientLux > 0f)
-        assertTrue(reportedAmbientLux < 10_000f)
-
-        // t = 4000
-        // lux has been high (10000) for more than 1000ms.
-        testInjector.clock.fastForward(timeIncrement)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(10_000f))
-        assertEquals(10_000f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // t = 4500
-        testInjector.clock.fastForward(timeIncrement)
-        // short horizon is high, long horizon is high too
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(10_000f))
-        assertEquals(10_000f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // t = 5000
-        testInjector.clock.fastForward(timeIncrement)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(0f))
-        assertTrue(reportedAmbientLux > 0f)
-        assertTrue(reportedAmbientLux < 10_000f)
-
-        // t = 5500
-        testInjector.clock.fastForward(timeIncrement)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(0f))
-        assertTrue(reportedAmbientLux > 0f)
-        assertTrue(reportedAmbientLux < 10_000f)
-
-        // t = 6000
-        testInjector.clock.fastForward(timeIncrement)
-        // at short horizon, first value outside will be used in calculation (t = 4500)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(0f))
-        assertTrue(reportedAmbientLux > 0f)
-        assertTrue(reportedAmbientLux < 10_000f)
-
-        // t = 6500
-        testInjector.clock.fastForward(timeIncrement)
-        // ambient lux goes to 0
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(0f))
-        assertEquals(0f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // only the values within the horizon should be kept
-        assertArrayEquals(floatArrayOf(10_000f, 0f, 0f, 0f, 0f),
-            lightSensorController.lastSensorValues, FLOAT_TOLERANCE)
-        assertArrayEquals(longArrayOf(4_500, 5_000, 5_500, 6_000, 6_500),
-            lightSensorController.lastSensorTimestamps)
-    }
-
-    @Test
-    fun `test brightening debounce`() {
-        val lightSensorController = LightSensorController(
-            createLightSensorControllerConfig(
-                lightSensorWarmUpTimeConfig = 0, // no warmUp time, can use first event
-                brighteningLightDebounceConfig = 1500,
-                ambientLightHorizonShort = 0, // only last value will be used for lux calculation
-                ambientLightHorizonLong = 10_000,
-                // brightening threshold is set to previous lux value
-                ambientBrightnessThresholds = createHysteresisLevels(
-                    brighteningThresholdLevels = floatArrayOf(),
-                    brighteningThresholdsPercentages = floatArrayOf(),
-                    minBrighteningThreshold = 0f
-                )
-            ), testInjector, testHandler)
-        lightSensorController.setIdleMode(false)
-
-        var reportedAmbientLux = 0f
-        lightSensorController.setListener { lux ->
-            reportedAmbientLux = lux
-        }
-        lightSensorController.enableLightSensorIfNeeded()
-
-        assertThat(testInjector.sensorEventListener).isNotNull()
-
-        // t0 (0)
-        // Initial lux, initial brightening threshold
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(1200f))
-        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // t1 (1000)
-        // Lux increase, first brightening event
-        testInjector.clock.fastForward(1000)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(1800f))
-        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // t2 (2000) (t2 - t1 < brighteningLightDebounceConfig)
-        // Lux increase, but isn't steady yet
-        testInjector.clock.fastForward(1000)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(2000f))
-        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // t3 (3000) (t3 - t1 < brighteningLightDebounceConfig)
-        // Lux increase, but isn't steady yet
-        testInjector.clock.fastForward(1000)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(2200f))
-        assertEquals(2200f, reportedAmbientLux, FLOAT_TOLERANCE)
-    }
-
-    @Test
-    fun `test sensor readings`() {
-        val ambientLightHorizonLong = 2_500
-        val lightSensorController = LightSensorController(
-            createLightSensorControllerConfig(
-                ambientLightHorizonLong = ambientLightHorizonLong
-            ), testInjector, testHandler)
-        lightSensorController.setListener { }
-        lightSensorController.setIdleMode(false)
-        lightSensorController.enableLightSensorIfNeeded()
-
-        // Choose values such that the ring buffer's capacity is extended and the buffer is pruned
-        val increment = 11
-        var lux = 5000
-        for (i in 0 until 1000) {
-            lux += increment
-            testInjector.clock.fastForward(increment.toLong())
-            testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(lux.toFloat()))
-        }
-
-        val valuesCount = ceil(ambientLightHorizonLong.toDouble() / increment + 1).toInt()
-        val sensorValues = lightSensorController.lastSensorValues
-        val sensorTimestamps = lightSensorController.lastSensorTimestamps
-
-        // Only the values within the horizon should be kept
-        assertEquals(valuesCount, sensorValues.size)
-        assertEquals(valuesCount, sensorTimestamps.size)
-
-        var sensorTimestamp = testInjector.clock.now()
-        for (i in valuesCount - 1 downTo 1) {
-            assertEquals(lux.toFloat(), sensorValues[i], FLOAT_TOLERANCE)
-            assertEquals(sensorTimestamp, sensorTimestamps[i])
-            lux -= increment
-            sensorTimestamp -= increment
-        }
-        assertEquals(lux.toFloat(), sensorValues[0], FLOAT_TOLERANCE)
-        assertEquals(testInjector.clock.now() - ambientLightHorizonLong, sensorTimestamps[0])
-    }
-
-    @Test
-    fun `test darkening debounce`() {
-        val lightSensorController = LightSensorController(
-            createLightSensorControllerConfig(
-                lightSensorWarmUpTimeConfig = 0, // no warmUp time, can use first event
-                darkeningLightDebounceConfig = 1500,
-                ambientLightHorizonShort = 0, // only last value will be used for lux calculation
-                ambientLightHorizonLong = 10_000,
-                // darkening threshold is set to previous lux value
-                ambientBrightnessThresholds = createHysteresisLevels(
-                    darkeningThresholdLevels = floatArrayOf(),
-                    darkeningThresholdsPercentages = floatArrayOf(),
-                    minDarkeningThreshold = 0f
-                )
-            ), testInjector, testHandler)
-
-        lightSensorController.setIdleMode(false)
-
-        var reportedAmbientLux = 0f
-        lightSensorController.setListener { lux ->
-            reportedAmbientLux = lux
-        }
-        lightSensorController.enableLightSensorIfNeeded()
-
-        assertThat(testInjector.sensorEventListener).isNotNull()
-
-        // t0 (0)
-        // Initial lux, initial darkening threshold
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(1200f))
-        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // t1 (1000)
-        // Lux decreased, first darkening event
-        testInjector.clock.fastForward(1000)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(800f))
-        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // t2 (2000) (t2 - t1 < darkeningLightDebounceConfig)
-        // Lux decreased, but isn't steady yet
-        testInjector.clock.fastForward(1000)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(500f))
-        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // t3 (3000) (t3 - t1 < darkeningLightDebounceConfig)
-        // Lux decreased, but isn't steady yet
-        testInjector.clock.fastForward(1000)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(400f))
-        assertEquals(400f, reportedAmbientLux, FLOAT_TOLERANCE)
-    }
-
-    @Test
-    fun `test brightening debounce in idle mode`() {
-        val lightSensorController = LightSensorController(
-            createLightSensorControllerConfig(
-                lightSensorWarmUpTimeConfig = 0, // no warmUp time, can use first event
-                brighteningLightDebounceConfigIdle = 1500,
-                ambientLightHorizonShort = 0, // only last value will be used for lux calculation
-                ambientLightHorizonLong = 10_000,
-                // brightening threshold is set to previous lux value
-                ambientBrightnessThresholdsIdle = createHysteresisLevels(
-                    brighteningThresholdLevels = floatArrayOf(),
-                    brighteningThresholdsPercentages = floatArrayOf(),
-                    minBrighteningThreshold = 0f
-                )
-            ), testInjector, testHandler)
-        lightSensorController.setIdleMode(true)
-
-        var reportedAmbientLux = 0f
-        lightSensorController.setListener { lux ->
-            reportedAmbientLux = lux
-        }
-        lightSensorController.enableLightSensorIfNeeded()
-
-        assertThat(testInjector.sensorEventListener).isNotNull()
-
-        // t0 (0)
-        // Initial lux, initial brightening threshold
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(1200f))
-        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // t1 (1000)
-        // Lux increase, first brightening event
-        testInjector.clock.fastForward(1000)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(1800f))
-        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // t2 (2000) (t2 - t1 < brighteningLightDebounceConfigIdle)
-        // Lux increase, but isn't steady yet
-        testInjector.clock.fastForward(1000)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(2000f))
-        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // t3 (3000) (t3 - t1 < brighteningLightDebounceConfigIdle)
-        // Lux increase, but isn't steady yet
-        testInjector.clock.fastForward(1000)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(2200f))
-        assertEquals(2200f, reportedAmbientLux, FLOAT_TOLERANCE)
-    }
-
-    @Test
-    fun `test darkening debounce in idle mode`() {
-        val lightSensorController = LightSensorController(
-            createLightSensorControllerConfig(
-                lightSensorWarmUpTimeConfig = 0, // no warmUp time, can use first event
-                darkeningLightDebounceConfigIdle = 1500,
-                ambientLightHorizonShort = 0, // only last value will be used for lux calculation
-                ambientLightHorizonLong = 10_000,
-                // darkening threshold is set to previous lux value
-                ambientBrightnessThresholdsIdle = createHysteresisLevels(
-                    darkeningThresholdLevels = floatArrayOf(),
-                    darkeningThresholdsPercentages = floatArrayOf(),
-                    minDarkeningThreshold = 0f
-                )
-            ), testInjector, testHandler)
-
-        lightSensorController.setIdleMode(true)
-
-        var reportedAmbientLux = 0f
-        lightSensorController.setListener { lux ->
-            reportedAmbientLux = lux
-        }
-        lightSensorController.enableLightSensorIfNeeded()
-
-        assertThat(testInjector.sensorEventListener).isNotNull()
-
-        // t0 (0)
-        // Initial lux, initial darkening threshold
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(1200f))
-        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // t1 (1000)
-        // Lux decreased, first darkening event
-        testInjector.clock.fastForward(1000)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(800f))
-        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // t2 (2000) (t2 - t1 < darkeningLightDebounceConfigIdle)
-        // Lux decreased, but isn't steady yet
-        testInjector.clock.fastForward(1000)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(500f))
-        assertEquals(1200f, reportedAmbientLux, FLOAT_TOLERANCE)
-
-        // t3 (3000) (t3 - t1 < darkeningLightDebounceConfigIdle)
-        // Lux decreased, but isn't steady yet
-        testInjector.clock.fastForward(1000)
-        testInjector.sensorEventListener!!.onSensorChanged(sensorEvent(400f))
-        assertEquals(400f, reportedAmbientLux, FLOAT_TOLERANCE)
-    }
-
-
-    private fun sensorEvent(value: Float) = SensorEvent(
-        testInjector.testSensor, 0, 0, floatArrayOf(value)
-    )
-
-    private class TestInjector : Injector {
-        val testSensor: Sensor = TestUtils.createSensor(Sensor.TYPE_LIGHT, "Light Sensor")
-        val clock: OffsettableClock = OffsettableClock.Stopped()
-
-        var sensorEventListener: SensorEventListener? = null
-        override fun getClock(): Clock {
-            return object : Clock() {
-                override fun uptimeMillis(): Long {
-                    return clock.now()
-                }
-            }
-        }
-
-        override fun getLightSensor(config: LightSensorControllerConfig): Sensor {
-            return testSensor
-        }
-
-        override fun registerLightSensorListener(
-            listener: SensorEventListener,
-            sensor: Sensor,
-            rate: Int,
-            handler: Handler
-        ): Boolean {
-            sensorEventListener = listener
-            return true
-        }
-
-        override fun unregisterLightSensorListener(listener: SensorEventListener) {
-            sensorEventListener = null
-        }
-
-        override fun getTag(): String {
-            return "LightSensorControllerTest"
-        }
-    }
-}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/TestUtils.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/TestUtils.kt
deleted file mode 100644
index 1328f5f..0000000
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/TestUtils.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.server.display.brightness
-
-import com.android.server.display.HysteresisLevels
-import com.android.server.display.config.SensorData
-
-@JvmOverloads
-fun createLightSensorControllerConfig(
-    initialSensorRate: Int = 1,
-    normalSensorRate: Int = 2,
-    resetAmbientLuxAfterWarmUpConfig: Boolean = true,
-    ambientLightHorizonShort: Int = 1,
-    ambientLightHorizonLong: Int = 10_000,
-    lightSensorWarmUpTimeConfig: Int = 0,
-    weightingIntercept: Int = 10_000,
-    ambientBrightnessThresholds: HysteresisLevels = createHysteresisLevels(),
-    ambientBrightnessThresholdsIdle: HysteresisLevels = createHysteresisLevels(),
-    brighteningLightDebounceConfig: Long = 100_000,
-    darkeningLightDebounceConfig: Long = 100_000,
-    brighteningLightDebounceConfigIdle: Long = 100_000,
-    darkeningLightDebounceConfigIdle: Long = 100_000,
-    ambientLightSensor: SensorData = SensorData()
-) = LightSensorController.LightSensorControllerConfig(
-    initialSensorRate,
-    normalSensorRate,
-    resetAmbientLuxAfterWarmUpConfig,
-    ambientLightHorizonShort,
-    ambientLightHorizonLong,
-    lightSensorWarmUpTimeConfig,
-    weightingIntercept,
-    ambientBrightnessThresholds,
-    ambientBrightnessThresholdsIdle,
-    brighteningLightDebounceConfig,
-    darkeningLightDebounceConfig,
-    brighteningLightDebounceConfigIdle,
-    darkeningLightDebounceConfigIdle,
-    ambientLightSensor
-)
-
-fun createHysteresisLevels(
-    brighteningThresholdsPercentages: FloatArray = floatArrayOf(),
-    darkeningThresholdsPercentages: FloatArray = floatArrayOf(),
-    brighteningThresholdLevels: FloatArray = floatArrayOf(),
-    darkeningThresholdLevels: FloatArray = floatArrayOf(),
-    minDarkeningThreshold: Float = 0f,
-    minBrighteningThreshold: Float = 0f,
-    potentialOldBrightnessRange: Boolean = false
-) = HysteresisLevels(
-    brighteningThresholdsPercentages,
-    darkeningThresholdsPercentages,
-    brighteningThresholdLevels,
-    darkeningThresholdLevels,
-    minDarkeningThreshold,
-    minBrighteningThreshold,
-    potentialOldBrightnessRange
-)
\ No newline at end of file
diff --git a/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceContentTest.java b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceContentTest.java
index 2366f56..7aafa8e 100644
--- a/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceContentTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceContentTest.java
@@ -24,9 +24,11 @@
 
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyZeroInteractions;
 
+import android.content.pm.PackageManagerInternal;
 import android.media.projection.MediaProjectionInfo;
 import android.media.projection.MediaProjectionManager;
 import android.os.Binder;
@@ -72,6 +74,7 @@
 
     @Mock private WindowManagerInternal mWindowManager;
     @Mock private MediaProjectionManager mProjectionManager;
+    @Mock private PackageManagerInternal mPackageManagerInternal;
     private MediaProjectionInfo mMediaProjectionInfo;
 
     @Captor
@@ -91,7 +94,7 @@
         mSensitiveContentProtectionManagerService =
                 new SensitiveContentProtectionManagerService(mContext);
         mSensitiveContentProtectionManagerService.init(mProjectionManager, mWindowManager,
-                new ArraySet<>(Set.of(mExemptedScreenRecorderPackage)));
+                mPackageManagerInternal, new ArraySet<>(Set.of(mExemptedScreenRecorderPackage)));
         verify(mProjectionManager).addCallback(mMediaProjectionCallbackCaptor.capture(), any());
         mMediaPorjectionCallback = mMediaProjectionCallbackCaptor.getValue();
         mMediaProjectionInfo =
@@ -146,6 +149,20 @@
     }
 
     @Test
+    public void testAutofillServicePackageExemption() {
+        String testAutofillService = mScreenRecorderPackage + "/com.example.SampleAutofillService";
+        int userId = Process.myUserHandle().getIdentifier();
+        Settings.Secure.putStringForUser(mContext.getContentResolver(),
+                Settings.Secure.AUTOFILL_SERVICE, testAutofillService , userId);
+
+        mMediaPorjectionCallback.onStart(mMediaProjectionInfo);
+        mSensitiveContentProtectionManagerService.setSensitiveContentProtection(
+                mPackageInfo.getWindowToken(), mPackageInfo.getPkg(), mPackageInfo.getUid(), true);
+        verify(mWindowManager, never())
+                .addBlockScreenCaptureForApps(mPackageInfoCaptor.capture());
+    }
+
+    @Test
     public void testDeveloperOptionDisableFeature() {
         mockDisabledViaDeveloperOption();
         mMediaProjectionCallbackCaptor.getValue().onStart(mMediaProjectionInfo);
diff --git a/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceNotificationTest.java b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceNotificationTest.java
index e74fe29..5065144 100644
--- a/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceNotificationTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/SensitiveContentProtectionManagerServiceNotificationTest.java
@@ -31,6 +31,7 @@
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
+import android.content.pm.PackageManagerInternal;
 import android.media.projection.MediaProjectionInfo;
 import android.media.projection.MediaProjectionManager;
 import android.platform.test.annotations.RequiresFlagsEnabled;
@@ -104,6 +105,9 @@
     private WindowManagerInternal mWindowManager;
 
     @Mock
+    private PackageManagerInternal mPackageManagerInternal;
+
+    @Mock
     private StatusBarNotification mNotification1;
 
     @Mock
@@ -141,7 +145,7 @@
         setupSensitiveNotification();
 
         mSensitiveContentProtectionManagerService.init(mProjectionManager, mWindowManager,
-                new ArraySet<>(Set.of(EXEMPTED_SCREEN_RECORDER_PACKAGE)));
+                mPackageManagerInternal, new ArraySet<>(Set.of(EXEMPTED_SCREEN_RECORDER_PACKAGE)));
 
         // Obtain useful mMediaProjectionCallback
         verify(mProjectionManager).addCallback(mMediaProjectionCallbackCaptor.capture(), any());
diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
index 9975221..ce5cee0 100644
--- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
@@ -137,6 +137,8 @@
 import android.net.Uri;
 import android.os.BatteryManager;
 import android.os.Bundle;
+import android.os.Environment;
+import android.os.FileUtils;
 import android.os.Handler;
 import android.os.HandlerExecutor;
 import android.os.IBinder;
@@ -149,6 +151,7 @@
 import android.os.ServiceManager;
 import android.os.SystemProperties;
 import android.os.UserHandle;
+import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.annotations.Presubmit;
 import android.platform.test.flag.junit.SetFlagsRule;
@@ -159,6 +162,7 @@
 import android.util.Log;
 import android.util.SparseArray;
 
+import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.dx.mockito.inline.extended.MockedVoidMethod;
@@ -183,6 +187,7 @@
 
 import libcore.util.EmptyArray;
 
+import org.junit.After;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -194,6 +199,7 @@
 import org.mockito.quality.Strictness;
 import org.mockito.stubbing.Answer;
 
+import java.io.File;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
@@ -224,6 +230,7 @@
     private ActivityManager.UidFrozenStateChangedCallback mUidFrozenStateCallback;
     private IAppOpsCallback mIAppOpsCallback;
     private IAlarmManager mBinder;
+    private File mTestDir;
     @Mock
     private Context mMockContext;
     @Mock
@@ -413,6 +420,7 @@
             .mockStatic(PermissionManagerService.class)
             .mockStatic(ServiceManager.class)
             .mockStatic(SystemProperties.class)
+            .mockStatic(Environment.class)
             .spyStatic(UserHandle.class)
             .afterSessionFinished(
                     () -> LocalServices.removeServiceForTest(AlarmManagerInternal.class))
@@ -429,7 +437,8 @@
      */
     private void disableFlagsNotSetByAnnotation() {
         try {
-            mSetFlagsRule.disableFlags(Flags.FLAG_USE_FROZEN_STATE_TO_DROP_LISTENER_ALARMS);
+            mSetFlagsRule.disableFlags(Flags.FLAG_USE_FROZEN_STATE_TO_DROP_LISTENER_ALARMS,
+                    Flags.FLAG_START_USER_BEFORE_SCHEDULED_ALARMS);
         } catch (FlagSetException fse) {
             // Expected if the test about to be run requires this enabled.
         }
@@ -460,7 +469,10 @@
         when(mUsageStatsManagerInternal.getAppStandbyBucket(eq(TEST_CALLING_PACKAGE),
                 eq(TEST_CALLING_USER), anyLong())).thenReturn(STANDBY_BUCKET_ACTIVE);
         doReturn(Looper.getMainLooper()).when(Looper::myLooper);
-
+        mTestDir = new File(InstrumentationRegistry.getInstrumentation().getTargetContext()
+                .getFilesDir(), "alarmsTestDir");
+        mTestDir.mkdirs();
+        doReturn(mTestDir).when(Environment::getDataSystemDirectory);
         when(mMockContext.getContentResolver()).thenReturn(mContentResolver);
 
         doReturn(mDeviceConfigKeys).when(mDeviceConfigProperties).getKeyset();
@@ -579,6 +591,12 @@
         setTestableQuotas();
     }
 
+    @After
+    public void tearDown() {
+        // Clean up test dir to remove persisted user files.
+        FileUtils.deleteContentsAndDir(mTestDir);
+    }
+
     private void setTestAlarm(int type, long triggerTime, PendingIntent operation) {
         setTestAlarm(type, triggerTime, operation, 0, FLAG_STANDALONE, TEST_CALLING_UID);
     }
@@ -3792,6 +3810,7 @@
     }
 
     @EnableFlags(Flags.FLAG_USE_FROZEN_STATE_TO_DROP_LISTENER_ALARMS)
+    @DisableFlags(Flags.FLAG_START_USER_BEFORE_SCHEDULED_ALARMS)
     @Test
     public void exactListenerAlarmsRemovedOnFrozen() {
         mockChangeEnabled(EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED, true);
@@ -3823,6 +3842,7 @@
     }
 
     @EnableFlags(Flags.FLAG_USE_FROZEN_STATE_TO_DROP_LISTENER_ALARMS)
+    @DisableFlags(Flags.FLAG_START_USER_BEFORE_SCHEDULED_ALARMS)
     @Test
     public void alarmCountOnListenerFrozen() {
         mockChangeEnabled(EXACT_LISTENER_ALARMS_DROPPED_ON_CACHED, true);
diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java
new file mode 100644
index 0000000..5d3e499
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/UserWakeupStoreTest.java
@@ -0,0 +1,170 @@
+/*
+ * 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.alarm;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
+import static com.android.server.alarm.UserWakeupStore.BUFFER_TIME_MS;
+import static com.android.server.alarm.UserWakeupStore.USER_START_TIME_DEVIATION_LIMIT_MS;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.os.Environment;
+import android.os.FileUtils;
+import android.os.SystemClock;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.os.BackgroundThread;
+import com.android.modules.utils.testing.ExtendedMockitoRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.concurrent.ExecutorService;
+
+@RunWith(AndroidJUnit4.class)
+public class UserWakeupStoreTest {
+    private static final int USER_ID_1 = 10;
+    private static final int USER_ID_2 = 11;
+    private static final int USER_ID_3 = 12;
+    private static final long TEST_TIMESTAMP = 150_000;
+    private static final File TEST_SYSTEM_DIR = new File(InstrumentationRegistry
+            .getInstrumentation().getContext().getDataDir(), "alarmsTestDir");
+    private static final File ROOT_DIR = new File(TEST_SYSTEM_DIR, UserWakeupStore.ROOT_DIR_NAME);
+    private ExecutorService mMockExecutorService = null;
+    UserWakeupStore mUserWakeupStore;
+
+    @Rule
+    public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this)
+            .mockStatic(Environment.class)
+            .mockStatic(BackgroundThread.class)
+            .build();
+
+    @Before
+    public void setUp() {
+        TEST_SYSTEM_DIR.mkdirs();
+        doReturn(TEST_SYSTEM_DIR).when(Environment::getDataSystemDirectory);
+        mMockExecutorService = Mockito.mock(ExecutorService.class);
+        Mockito.doAnswer((invocation) -> {
+            Runnable task = invocation.getArgument(0);
+            task.run();
+            return null;
+        }).when(mMockExecutorService).execute(Mockito.any(Runnable.class));
+        doReturn(mMockExecutorService).when(BackgroundThread::getExecutor);
+        mUserWakeupStore = new UserWakeupStore();
+        spyOn(mUserWakeupStore);
+        mUserWakeupStore.init();
+    }
+
+    @After
+    public void tearDown() {
+        // Clean up test dir to remove persisted user files.
+        FileUtils.deleteContentsAndDir(TEST_SYSTEM_DIR);
+    }
+
+    @Test
+    public void testAddWakeups() {
+        mUserWakeupStore.addUserWakeup(USER_ID_1, TEST_TIMESTAMP - 19_000);
+        mUserWakeupStore.addUserWakeup(USER_ID_2, TEST_TIMESTAMP - 7_000);
+        mUserWakeupStore.addUserWakeup(USER_ID_3, TEST_TIMESTAMP - 13_000);
+        assertEquals(3, mUserWakeupStore.getUserIdsToWakeup(TEST_TIMESTAMP).length);
+        ArrayList<Integer> userIds = new ArrayList<>();
+        userIds.add(USER_ID_1);
+        userIds.add(USER_ID_2);
+        userIds.add(USER_ID_3);
+        final int[] usersToWakeup = mUserWakeupStore.getUserIdsToWakeup(TEST_TIMESTAMP);
+        ArrayList<Integer> userWakeups = new ArrayList<>();
+        for (int i = 0; i < usersToWakeup.length; i++) {
+            userWakeups.add(usersToWakeup[i]);
+        }
+        Collections.sort(userIds);
+        Collections.sort(userWakeups);
+        assertEquals(userIds, userWakeups);
+
+        final File file = new File(ROOT_DIR , "usersWithAlarmClocks.xml");
+        assertTrue(file.exists());
+    }
+
+    @Test
+    public void testAddMultipleWakeupsForUser_ensureOnlyLastWakeupRemains() {
+        final long finalAlarmTime = TEST_TIMESTAMP - 13_000;
+        mUserWakeupStore.addUserWakeup(USER_ID_1, TEST_TIMESTAMP - 29_000);
+        mUserWakeupStore.addUserWakeup(USER_ID_1, TEST_TIMESTAMP - 7_000);
+        mUserWakeupStore.addUserWakeup(USER_ID_1, finalAlarmTime);
+        assertEquals(1, mUserWakeupStore.getUserIdsToWakeup(TEST_TIMESTAMP).length);
+        final long alarmTime = mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_1)
+                + BUFFER_TIME_MS;
+        assertTrue(finalAlarmTime + USER_START_TIME_DEVIATION_LIMIT_MS >= alarmTime);
+        assertTrue(finalAlarmTime - USER_START_TIME_DEVIATION_LIMIT_MS <= alarmTime);
+    }
+
+    @Test
+    public void testRemoveWakeupForUser_negativeWakeupTimeIsReturnedForUser() {
+        mUserWakeupStore.addUserWakeup(USER_ID_1, TEST_TIMESTAMP - 19_000);
+        mUserWakeupStore.addUserWakeup(USER_ID_2, TEST_TIMESTAMP - 7_000);
+        mUserWakeupStore.addUserWakeup(USER_ID_3, TEST_TIMESTAMP - 13_000);
+        assertEquals(3, mUserWakeupStore.getUserIdsToWakeup(TEST_TIMESTAMP).length);
+        mUserWakeupStore.removeUserWakeup(USER_ID_3);
+        assertEquals(-1, mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_3));
+        assertTrue(mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_2) > 0);
+    }
+
+    @Test
+    public void testGetNextUserWakeup() {
+        mUserWakeupStore.addUserWakeup(USER_ID_1, TEST_TIMESTAMP - 19_000);
+        mUserWakeupStore.addUserWakeup(USER_ID_2, TEST_TIMESTAMP - 3_000);
+        mUserWakeupStore.addUserWakeup(USER_ID_3, TEST_TIMESTAMP - 13_000);
+        assertEquals(mUserWakeupStore.getNextWakeupTime(),
+                mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_1));
+        mUserWakeupStore.removeUserWakeup(USER_ID_1);
+        assertEquals(mUserWakeupStore.getNextWakeupTime(),
+                mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_3));
+    }
+
+    @Test
+    public void testWriteAndReadUsersFromFile() {
+        mUserWakeupStore.addUserWakeup(USER_ID_1, TEST_TIMESTAMP - 19_000);
+        mUserWakeupStore.addUserWakeup(USER_ID_2, TEST_TIMESTAMP - 7_000);
+        mUserWakeupStore.addUserWakeup(USER_ID_3, TEST_TIMESTAMP - 13_000);
+        assertEquals(3, mUserWakeupStore.getUserIdsToWakeup(TEST_TIMESTAMP).length);
+        mUserWakeupStore.init();
+        final long realtime = SystemClock.elapsedRealtime();
+        assertEquals(0, mUserWakeupStore.getUserIdsToWakeup(TEST_TIMESTAMP).length);
+        assertTrue(mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_2) > realtime);
+        assertTrue(mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_1)
+                < mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_3));
+        assertTrue(mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_3)
+                < mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_2));
+        assertTrue(mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_1) - realtime
+                < BUFFER_TIME_MS + USER_START_TIME_DEVIATION_LIMIT_MS);
+        assertTrue(mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_3) - realtime
+                < 2 * BUFFER_TIME_MS + USER_START_TIME_DEVIATION_LIMIT_MS);
+        assertTrue(mUserWakeupStore.getWakeupTimeForUserForTest(USER_ID_2) - realtime
+                < 3 * BUFFER_TIME_MS + USER_START_TIME_DEVIATION_LIMIT_MS);
+    }
+    //TODO: b/330264023 - Add tests for I/O in usersWithAlarmClocks.xml.
+}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java b/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java
index 344e2c2..69a98ac 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/BrailleDisplayConnectionTest.java
@@ -27,6 +27,7 @@
 import static org.mockito.Mockito.when;
 
 import android.accessibilityservice.BrailleDisplayController;
+import android.accessibilityservice.IBrailleDisplayController;
 import android.content.Context;
 import android.os.Bundle;
 import android.os.IBinder;
@@ -174,6 +175,17 @@
         }
 
         @Test
+        public void defaultNativeScanner_getName_returnsName() {
+            String name = "My Braille Display";
+            when(mNativeInterface.getHidrawName(anyInt())).thenReturn(name);
+
+            BrailleDisplayConnection.BrailleDisplayScanner scanner =
+                    BrailleDisplayConnection.getDefaultNativeScanner(mNativeInterface);
+
+            assertThat(scanner.getName(NULL_PATH)).isEqualTo(name);
+        }
+
+        @Test
         public void write_bypassesServiceSideCheckWithLargeBuffer_disconnects() {
             Mockito.doNothing().when(mBrailleDisplayConnection).disconnect();
             mBrailleDisplayConnection.write(
@@ -201,6 +213,38 @@
             verify(mBrailleDisplayConnection).disconnect();
         }
 
+        @Test
+        public void connect_unableToGetUniq_usesNameFallback() throws Exception {
+            try {
+                IBrailleDisplayController controller =
+                        Mockito.mock(IBrailleDisplayController.class);
+                final Path path = Path.of("/dev/null");
+                final String macAddress = "00:11:22:33:AA:BB";
+                final String name = "My Braille Display";
+                final byte[] descriptor = {0x05, 0x41};
+                Bundle bd = new Bundle();
+                bd.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_HIDRAW_PATH,
+                        path.toString());
+                bd.putByteArray(BrailleDisplayController.TEST_BRAILLE_DISPLAY_DESCRIPTOR,
+                        descriptor);
+                bd.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_NAME, name);
+                bd.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH, true);
+                bd.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, null);
+                BrailleDisplayConnection.BrailleDisplayScanner scanner =
+                        mBrailleDisplayConnection.setTestData(List.of(bd));
+                // Validate that the test data is set up correctly before attempting connection:
+                assertThat(scanner.getUniqueId(path)).isNull();
+                assertThat(scanner.getName(path)).isEqualTo(name);
+
+                mBrailleDisplayConnection.connectLocked(
+                        macAddress, name, BrailleDisplayConnection.BUS_BLUETOOTH, controller);
+
+                verify(controller).onConnected(eq(mBrailleDisplayConnection), eq(descriptor));
+            } finally {
+                mBrailleDisplayConnection.disconnect();
+            }
+        }
+
         // BrailleDisplayConnection#setTestData() is used to enable CTS testing with
         // test Braille display data, but its own implementation should also be tested
         // so that issues in this helper don't cause confusing failures in CTS.
@@ -220,6 +264,9 @@
             String uniq1 = "uniq1", uniq2 = "uniq2";
             bd1.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, uniq1);
             bd2.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_UNIQUE_ID, uniq2);
+            String name1 = "name1", name2 = "name2";
+            bd1.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_NAME, name1);
+            bd2.putString(BrailleDisplayController.TEST_BRAILLE_DISPLAY_NAME, name2);
             int bus1 = BrailleDisplayConnection.BUS_USB, bus2 =
                     BrailleDisplayConnection.BUS_BLUETOOTH;
             bd1.putBoolean(BrailleDisplayController.TEST_BRAILLE_DISPLAY_BUS_BLUETOOTH,
@@ -235,6 +282,8 @@
             expect.that(scanner.getDeviceReportDescriptor(path2)).isEqualTo(desc2);
             expect.that(scanner.getUniqueId(path1)).isEqualTo(uniq1);
             expect.that(scanner.getUniqueId(path2)).isEqualTo(uniq2);
+            expect.that(scanner.getName(path1)).isEqualTo(name1);
+            expect.that(scanner.getName(path2)).isEqualTo(name2);
             expect.that(scanner.getDeviceBusType(path1)).isEqualTo(bus1);
             expect.that(scanner.getDeviceBusType(path2)).isEqualTo(bus2);
         }
diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
index 643dcec..0a2a855 100644
--- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -123,6 +123,7 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
@@ -678,6 +679,87 @@
     }
 
     /**
+     * Test that, when exceeding the maximum number of running users, a profile of the current user
+     * is not stopped.
+     */
+    @Test
+    public void testStoppingExcessRunningUsersAfterSwitch_currentProfileNotStopped()
+            throws Exception {
+        mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true,
+                /* maxRunningUsers= */ 5, /* delayUserDataLocking= */ false);
+
+        final int PARENT_ID = 200;
+        final int PROFILE1_ID = 201;
+        final int PROFILE2_ID = 202;
+        final int FG_USER_ID = 300;
+        final int BG_USER_ID = 400;
+
+        setUpUser(PARENT_ID, 0).profileGroupId = PARENT_ID;
+        setUpUser(PROFILE1_ID, UserInfo.FLAG_PROFILE).profileGroupId = PARENT_ID;
+        setUpUser(PROFILE2_ID, UserInfo.FLAG_PROFILE).profileGroupId = PARENT_ID;
+        setUpUser(FG_USER_ID, 0).profileGroupId = FG_USER_ID;
+        setUpUser(BG_USER_ID, 0).profileGroupId = UserInfo.NO_PROFILE_GROUP_ID;
+        mUserController.onSystemReady(); // To set the profileGroupIds in UserController.
+
+        assertEquals(newHashSet(
+                SYSTEM_USER_ID),
+                new HashSet<>(mUserController.getRunningUsersLU()));
+
+        int numberOfUserSwitches = 1;
+        addForegroundUserAndContinueUserSwitch(PARENT_ID, UserHandle.USER_SYSTEM,
+                numberOfUserSwitches, false);
+        mUserController.finishUserSwitch(mUserStates.get(PARENT_ID));
+        waitForHandlerToComplete(mInjector.mHandler, HANDLER_WAIT_TIME_MS);
+        assertTrue(mUserController.canStartMoreUsers());
+        assertEquals(newHashSet(
+                SYSTEM_USER_ID, PARENT_ID),
+                new HashSet<>(mUserController.getRunningUsersLU()));
+
+        assertThat(mUserController.startProfile(PROFILE1_ID, true, null)).isTrue();
+        assertEquals(newHashSet(
+                SYSTEM_USER_ID, PROFILE1_ID, PARENT_ID),
+                new HashSet<>(mUserController.getRunningUsersLU()));
+
+        numberOfUserSwitches++;
+        addForegroundUserAndContinueUserSwitch(FG_USER_ID, PARENT_ID,
+                numberOfUserSwitches, false);
+        mUserController.finishUserSwitch(mUserStates.get(FG_USER_ID));
+        waitForHandlerToComplete(mInjector.mHandler, HANDLER_WAIT_TIME_MS);
+        assertTrue(mUserController.canStartMoreUsers());
+        assertEquals(newHashSet(
+                SYSTEM_USER_ID, PROFILE1_ID, PARENT_ID, FG_USER_ID),
+                new HashSet<>(mUserController.getRunningUsersLU()));
+
+        mUserController.startUser(BG_USER_ID, USER_START_MODE_BACKGROUND);
+        assertEquals(newHashSet(
+                SYSTEM_USER_ID, PROFILE1_ID, PARENT_ID, BG_USER_ID, FG_USER_ID),
+                new HashSet<>(mUserController.getRunningUsersLU()));
+
+        // Now we exceed the maxRunningUsers parameter (of 5):
+        assertThat(mUserController.startProfile(PROFILE2_ID, true, null)).isTrue();
+        // Currently, starting a profile doesn't trigger evaluating whether we've exceeded max, so
+        // we expect no users to be stopped. This policy may change in the future. Log but no fail.
+        if (!newHashSet(SYSTEM_USER_ID, PROFILE1_ID, BG_USER_ID, PROFILE2_ID, PARENT_ID, FG_USER_ID)
+                .equals(new HashSet<>(mUserController.getRunningUsersLU()))) {
+            Log.w(TAG, "Starting a profile that exceeded max running users didn't lead to "
+                    + "expectations: " + mUserController.getRunningUsersLU());
+        }
+
+        numberOfUserSwitches++;
+        addForegroundUserAndContinueUserSwitch(PARENT_ID, FG_USER_ID,
+                numberOfUserSwitches, false);
+        mUserController.finishUserSwitch(mUserStates.get(PARENT_ID));
+        waitForHandlerToComplete(mInjector.mHandler, HANDLER_WAIT_TIME_MS);
+        // We've now done a user switch and should notice that we've exceeded the maximum number of
+        // users. The oldest background user should be stopped (BG_USER); even though PROFILE1 was
+        // older, it should not be stopped since it's a profile of the (new) current user.
+        assertFalse(mUserController.canStartMoreUsers());
+        assertEquals(newHashSet(
+                SYSTEM_USER_ID, PROFILE1_ID, PROFILE2_ID, FG_USER_ID, PARENT_ID),
+                new HashSet<>(mUserController.getRunningUsersLU()));
+    }
+
+    /**
      * Test that, in getRunningUsersLU, parents come after their profile, even if the profile was
      * started afterwards.
      */
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
index 363ae14..21251c3 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
@@ -537,7 +537,7 @@
         }).when(appWindow.mSession).setOnBackInvokedCallbackInfo(eq(appWindow.mClient), any());
 
         addToWindowMap(appWindow, true);
-        dispatcher.attachToWindow(appWindow.mSession, appWindow.mClient);
+        dispatcher.attachToWindow(appWindow.mSession, appWindow.mClient, null);
 
 
         OnBackInvokedCallback appCallback = createBackCallback(appLatch);
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 85172e0..680738b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -27,6 +27,7 @@
 import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
 import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_16_9;
 import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_3_2;
@@ -928,6 +929,17 @@
     }
 
     @Test
+    public void testIsLetterboxed_activityFromBubble_returnsFalse() {
+        setUpDisplaySizeWithApp(1000, 2500);
+        mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+        spyOn(mActivity);
+        doReturn(true).when(mActivity).getLaunchedFromBubble();
+        prepareUnresizable(mActivity, SCREEN_ORIENTATION_LANDSCAPE);
+
+        assertFalse(mActivity.areBoundsLetterboxed());
+    }
+
+    @Test
     public void testAspectRatioMatchParentBoundsAndImeAttachable() {
         setUpApp(new TestDisplayContent.Builder(mAtm, 1000, 2000).build());
         prepareUnresizable(mActivity, 2f /* maxAspect */, SCREEN_ORIENTATION_UNSPECIFIED);
@@ -1931,8 +1943,7 @@
         assertThat(mActivity.inSizeCompatMode()).isTrue();
         assertActivityMaxBoundsSandboxed();
 
-
-	final int scale = dh / dw;
+        final int scale = dh / dw;
 
         // App bounds should be dh / scale x dw / scale
         assertEquals(dw, rotatedDisplayBounds.width());
@@ -4137,6 +4148,37 @@
     }
 
     @Test
+    public void testFixedAspectRatioAppInPortraitCloseToSquareDisplay_notInSizeCompat() {
+        setUpDisplaySizeWithApp(2200, 2280);
+        mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+        final DisplayContent display = mActivity.mDisplayContent;
+        // Simulate taskbar, final app bounds are (0, 0, 2200, 2130) - landscape
+        final WindowState navbar = createWindow(null, TYPE_NAVIGATION_BAR, mDisplayContent,
+                "navbar");
+        final Binder owner = new Binder();
+        navbar.mAttrs.providedInsets = new InsetsFrameProvider[] {
+                new InsetsFrameProvider(owner, 0, WindowInsets.Type.navigationBars())
+                        .setInsetsSize(Insets.of(0, 0, 0, 150))
+        };
+        display.getDisplayPolicy().addWindowLw(navbar, navbar.mAttrs);
+        assertTrue(navbar.providesDisplayDecorInsets()
+                && display.getDisplayPolicy().updateDecorInsetsInfo());
+        display.sendNewConfiguration();
+
+        prepareMinAspectRatio(mActivity, OVERRIDE_MIN_ASPECT_RATIO_LARGE_VALUE,
+                SCREEN_ORIENTATION_LANDSCAPE);
+        // To force config to update again but with the same landscape orientation.
+        mActivity.setRequestedOrientation(SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
+
+        assertTrue(mActivity.shouldCreateCompatDisplayInsets());
+        assertNotNull(mActivity.getCompatDisplayInsets());
+        // Activity is not letterboxed for fixed orientation because orientation is respected
+        // with insets, and should not be in size compat mode
+        assertFalse(mActivity.isLetterboxedForFixedOrientationAndAspectRatio());
+        assertFalse(mActivity.inSizeCompatMode());
+    }
+
+    @Test
     public void testApplyAspectRatio_activityAlignWithParentAppVertical() {
         if (Flags.insetsDecoupledConfiguration()) {
             // TODO (b/151861875): Re-enable it. This is disabled temporarily because the config
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
index a88680a..dab966b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
@@ -833,7 +833,7 @@
         final ActivityRecord activity = new ActivityBuilder(mAtm).setTask(task).build();
         final ActivityRecord.CompatDisplayInsets compatInsets =
                 new ActivityRecord.CompatDisplayInsets(
-                        display, activity, /* fixedOrientationBounds= */ null);
+                        display, activity, /* letterboxedContainerBounds */ null);
         task.computeConfigResourceOverrides(inOutConfig, parentConfig, compatInsets);
 
         assertEquals(largerLandscapeBounds, inOutConfig.windowConfiguration.getAppBounds());
@@ -1960,6 +1960,27 @@
         verify(task).startPausing(eq(true) /* userLeaving */, anyBoolean(), any(), any());
     }
 
+    @Test
+    public void testGetBottomMostActivityInSamePackage() {
+        final String packageName = "homePackage";
+        final TaskFragmentOrganizer organizer = new TaskFragmentOrganizer(Runnable::run);
+        final Task task = new TaskBuilder(mSupervisor).setCreateActivity(false).build();
+        task.realActivity = new ComponentName(packageName, packageName + ".root_activity");
+        doNothing().when(task).sendTaskFragmentParentInfoChangedIfNeeded();
+
+        final TaskFragment fragment1 = createTaskFragmentWithEmbeddedActivity(task, organizer);
+        final ActivityRecord activityDifferentPackage =
+                new ActivityBuilder(mAtm).setTask(task).build();
+        final ActivityRecord activitySamePackage =
+                new ActivityBuilder(mAtm)
+                        .setComponent(new ComponentName(packageName, packageName + ".activity2"))
+                        .setTask(task).build();
+
+        assertEquals(fragment1.getChildAt(0), task.getBottomMostActivity());
+        assertEquals(activitySamePackage, task.getBottomMostActivityInSamePackage());
+        assertNotEquals(activityDifferentPackage, task.getBottomMostActivityInSamePackage());
+    }
+
     private Task getTestTask() {
         return new TaskBuilder(mSupervisor).setCreateActivity(true).build();
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
index 55a00fc..48fc2dc 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowContainerTests.java
@@ -799,6 +799,7 @@
         verify(child).onConfigurationChanged(any());
     }
 
+    @SuppressWarnings("SelfComparison")
     @Test
     public void testCompareTo() {
         final TestWindowContainerBuilder builder = new TestWindowContainerBuilder(mWm);
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index df32fbd..ebdd556 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -8013,6 +8013,27 @@
                 KEY_SCAN_LIMITED_SERVICE_AFTER_VOLTE_FAILURE_BOOL =
                     KEY_PREFIX + "scan_limited_service_after_volte_failure_bool";
 
+        /**
+         * This config defines {@link ImsReasonInfo} code with which the emergency call
+         * shall be retried.
+         *
+         * <p>
+         * If the reason code is one of the following, the emergency call shall be retried
+         * regardless of this configuration.
+         * <ul>
+         * <li>{@link ImsReasonInfo#CODE_LOCAL_CALL_CS_RETRY_REQUIRED}</li>
+         * <li>{@link ImsReasonInfo#CODE_LOCAL_NOT_REGISTERED}</li>
+         * <li>{@link ImsReasonInfo#CODE_SIP_ALTERNATE_EMERGENCY_CALL}</li>
+         * </ul>
+         * <p>
+         *
+         * This config is empty by default.
+         *
+         * @hide
+         */
+        public static final String KEY_IMS_REASONINFO_CODE_TO_RETRY_EMERGENCY_INT_ARRAY =
+                KEY_PREFIX + "ims_reasoninfo_code_to_retry_emergency_int_array";
+
         private static PersistableBundle getDefaults() {
             PersistableBundle defaults = new PersistableBundle();
             defaults.putBoolean(KEY_RETRY_EMERGENCY_ON_IMS_PDN_BOOL, false);
@@ -8085,6 +8106,8 @@
             defaults.putBoolean(KEY_START_QUICK_CROSS_STACK_REDIAL_TIMER_WHEN_REGISTERED_BOOL,
                     true);
             defaults.putBoolean(KEY_SCAN_LIMITED_SERVICE_AFTER_VOLTE_FAILURE_BOOL, false);
+            defaults.putIntArray(KEY_IMS_REASONINFO_CODE_TO_RETRY_EMERGENCY_INT_ARRAY,
+                    new int[0]);
 
             return defaults;
         }
@@ -9836,7 +9859,7 @@
      * An integer key holds the time interval for refreshing or re-querying the satellite
      * entitlement status from the entitlement server to ensure it is the latest.
      *
-     * The default value is 30 days (1 month).
+     * The default value is 7 days.
      */
     @FlaggedApi(Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG)
     public static final String KEY_SATELLITE_ENTITLEMENT_STATUS_REFRESH_DAYS_INT =
@@ -9854,6 +9877,19 @@
             "satellite_entitlement_supported_bool";
 
     /**
+     * Indicates the appName that is used when querying the entitlement server for satellite.
+     *
+     * The default value is androidSatmode.
+     *
+     * Reference: GSMA TS.43-v11, 2.8.5 Fast Authentication and Token Management.
+     * `app_name` is an optional attribute in the request and may vary depending on the carrier
+     * requirement.
+     * @hide
+     */
+    public static final String KEY_SATELLITE_ENTITLEMENT_APP_NAME_STRING =
+            "satellite_entitlement_app_name_string";
+
+    /**
      * Indicating whether DUN APN should be disabled when the device is roaming. In that case,
      * the default APN (i.e. internet) will be used for tethering.
      *
@@ -10995,8 +11031,9 @@
                 CellSignalStrengthLte.USE_RSRP);
         sDefaults.putBoolean(KEY_REMOVE_SATELLITE_PLMN_IN_MANUAL_NETWORK_SCAN_BOOL, true);
         sDefaults.putBoolean(KEY_OVERRIDE_WFC_ROAMING_MODE_WHILE_USING_NTN_BOOL, true);
-        sDefaults.putInt(KEY_SATELLITE_ENTITLEMENT_STATUS_REFRESH_DAYS_INT, 30);
+        sDefaults.putInt(KEY_SATELLITE_ENTITLEMENT_STATUS_REFRESH_DAYS_INT, 7);
         sDefaults.putBoolean(KEY_SATELLITE_ENTITLEMENT_SUPPORTED_BOOL, false);
+        sDefaults.putString(KEY_SATELLITE_ENTITLEMENT_APP_NAME_STRING, "androidSatmode");
         sDefaults.putBoolean(KEY_DISABLE_DUN_APN_WHILE_ROAMING_WITH_PRESET_APN_BOOL, false);
         sDefaults.putString(KEY_DEFAULT_PREFERRED_APN_NAME_STRING, "");
         sDefaults.putBoolean(KEY_SUPPORTS_CALL_COMPOSER_BOOL, false);
diff --git a/wifi/tests/Android.bp b/wifi/tests/Android.bp
index 5a0f742..1d3e4bd 100644
--- a/wifi/tests/Android.bp
+++ b/wifi/tests/Android.bp
@@ -39,7 +39,7 @@
         "androidx.test.core",
         "frameworks-base-testutils",
         "guava",
-        "mockito-target-minus-junit4",
+        "mockito-target-extended-minus-junit4",
         "truth",
     ],
 
@@ -48,6 +48,12 @@
         "android.test.base",
     ],
 
+    // Required by Extended Mockito
+    jni_libs: [
+        "libdexmakerjvmtiagent",
+        "libstaticjvmtiagent",
+    ],
+
     test_suites: [
         "general-tests",
     ],
diff --git a/wifi/tests/AndroidManifest.xml b/wifi/tests/AndroidManifest.xml
index 18986fc..66056e5 100644
--- a/wifi/tests/AndroidManifest.xml
+++ b/wifi/tests/AndroidManifest.xml
@@ -18,7 +18,7 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="android.net.wifi.nonupdatable.test">
 
-    <application>
+    <application android:debuggable="true">
         <uses-library android:name="android.test.runner"/>
         <activity android:label="WifiTestDummyLabel"
              android:name="WifiTestDummyName"